📖

Momento API Key と トークンの違いについて

2023/10/17に公開

先日もめんと会でドキュメントの輪読をした際に、API Key と トークンの違いについて議論がありました。少しわかりずらいので改めて解説したいと思います。
https://momentocommunity.connpass.com/

ドキュメントヒストリーなどを見ているとどうも、API Key と トークンの取り扱いや言葉の定義に変遷があり、Auth Tokenなどという表現もありますが、この記事では API Key と トークンで言葉を統一します。

API Key

マネージメントコンソールから取得可能なトークンです。マネージメントコンソールでも「トークン」と表現しているので勘違いしそうになりますが、こちらはAPI Keyです。

キャッシュ(名前空間)単位、オペレーション(読み書き)単位で設定が可能です。

基本この鍵は有効期間内はStaticであり、Rotationが推奨されています。主にバックエンドサーバからMomentoへの通信に用いられます。

リフレッシュトークンは、API Keyを更新する際に用いるものです。

トークン

フロントエンドプログラムから個別キャッシュキーを操作する際に利用するものです。
キャッシュ:名前空間
キー:キャッシュの一意識別子
アイテム:キャッシュのデータ
トークンはDisposableTokenScopesでその権限範囲を設定します。
特定のキーに対する特定のオペレーションという限られた範囲で有効になります。

  const oneKeyOneCacheToken = await authClient.generateDisposableToken(
    DisposableTokenScopes.cacheKeyReadWrite('default-cache', 'mo'),
    ExpiresIn.minutes(30)
  );

の場合、default-cacheの名前空間に存在しているmoというキーに対して、読み書きの権限が30分だけ付与される一時トークンを発行します。
特定ユーザーに対する一時的なトークンの発行をこまめに行うことで、トークンの危殆化が懸念される際に、システム全体のAPI Keyの入れ替えを防ぐことが出来ます。
以下のページに細かい定義があります。
https://docs.momentohq.com/cache/develop/api-reference/auth
Auth APIと記載されておりややこしいのですが、これはトークンです。

サンプルコード

以下でトークンのテストが可能です。

const { CacheGet, CacheSet, CacheClient, Configurations, CredentialProvider, AuthClient, DisposableTokenScopes, ExpiresIn, GenerateDisposableToken } = require('@gomomento/sdk');

async function main() {
  let momento;

  const authClient = new AuthClient({
    credentialProvider: CredentialProvider.fromString({
      apiKey: "<api key>",
    }),
  });

  // Generate a disposable token with read-write access to a specific key in one cache
  const oneKeyOneCacheToken = await authClient.generateDisposableToken(
    DisposableTokenScopes.cacheKeyReadWrite('default-cache', 'mo'),
    ExpiresIn.minutes(30)
  );
  if (oneKeyOneCacheToken instanceof GenerateDisposableToken.Success) {
    console.log('Generated a disposable API key with access to the "mo" key in the "default-cache" cache!');
    
      momento = await CacheClient.create({
      configuration: Configurations.Laptop.v1(),
      credentialProvider: CredentialProvider.fromString({
        authToken: oneKeyOneCacheToken.authToken,
      }),
      defaultTtlSeconds: 60,
    });
    console.log(`API key starts with: ${oneKeyOneCacheToken.authToken.substring(0, 20)}`);
    console.log(`Expires At: ${oneKeyOneCacheToken.expiresAt.epoch()}`);
  } else if (oneKeyOneCacheToken instanceof GenerateDisposableToken.Error) {
    throw new Error(
      `An error occurred while attempting to call generateApiKey with disposable token scope: ${oneKeyOneCacheToken.errorCode()}: ${oneKeyOneCacheToken.toString()}`
    );
  }

  console.log('Storing key=default-cache, value=FOO');
  const setResponse = await momento?.set('default-cache', 'mo', 'FOO');
  if (setResponse instanceof CacheSet.Success) {
    console.log('Key stored successfully!');
  } else {
    console.log(`Error setting key: ${setResponse?.toString()}`);
  }

  const getResponse = await momento?.get('default-cache', 'mo');
  if (getResponse instanceof CacheGet.Hit) {
    console.log(`cache hit: ${getResponse.valueString()}`);
  } else if (getResponse instanceof CacheGet.Miss) {
    console.log('cache miss');
  } else if (getResponse instanceof CacheGet.Error) {
    console.log(`Error: ${getResponse.message()}`);
  }
}

main()
  .then(() => {
    console.log('success!!');
  })
  .catch((e) => {
    console.error(`Uncaught exception while running example: ${e.message}`);
    throw e;
  });

まず以下の部分でauthClientにAPI Keyをセットします。

const authClient = new AuthClient({
    credentialProvider: CredentialProvider.fromString({
      apiKey: "<api key>",
    }),
});

トークンはAPI Keyから派生されますが、Fine-Graded Access Tokenからの派生は行えまん。以下のエラーが出ます。

Error: An error occurred while attempting to call generateApiKey with disposable token scope: INVALID_ARGUMENT_ERROR: Invalid argument passed to Momento client: 3 INVALID_ARGUMENT: invalid parameter: auth_token, Only tokens with SuperUser permissions can generate disposable tokens

次に以下の部分でトークンを発行します。

  const oneKeyOneCacheToken = await authClient.generateDisposableToken(
    DisposableTokenScopes.cacheKeyReadWrite('default-cache', 'mo'),
    ExpiresIn.minutes(30)
  );

DisposableTokenScopesdefault-cacheという名前空間に存在しているmoというキーに限定して値の読み書きを可能とします。(cacheKeyReadWrite)
またこの一時トークンは30分有効です。派生されたトークンを用いて以下でCacheClientを生成しています。

      momento = await CacheClient.create({
      configuration: Configurations.Laptop.v1(),
      credentialProvider: CredentialProvider.fromString({
        authToken: oneKeyOneCacheToken.authToken,
      }),
      defaultTtlSeconds: 60,
    });

このクライアントは、default-cacheという名前空間に存在しているmoというキーに限定して値の読み書きを30分間可能とします。試しに以下の部分の20を例えば300などにしてみるとAPI Keyと異なる値がセットされていることがわかります。

console.log(`API key starts with: ${oneKeyOneCacheToken.authToken.substring(0, 20)}`);

API Key と トークンの実体

Base64 エンコードされたJWTトークンがその実態です。
Base64エンコードすると以下のようになります。

{
"endpoint":"cell-ap-northeast-1-1.prod.a.momentohq.com",
"api_key":"<key>"
}

さらにkeyの部分をJWTでコードすると以下が出力されます。

ヘッダ
{
  "typ": "JWT",
  "alg": "HS256"
}
{
  "sub": "momentbigfun@gmail.com",
  "ver": 1,
  "exp": 1697522409,
  "p": "<token>",
  "t": "disposable"
}

disposableとなっているのがトークンです。API Keyにはtのフィールドは存在していません。

Discussion