📚

Auth0から取得したトークンがインメモリ保存されている場合の挙動について

2023/08/20に公開

はじめに

先日るとVueを使ったSPAのアプリケーションにおいて、インメモリにAuth0から受け取ったトークンなどの情報が保存されている場合、情報を取得するgetAccessTokenSilentlyメソッドの挙動について疑問を持ちました。
話題になった際は、ブラウザとかにキャッシュしてそこから取得しているのかな?ぐらいの理解しか私はしていませんでした。
詳細が気になり調査をしたところある程度理解できたなと感じたので、この記事を展開しました。
ただ、一サービスの一機能の話でかつ、備忘録的な内容なのはご留意ください。

結論

  • ドキュメントを参照するとトークンなどの情報はインメモリに保存している場合更新したり、別タブを開いたりしたら、インメモリからなくなることが分かります。
  • これはトークンの保存場所をsrc/cache/cache-memory.tsで定義されているInMemoryCacheクラスに保存しているからです。
  • InMemoryCacheクラスは新しくアプリを開いたり、更新をしたりするタイミングでインスタンス化をするので、Vueの場合main.tsが起動する処理の場合データは初期化されます。
  • 以上のことから、getAccessTokenSilentlyメソッドは、InMemoryCacheクラスから値を取得していることが分かります。
    これからの話はコードをベタベタ書いていますが、言っていることは上記結論を説明するためです。
    そのことを念頭に置いていただけますと幸いです。

コードを追う

まずgetAccessTokenSilentlyメソッドの記載場所ですが、こちらはauth0-vue/src/plugin.tsに存在します。
上記メソッド内の処理を見るとAuth0ClientクラスのgetTokenSilentlyメソッドを使用していることがわかります。
Auth0Client.tsにあるgetTokenSilentlyメソッドは_getTokenSilentlyメソッドを使用しており、_getTokenSilentlyメソッドはトークン情報を取得するのに中心となる処理です。
それでは**Auth0Client.ts**にあるアクセストークンを取得する_getTokenSilentlyメソッドの全体像を展開します。

//src/Auth0Client.ts
private async _getTokenSilently(
    options: GetTokenSilentlyOptions & {
      authorizationParams: AuthorizationParams & { scope: string };
    }
  ): Promise<undefined | GetTokenSilentlyVerboseResponse> {
    const { cacheMode, ...getTokenOptions } = options;

    // Check the cache before acquiring the lock to avoid the latency of
    // `lock.acquireLock` when the cache is populated.
    if (cacheMode !== 'off') {
      const entry = await this._getEntryFromCache({
        scope: getTokenOptions.authorizationParams.scope,
        audience: getTokenOptions.authorizationParams.audience || 'default',
        clientId: this.options.clientId
      });

      if (entry) {
        return entry;
      }
    }

    if (cacheMode === 'cache-only') {
      return;
    }

    if (
      await retryPromise(
        () => lock.acquireLock(GET_TOKEN_SILENTLY_LOCK_KEY, 5000),
        10
      )
    ) {
      try {
        window.addEventListener('pagehide', this._releaseLockOnPageHide);

        // Check the cache a second time, because it may have been populated
        // by a previous call while this call was waiting to acquire the lock.
        if (cacheMode !== 'off') {
          const entry = await this._getEntryFromCache({
            scope: getTokenOptions.authorizationParams.scope,
            audience: getTokenOptions.authorizationParams.audience || 'default',
            clientId: this.options.clientId
          });

          if (entry) {
            return entry;
          }
        }

        const authResult = this.options.useRefreshTokens
          ? await this._getTokenUsingRefreshToken(getTokenOptions)
          : await this._getTokenFromIFrame(getTokenOptions);

        const { id_token, access_token, oauthTokenScope, expires_in } =
          authResult;

        return {
          id_token,
          access_token,
          ...(oauthTokenScope ? { scope: oauthTokenScope } : null),
          expires_in
        };
      } finally {
        await lock.releaseLock(GET_TOKEN_SILENTLY_LOCK_KEY);
        window.removeEventListener('pagehide', this._releaseLockOnPageHide);
      }
    } else {
      throw new TimeoutError();
    }
  }

今回の内容に関係がある部分は下記部分の処理です。

const entry = await this._getEntryFromCache({
        scope: getTokenOptions.authorizationParams.scope,
        audience: getTokenOptions.authorizationParams.audience || 'default',
        clientId: this.options.clientId
      });

そのため、同ファイルにある_getEntryFromCacheメソッドを見てみます。

private async _getEntryFromCache({
    scope,
    audience,
    clientId
  }: {
    scope: string;
    audience: string;
    clientId: string;
  }): Promise<undefined | GetTokenSilentlyVerboseResponse> {
    const entry = await this.cacheManager.get(
      new CacheKey({
        scope,
        audience,
        clientId
      }),
      60 // get a new token if within 60 seconds of expiring
    );

    if (entry && entry.access_token) {
      const { access_token, oauthTokenScope, expires_in } = entry as CacheEntry;
      const cache = await this._getIdTokenFromCache();
      return (
        cache && {
          id_token: cache.id_token,
          access_token,
          ...(oauthTokenScope ? { scope: oauthTokenScope } : null),
          expires_in
        }
      );
    }
  }

このメソッドで実際にトークンなどの情報を取得しているのはsrc/cache/cache-manager.tsにあるCacheManagerクラスのgetメソッドです。
CacheManagerクラスのgetメソッドでトークンなどの情報を取得している部分は下記コードです。

let wrappedEntry = await this.cache.get<WrappedCacheEntry>(
      cacheKey.toKey()
    );

cacheプロパティはICacheというインターフェースが定義されています。

export interface ICache {
  set<T = Cacheable>(key: string, entry: T): MaybePromise<void>;
  get<T = Cacheable>(key: string): MaybePromise<T | undefined>;
  remove(key: string): MaybePromise<void>;
  allKeys?(): MaybePromise<string[]>;
}

コードを辿って行くと、最後はインターフェースにたどり着くことからAuth0が受け取った情報はこのcacheプロパティが持っていることが分かります。
そのため、chacheプロパティを使用しているCacheManagerクラスの呼び出し部分を確認します。
CacheManagerクラスはAuth0Client.tsの216行目で以下のように定義しています。

this.cacheManager = new CacheManager(
      cache,
      !cache.allKeys
        ? new CacheKeyManifest(cache, this.options.clientId)
        : undefined,
      this.nowProvider
    );

第一引数のcacheはsrc/Auth0Client.tsの160行目付近で定義されています。

let cache: ICache;

    if (options.cache) {
      cache = options.cache;
    } else {
      cacheLocation = options.cacheLocation || CACHE_LOCATION_MEMORY;

      if (!cacheFactory(cacheLocation)) {
        throw new Error(`Invalid cache location "${cacheLocation}"`);
      }

      cache = cacheFactory(cacheLocation)();
    }

cacheFactoryメソッドはsrc/Auth0Client.utils.tsに以下のように定義されています。

/**
 * @ignore
 */
const cacheLocationBuilders: Record<string, () => ICache> = {
  memory: () => new InMemoryCache().enclosedCache,
  localstorage: () => new LocalStorageCache()
};

/**
 * @ignore
 */
export const cacheFactory = (location: string) => {
  return cacheLocationBuilders[location];
};

そして、memoryプロパティ内のメソッドについてはsrc/cache/cache-memory.tsで定義されているInMemoryCacheクラスのenclosedCacheメソッドを格納しています。
InMemoryCacheクラスの中身を見ると外部ファイルを基本使用していないので、InMemoryCahceがトークンを保持しているクラスだと分かります。
よって、getTokenSilentlyメソッドはインメモリから情報を取得する場合、InMemoryCahceクラスから値を取得していると言えそうです。
トークンを保存する部分はわかったので、InMemoryCahceクラスがどのタイミングでインスタンスされるかを確認していきます。
InMemoryCacheクラスを呼び出すcache = cacheFactory(cacheLocation)();はAuth0Clientクラスのコンストラクタ内に記載されています。
すなわち、InMemoryCacheがインスタンス化するのはAuth0Clientがインスタンス化されたタイミングと同一といえます。
よって、今からはAuth0Clientクラスがどのタイミングで呼ばれたかを確認します。
VueプロジェクトでAuth0を活用する場合に用いる@auth0/auth0-vueでAuth0Clientクラスを使用しているのはsrc/plugin.tsのAuth0Pluginクラスです。
そして、Auth0Pluginクラスをインスタンス化しているのはauth0-vue/src/index.tsにあるcreateAuth0関数です。
このcreateAuth0関数はクイックスタートを確認すると、Vueプロジェクトのmain.tsで呼ばれています。
以上のことから、Auth0においてトークン情報をインメモリに保存している場合、Vueプロジェクトのmain.tsが呼ばれるまでは情報を保持し続けてmain.tsが再度呼ばれたタイミングで初期化されることが分かります。
だからAuth0は更新や新しいタブで開いたときは値を保持し続けないといっていたんですね。

おわりに

今回は、Auth0がSPA用に実装したトークン情報を取得するメソッドの挙動について見ていきました。
また、そのトークンがインメモリに保存されている場合の生存期間も見ていきました。
「インメモリに保存ってなんやねん」からは脱却できたので、また一つAuth0の理解を深めることができました
Auth0の機能理解ができた点は満足ですが、今記事は文章の構成が悪く何の話をしているのかが分かりにくい文章だったので、そこは反省点です。
次は改善できたらなと思います。
ここまで読んでいただきありがとうございました。

Discussion