カスタムトークン交換の早期アクセス

RFC 8693で定義されているように、カスタムトークン交換はアプリケーションが/oauth/tokenエンドポイントを呼び出したときに既存のトークンをAuth0トークンに交換できるようにします。これは以下のような高度な統合に役立ちます。

  • 別のオーディエンスのAuth0トークンを取得する

  • 外部のIDプロバイダーを統合する

  • Auth0に移行する

詳細については、「ユースケースの例とサンプルコード」をお読みください。

トークン交換を管理して必要な特定のユースケースに調整するために、1つ以上のカスタムトークン交換プロファイルを定義できます。それぞれのプロファイルでは、トランザクションにユーザー情報を提供するsubject_token_typeアクションが1対1でマッピングされます。アクション内にはカスタムコードを作成して、/oauth/tokenエンドポイントに渡されたサブジェクトトークンの復号化と検証を行うことができます。

カスタムトークン交換はユーザーの認証に使用できます。たとえば、アクション内でユースケースの認可ロジックを適用して、トランザクションにユーザーを設定できます。そうすると、Auth0がアクセストークン、IDトークンとリフレッシュトークンをユーザーに発行します。

セットアップ

アプリケーション

カスタムトークン交換を使用するには、Auth0 DashboardまたはManagement APIを使用して新しいアプリケーションを作成する必要があります。カスタムトークン交換には、複数のアプリケーションを作成して使用できます。

新しいアプリケーションを作成するには以下を行います。

1. カスタムトークン交換はデフォルトで無効に設定されます。カスタムトークン交換を有効化するには、Management APIを使用してPOST呼び出しをクライアント作成エンドポイントに対して行うか、PATCH呼び出しをクライアント更新エンドポイントに対して行います。token_exchangeallow_any_profile_of_type属性を["custom_authentication"]に設定します。

{
  "token_exchange": {
    "allow_any_profile_of_type": ["custom_authentication"]
  }
}

Was this helpful?

/

2. アプリケーションにデータベース接続またはエンタープライズ接続を有効化して、カスタムトークン交換で使用できるようにします。

3. アプリケーションに[First-Party(ファーストパーティー)]フラグがあり、[Dashboard]>[Applications(アプリケーション)]>[Advanced Settings(詳細設定)]>[OAuth]で[OIDC Conformant(OIDC準拠)]として構成されていることを確認します。

アプリケーションを作成したら、client_idclient_secretを必ずメモして、後で/oauth/tokenエンドポイントを呼び出すときに使用できるようにします。

カスタムトークン交換プロファイル

カスタムトークン交換プロファイルはそれぞれsubject_token_typeにマッピングされ、該当するユースケースのコードロジックがあるアクションと関連付けられます。

特定のsubject_token_type値を含めて/oauth/tokenエンドポイントに送信されたカスタムトークン交換要求は、対応するカスタムトークン交換プロファイルにマッピングされ、処理のために関連するアクションに送られます。

カスタムトークン交換プロファイルを作成するには、まずプロファイルにアクションを作成します。

アクションを作成する

Auth0 Dashboardで以下を行います。

  1. [Actions(アクション)]>[Library(ライブラリー)]に移動します。

2. [Create Action(アクションを作成)]>[Build from scratch(初めから構築する)]を選択します。

3. [Create Action(アクションを作成)]ダイアログに名前を入力し、ドロップダウンから[Custom Token Exchange(カスタムトークン交換)]トリガーを選択します。

4. [Create(作成)]を選択します。

5. アクションを[Deploy(デプロイ)]します。

アクションをデプロイすると、Auth0がアクションIDを割り当てます。アクションにはカスタムロジックを追加しなければなりませんが、その前にアクションIDを取得して、カスタムトークン交換プロファイルを作成します。

6. Auth0 DashboardでアクションIDを取得するには、ブラウザーウィンドウのURLを確認します。下の画像が示すように、アクションIDはURLの末尾の部分にあります。

アクションIDはManagement APIで取得することもできます。まず、APIを使用するためにManagement APIトークンを取得します。そして、以下のGET要求を/actionsエンドポイントに送信します。

curl --location 'https://{{YOUR _TENANT}}/api/v2/actions/actions?actionName={{ACTION_NAME}}' \
--header 'Authorization: Bearer {{MANAGEMENT_API_TOKEN}}' \

Was this helpful?

/

応答本文のactions[0].id内にアクションIDが含まれているはずです。アクションIDはカスタムトークン交換プロファイルの作成に必要です。

カスタムトークン交換プロファイルを作成する

カスタムトークン交換プロファイルを作成するには、Management APIを使用して、POST要求に以下のパラメーターを含めて/token-exchange-profilesエンドポイントに送信します。

curl --location 'https://{{YOUR _TENANT}}/api/v2/token-exchange-profiles' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer {{MANAGEMENT_API_TOKEN}}' \
--data '{
    "name": "{{PROFILE_NAME}}",
    "subject_token_type": "{{UNIQUE_PROFILE_TOKEN_TYPE_URI}}",
    "action_id": "{{ACTION_ID}}",
    "type": "custom_authentication"
}'

Was this helpful?

/

パラメーター 説明
subject_token_type 特有なプロファイルのトークンタイプのhttps://またはurnで始まるURIです。

以下の名前空間は予約されているため使用できません。

  • http://auth0.com
  • https://auth0.com
  • http://okta.com
  • https://okta.com
  • urn:ietf
  • urn:auth0
  • urn:okta
action_id カスタムトークンプロファイルに関連付けられているアクションのアクションIDです。
type custom_authenticationに設定します。

カスタムトークン交換プロファイルが正常に作成されると、以下の応答を受け取ります。

{
  "id":"tep_9xqewuejpa2RTltf",
  "name":"{{PROFILE_NAME}}",
  "type":"custom_authentication",
  "subject_token_type":"{{UNIQUE_PROFILE_TOKEN_TYPE_URI}}",
  "action_id":"{{ACTION_ID}}",
  "created_at":"2025-01-30T13:19:00.616Z",
  "updated_at":"2025-01-30T13:19:00.616Z"
}

Was this helpful?

/

これで、ユースケースに実装するカスタムトークン交換のコード作成とテストの準備が整いました。

カスタムトークン交換プロファイルを管理する

カスタムトークン交換プロファイルを管理するには、Management APIを使用して/token-exchange-profilesエンドポイントに要求を送信します。

カスタムトークン交換プロファイルを取得するには、以下の要求を行います。複数のプロファイルがある場合のために、このエンドポイントはチェックポイントページネーションに対応しています。

curl --location 'https://{{YOUR _TENANT}}/api/v2/token-exchange-profiles' \
--header 'Authorization: Bearer {{MANAGEMENT_API_TOKEN}}' \

Was this helpful?

/

カスタムトークン交換プロファイルのnameまたはsubject_token_typeを更新するには、以下のPATCH要求を行います。アクションIDは変更できませんが、実行されるカスタムコードはActionsエディターで変更できます。

curl --location --request PATCH 'https://{{YOUR _TENANT}}/api/v2/token-exchange-profiles/{{PROFILE_ID}}' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer {{MANAGEMENT_API_TOKEN}}' \
--data '{
    "name": "external-idp-migration",
    "subject_token_type": "urn:partner0:external-idp-migration"
}'

Was this helpful?

/

カスタムトークン交換プロファイルを削除するには、以下のDELETE要求を行います。

curl --location --request DELETE 'https://{{YOUR _TENANT}}/api/v2/token-exchange-profiles/{{PROFILE_ID}}' \
--header 'Authorization: Bearer {{MANAGEMENT_API_TOKEN}}' \
--data ''

Was this helpful?

/

Actions API

カスタムトークン交換とログイン後アクション

カスタムトークン交換アクションは早期アクセス版のカスタムトークン交換の一部として提供され、「Actions APIを使用する」に記載の新しいAPIメソッドが使用できます。

アクセストークンにカスタムクレームを追加することなどが必要な場合は、トランザクションに設定済みのユーザーに対してカスタムトークン交換アクションを実行したた後に、ログイン後アクショントリガーを実行すると、ログインフローと同じ機能性が実現できます。

トークン交換の付与タイプを使用するトランザクションを識別するには、ログイン後アクションoauth2-token-exchangeと同じ値を持つevent.transaction.protocolを探します。トークン交換の付与タイプはカスタムトークン交換とネイティブソーシャルログインの両方のトランザクションで使用され、subject_token_typeはいずれかのカスタムトークン交換プロファイルに対応しているため、subject_token_type値を使用すれば、それらの2つを区別できます。

Actions APIを使用する

トークン交換アクションで使用できるように、Auth0は数々のAPIメソッドを提供しています。subject_token_typeに基づいてサブジェクトトークンの復号化と検証を行うアクションを実装してください。そうすることで、トランザクションのためにユーザー情報を入手できます。また、この情報を使用して、コードがトランザクションに必要な認可ポリシーを適用するようにします。トランザクションが続行できることを確認したら、対応するユーザーを設定してトランザクションを確定できます。そうすると、Auth0がアクセストークン、IDトークンとリフレッシュトークンをユーザーに発行します。これはユーザーを認証する1つの方法だと考えることができます。

カスタムトークン交換トランザクションはそれぞれ1つのテナントイベントログを生成します。トランザクションが成功すると種類がsecteのイベントログが生成され、トランザクションが失敗すると種類がfecteのイベントログが生成されます。これらの種類は受け取るかもしれないエラーを理解するのに役立ちます。/oauth/tokenエンドポイントからのエラーには詳細があまり含まれません。

api.authentication.setUserById(user_id)

任意の接続タイプに対して、指定のユーザーIDを基にユーザー属性を設定します。これにより、プロファイルを更新することなく、既存のユーザーを指定できます。このメソッドはユーザーが存在しないか、ブロックされていると失敗します。

パラメーター 説明
user_id ユーザーIDです。例:auth0|55562040asf0aef

exports.onExecuteCustomTokenExchange = async (event, api) => {

  // 1. Validate subject_token
  const subject_token = await validateToken(event.transaction.subject_token, jwksUri);

  // 2.  Apply your authorization policy on the user
  const isAuthorized = await authorizeAccess(subject_token.sub);
  if (!isAuthorized) {
    api.access.deny('Unauthorized_login', 'User cannot login due to reason: X');
  }

  // 3. Set the user for the transaction
  api.authentication.setUserById(subject_token.sub);

  return;
};

Was this helpful?

/

api.authentication.setUserByConnection(connection_name, user_profile, options)

ユーザーとそのユーザーに関するプロファイル属性を指定の接続内で設定します。これは、ユーザーがこの接続にログインし、指定されたユーザープロファイルをフェデレーションIdPが返すことと同等です。ユーザーが存在しない場合にこの操作がユーザーを作成するか、そして、提供されたユーザープロファイル属性を使ってプロファイルを更新するかを構成できます。

ユーザーがsetUserByConnection()を通してログインするたびに、ログイン回数が加算されます。このメソッドはユーザーがブロックされていると失敗します。

パラメーター 説明
connection_name ユーザープロファイルが設定される接続の名前です。512文字が上限です。
user_profile 設定するユーザープロファイル属性を含むオブジェクトです。プロパティは24が上限です。
options 更新と作成の動作を指定するオブジェクトです。

{updateBehavior:'replace' | 'none',creationBehavior:'create_if_not_exists' | 'none',}

ユーザーが存在する場合は、updateBahaviourが以下を行います。
  • replace:提供された接続のユーザー属性およびuser_idを置換します(既存のユーザー属性の中で提供されないものは、ユーザーから削除されます。部分的な更新には対応していません。)
  • none:ユーザーが存在する場合は、プロファイルを更新しません。ユーザーが存在しない場合は、creationBehaviorの構成に応じて、提供されたプロファイル属性でプロファイルを作成します。
  • ユーザーが存在しない場合は、creationBehaviorが以下を行います。
    • create_if_not_exists:ユーザーを作成します
    • none:ユーザーを作成することなく、エラーを返します

exports.onExecuteCustomTokenExchange = async (event, api) => {

  // 1. Validate subject_token
  const subject_token = await validateToken(event.transaction.subject_token, jwksUri);

  // 2.  Apply your authorization policy on the user
  const isAuthorized = await authorizeAccess(subject_token.sub);
  if (!isAuthorized) {
    api.access.deny('Unauthorized_login', 'User cannot login due to reason: X');
  }

  // 3. Set the user for the transaction
  api.authentication.setUserByConnection(
    'My Connection',
    {
      user_id: subject_token.sub,
      email: subject_token.email,
      email_verified: subject_token.email_verified,
      phone_number: subject_token.phone_number,
      phone_verified: subject_token.phone_number_verified,
      username: subject_token.preferred_username,
      name: subject_token.name,
      given_name: subject_token.given_name,
      family_name: subject_token.family_name,
      nickname: subject_token.nickname,
      verify_email: false
    },
    {
      creationBehavior: 'create_if_not_exists',
      updateBehavior: 'none'
    }
  );

  return;
};

Was this helpful?

/

対応されているユーザープロファイル属性

setUserByConnection()メソッドでは、ユーザー更新エンドポイントが対応しているプロファイル属性を設定できます。

  • user_id (必須):この接続またはプロバイダーでユーザーに一意の識別子です。通常は、この接続について外部のIDプロバイダーが提供するユーザーIDです。このパラメーターはcreationBehaviourupdateBehaviourの両方がnoneに設定されている場合にのみ必須です。

  • email

  • email_verified。デフォルトはfalseです。

  • username

  • phone_number

  • phone_verified。デフォルトはfalseです。

  • name

  • given_name

  • family_name

  • nickname

  • picture

上のリストにはない属性の設定が必要な場合は、メタデータフィールドを使用してください。

対応されている接続ストラテジー

現在のバージョンは以下の接続ストラテジーに対応しています。これ以外のストラテジーでは、setUserByConnection()メソッドが失敗します。他のストラテジーへの対応をご希望の場合には、Auth0サポートまでお問い合わせください。

エンタープライズ接続:

ソーシャル接続:

  • カスタムソーシャル接続

  • Google

  • Apple

  • Facebook

  • Github

  • Windowslive

作成の動作

ユーザーはcreationBehaviorcreate_if_not_existsに設定されている場合にのみ動的に作成されます。

ユーザーの作成には以下が必要です。

  • 使用している接続に構成されている識別子を提供しなければなりません。メールはデフォルトで必須です。

  • 柔軟な識別子と属性を使用する接続については、該当する属性が接続に有効化されていれば、ユーザー名や電話番号を提供できます。

  • 柔軟な識別子と属性を使用しない接続については以下を行います。

    • 接続に[Require Username(ユーザー名を必須にする)]trueに設定されている場合は、ユーザー名を提供できます。詳細については、「データベース接続にユーザー名を追加する」をお読みください。

    • phone_numberは提供できません。

  • email_verifiedphone_verifiedを指定しても構いません。

Auth0のデータベース接続では、ユーザーのためにランダムなパスワードが動的に生成されます。ユーザーの作成後に必要に応じてパスワードのリセットフローをトリガーするには、別のオプションがあります。

更新の動作

ユーザープロファイルはupdateBehaviorreplaceに設定されている場合にのみ更新されます。

以下の属性は編集できません。値を変更しようとすると、Auth0からエラーが返されます。

  • email

  • username

  • phone_number

  • email_verified

  • phone_verified

メール検証

email_verified=falseでユーザーが作成されると、Auth0は自動的に確認メールを送信します。この動作をオーバーライドするには、verify_email=falseをユーザープロファイル属性として指定します。ユーザープロファイルの一部として保管はされません。

exports.onExecuteCustomTokenExchange = async (event, api) => {

  // Validate subject_token
  const subject_token = await validateToken(event.transaction.subject_token, jwksUri);

  // Create a user but don't verify email
  api.authentication.setUserByConnection(
    'My Connection',
    {
      user_id: subject_token.sub,
      email: subject_token.email,
      email_verified: false,
      verify_email: false
    },
    {
      creationBehavior: 'create_if_not_exists',
      updateBehavior: 'none'
    }
  );

  return;
};

Was this helpful?

/

歓迎メールテンプレートを構成して有効化すると、確認メールが送信されない場合に、Auth0は新規作成されたユーザーに対して自動的に歓迎メールを送信します。

メタデータを設定する

ユーザー更新エンドポイントとは異なり、setUserByConnection()メソッドではユーザーやアプリケーションのメタデータを設定できません。その場合はapi.user.setAppMetadataを使用できます。ユーザーメタデータの正しい使い方については、「ユーザープロファイルでのメタデータの仕組み」をお読みください。メタデータのベストプラクティスについては、「ログイン後トリガーでユーザーメタデータを管理する方法」をお読みください。

api.user.setAppMetadata(name, value)

ログインを試行するユーザーのアプリケーションメタデータを設定します。

このメソッドはマージ動作に従うため、既存の属性に影響を与えることなく、追加する属性や更新する属性を指定できます。属性を削除するには、値をnullに設定します。

パラメーター 説明
name 文字列。メタデータプロパティの名前です。
value 文字列、オブジェクト、配列。メタデータプロパティの値です。

exports.onExecuteCustomTokenExchange = async (event, api) => {
  // Validate subject_token
  const subject_token = await validateToken(event.transaction.subject_token, jwksUri);

  // set the user for the transaction
  api.authentication.setUserById(subject_token.id);

  // set user group based on info contaiened in subject_token
  api.user.setAppMetadata('group', subject_token.group);

  return;
};

Was this helpful?

/

api.user.setUserMetadata(name, value)

ログインを試行するユーザーの一般的なメタデータを設定します。

このメソッドはマージ動作に従うため、既存の属性に影響を与えることなく、追加する属性や更新する属性を指定できます。属性を削除するには、値をnullに設定します。

パラメーター 説明
name 文字列。メタデータプロパティの名前です。
value 文字列、オブジェクト、配列。メタデータプロパティの値です。

exports.onExecuteCustomTokenExchange = async (event, api) => {
  // Validate subject_token
  const subject_token = await validateToken(event.transaction.subject_token, jwksUri);

  // set the user for the transaction
  api.authentication.setUserById(subject_token.id);

  // set user preferred_locale based on info contaiened in subject_token
  api.user.setUserMetadata('preferred_locale', subject_token.locale);

  return;
};

Was this helpful?

/

api.access.deny(code, reason)

ログイントランザクションを拒否して、呼び出し元にエラーを返します。

パラメーター 説明
code 応答でプロパティに含めて返される文字列です。

以下の2つの標準エラーコードを使用できます。
  • invalid_request400番台のステータスコードを返します
  • server_error500番台のステータスコードを返します

独自のエラーコードを使用する場合は、400番台のステータスコードを返します。
reason 応答のerror_descriptionプロパティに含めて返される文字列です。

exports.onExecuteCustomTokenExchange = async (event, api) => {

  // 1. Validate subject_token
  const subject_token = await validateToken(event.transaction.subject_token, jwksUri);

  // 2.  Apply your authorization policy on the user
  const isAuthorized = await authorizeAccess(subject_token.sub);
  if (!isAuthorized) {
    api.access.deny('Unauthorized_login', 'User cannot login due to reason: X');
  }

  // if user is authorized, go on as indicated here

};

Was this helpful?

/

api.access.rejectInvalidSubjectToken(reason)

トランザクションを拒否して、要求元の外部IPアドレスについて試行の失敗数を加算します。カスタムトークン交換は要求をinvalid_requestのエラーコードを使用した400 Bad Requestエラー応答で拒否します。

試行の失敗が最大数に達すると、該当するIPアドレスからのすべてのカスタムトークン交換要求について、Auth0はトラフィックをtoo_many_attemptsのエラーコードを使用した429 Too Many Requestsエラー応答でブロックします。詳細については、「攻撃防御」をお読みください。

このメソッドは、署名や暗号化が不適切、または有効期限切れのサブジェクトトークンを含むカスタムトークン交換要求を受け取った場合には必ず使用してください。また、なりすましやリプレイ攻撃など、不正使用が疑われる状況でも必ず使用してください。そうすることで、Auth0は構成に応じて、不審なIPのスロットリングを適用できるようになります。

不審なIPのスロットリングはデフォルトで最大10回まで、1時間あたりに6回の試行を許容します。詳細については、「攻撃防御」をお読みください。

パラメーター 説明
reason 応答でerror_descriptionプロパティに含めて返された文字列です。

exports.onExecuteCustomTokenExchange = async (event, api) => {

  try {
    // Validate subject_token
    const subject_token = await validateToken(event.transaction.subject_token, jwksUri);
    // set the user for the transaction
    api.authentication.setUserById(subject_token.id);

  } catch (error) {
    if (error.message === 'Invalid Token') {
      // If specifically the problem is the subject_token is invalid
      console.error('Invalid Token error');
      api.access.rejectInvalidSubjectToken('Invalid subject_token');
    } else {
      // if there is any other unexpected error, throw a server error
      throw error;
    }
  }

};

Was this helpful?

/

api.cache

実行間で維持されるデータの保管と取得を行います。

これらのメソッドはサブジェクトトークンを検証するために、署名検証の公開鍵などのデータをキャッシュする場合に役立ちます。jwks-uriからキーを取得する際のパフォーマンスを向上させることができます。

api.cache.delete(key)

提供されたkeyにキャッシュ済みの値が存在する場合は、それを記述したレコードを削除します。

値がキャッシュから削除されると、CacheWriteResultオブジェクトにtype: "success"を含めて返します。操作に失敗すると、type: "error"を返します。エラーの場合には、返すオブジェクトにcodeプロパティを含めて、失敗の詳細を示します。

パラメーター 説明
key 文字列。キャッシュに保管されているレコードのキーです。

api.cache.get(key)

提供されたkeyにキャッシュ済みの値が存在する場合は、それを記述したレコードを取得します。レコードが見つかった場合には、返されたオブジェクトのvalueプロパティにキャッシュ済みの値があります。

提供されたkeyにキャッシュが見つかった場合には、キャッシュレコードを返します。キャッシュレコードはvalueプロパティを含むオブジェクトで、このプロパティにはキャッシュ済みの値の他にもexpires_atプロパティが含まれ、レコードの最大有効期間をUNIXエポックからの経過ミリ秒数で示します。

重要:このキャッシュは、短命で一時的なデータ向けに設計されています。項目が所定のライフタイム内であったとしても、後のトランザクションでは利用できないかもしれません。

パラメーター 説明
key 文字列。キャッシュに保管されているレコードのキーです。

api.cache.set(key, value, [options])

指定されたkeyのキャッシュに文字列値を保管または更新します。

このキャッシュに保管された値は、それを設定するトリガーにスコープが限定されます。これはアクションのキャッシュ制限の対象になります。

このように保管された値には、指定されたttlまたはexpires_at値までのライフタイムがあります。ライフタイムが指定されない場合には、デフォルトのライフタイムである15分が使用されます。ライフタイムはアクションのキャッシュ制限が定める最大値を超過してはいけません。

値が正常に保管されると、CacheWriteSuccessを返します。それ以外の場合はCacheWriteErrorを返します。

パラメーター 説明
key 文字列。キャッシュに保管されているレコードのキーです。
value 文字列。保管するレコードの値です。
options 任意のオブジェクト。キャッシュの動作を調整するオプションです。
options.expires_at 任意の数値。UNIXエポックからのミリ秒単位で指定した絶対有効期限です。キャッシュ済みのレコードは早期に削除されることはあっても、expires_atで指定された時点を過ぎて存続することはありません。

__注意:__この値がttlで指定されている場合は指定するべきではありません。両方で指定された場合、2つの中で早い方の有効期限が適用されます。
options.ttl 任意の数値。このキャッシュエントリの存続時間をミリ秒単位で指定します。キャッシュ済みの値は早期に削除されることはあっても、ttlで指定された時点を過ぎて存続することはありません。

__注意:__この値がexpires_atで指定されている場合は指定するべきではありません。両方で指定された場合、2つの中で早い方の有効期限が適用されます。

アクションイベント

新しいActions APIメソッドに加えて、アクションイベントのデータを使用して、サブジェクトトークン、IPアドレス、クライアントなど、トークン交換要求のコンテキストについて知ることができます。

プロパティ タイプ
client
client_id 文字列 HOVc2PDFTH7eahimN4yNCo8mOtjfNjLV
name 文字列 My Web App
metadata オブジェクト {“foo”: “bar” }
tenant
id 文字列 dev_1234
request
geoip オブジェクト { … geoip object}
hostname 文字列 dev_1234.us.auth0.com
ip 文字列 123.42.42.34
user_agent 文字列 Mozilla/5.0
language 文字列 en
body オブジェクト { // raw req.body }
method 文字列 POST
transaction
subject_token_type 文字列 urn://cic-migration-token
subject_token 文字列 41598922a1745f7af70
requested_scopes 文字列[] [“openid”, “email”]
resource_server
id 文字列 http://acme-api/v1/profile

アクションをデプロイする

上記のAPIとイベントオブジェクトでトークン交換アクションを作成したら、ページの上部にある[Deploy(デプロイ)]をクリックして変更内容をデプロイします。

トークン交換を呼び出す

カスタムトークン交換を使用するには、以下のパラメーターを指定してPOST要求を/oauth/tokenエンドポイントに対して行います。以下に留意してください。

  • カスタムトークン交換に使用するsubject_tokensは、アクションコードが解釈できるのであれば、どのような形式や種類のトークンでも構いません。

  • subject_token_typeはそれぞれ特定のカスタムトークン交換プロファイルにマッピングされ、そのトランザクションを制御する特定のアクションに関連付けられます。

パラメーター 説明
grant_type カスタムトークン交換には urn:ietf:params:oauth:grant-type:token-exchange を使用します。
subject_token_type サブジェクトトークンの種類です。カスタムトークン交換では、独自に所有するURIでスコープが限定された任意の値です。たとえば、http://acme.com/legacy-tokenurn:acme:legacy-token.

です。以下の名前空間は予約されているため使用できません。
  • http://auth0.com
  • https://auth0.com
  • http://okta.com
  • https://okta.com
  • urn:ietf
  • urn:auth0
  • urn:okta
subject_token アクションが検証とユーザーの識別に使用するべきサブジェクトトークンです。
client_id トークン交換に使用しているアプリケーションのクライアントIDです。他の付与タイプについても、HTTP Basic認証でクライアントIDをAuthorizationヘッダーに含めて渡すことができます。
client_secret トークン交換に使用しているアプリケーションのクライアントシークレットです。他の付与タイプについても、HTTP Basic認証でクライアントシークレットをAuthorizationヘッダーに含めて渡すことができます。

他にも方法はありますが、詳細についてはAuth0 Authentication APIのリファレンスドキュメントを参照してください。

公開アプリケーションがカスタムトークン交換を使用できることに注意してください。利用する場合は必ず「攻撃防御」をお読みください。
audience Auth0で定義されているAPI識別子です。
scope OAuth2スコープのパラメーターです。

その他の拡張パラメーターは、対応するアクションのevent.request.bodyに含まれていますが、無視されます。

要求例

curl --location 'https://{{YOUR_TENANT}}/oauth/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \
--data-urlencode 'audience=https://api.acme.com' \
--data-urlencode 'scope=openid offline_access acme-scope1 acme-scope2' \
--data-urlencode 'subject_token_type=urn:acme:external-idp-migration' \
--data-urlencode 'subject_token=t8e7S2D9trQm73e .... iqBR3GjxDtbDVjpfQU' \
--data-urlencode 'client_id={{CLIENT_ID}}' \
--data-urlencode 'client_secret={{CLIENT_SECRET}}'

Was this helpful?

/

攻撃防御

なりすましやリプレイ攻撃で悪意のある攻撃者がサブジェクトトークンを推測または再利用しようとするのを防ぐために、カスタムトークン交換は不審なIPのスロットリングに対応しています。これにより、サブジェクトトークンが無効な場合に、アクション内のコードから信号を特異的に送信できるため、Auth0は外部IPからの試行の失敗をカウントできます。

1つのIPアドレスからの試行の失敗数が事前構成のしきい値に達すると、該当するIPアドレスからのカスタムトークン交換要求について、Auth0は以下のエラー応答でトラフィックをブロックします。

HTTP/1.1 429 Too Many Requests
Content-Type: application/json
{
    "error": "too_many_attempts",
    "error_description": "We have detected suspicious login behavior and further attempts will be blocked. Please contact the administrator."
}

Was this helpful?

/

構成済みの期間が過ぎると、IPアドレスは要求を再び行えるようになります。

すべての事例で推奨されることですが、不審なIPのスロットリングを正しく構成してアクティブ化することは、カスタムトークン交換をネイティブアプリケーションやシングルページアプリケーションで使用する場合には特に重要です。ネイティブアプリケーションやSPAなどの非機密アプリケーションは、自身を認証するためにシークレットを安全に保管できません。攻撃者は簡単にサブジェクトトークンを推測したり、窃盗や漏洩したものを使用したりできます。

不審なIPのスロットリングを正しく使用するためには、受け取ったサブジェクトトークンが厳密に検証されない場合は必ず、アクションのコードにapi.access.rejectInvalidSubjectTokenを使用してください。

不審なIPのスロットリングはAuth0テナントにはデフォルトでアクティブ化されます。アクティブ化や構成の方法については、「不審なIPのスロットリング」をお読みください。アクティブ化すると、カスタムトークン交換のデフォルト設定が適用されます。

  • しきい値:10。1つのIPアドレスが失敗した試行の最大数です。

  • スロットリングレート:1時間あたり6回。しきい値を超えない範囲で10分ごとに1回の試行が補充されます。

カスタムトークン交換には、Management APIを使用してカスタムのしきい値やスロットリングレートを構成できます。

まず、APIを使用するためにManagement APIトークンを取得します。そして、以下のGET要求を不審なIPのスロットリング設定取得エンドポイントに対して行います。

curl --location 'https://{{YOUR _TENANT}}/api/v2/attack-protection/suspicious-ip-throttling' \
--header 'Authorization: Bearer {{MANAGEMENT_API_TOKEN}}' \

Was this helpful?

/

以下のような応答を受け取ります。

{
  "enabled": true,
  "shields": [
    "admin_notification",
    "block"
  ],
  "allowlist": [],
  "stage": {
    "pre-login": {
      "max_attempts": 100,
      "rate": 864000
    },
    "pre-user-registration": {
      "max_attempts": 50,
      "rate": 1200
    },
    "pre-custom-token-exchange": {
      "max_attempts": 10,
      "rate": 600000
    }
  }
}

Was this helpful?

/

以下のPATCH要求ではpre-custom-token-exchangeステージを必要な値で更新できます。レートは新たに試行が許可されるまでをミリ秒単位の間隔で表していることに注意してください。

curl --location --request PATCH 'https://{{YOUR _TENANT}}/api/v2//attack-protection/suspicious-ip-throttling' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer {{MANAGEMENT_API_TOKEN}}' \
--data '{"stage":{"pre-custom-token-exchange":{"max_attempts":10,"rate":600000}}}'

Was this helpful?

/

ユースケースの例とサンプルコード

技術的な制約やユーザーエクスペリエンスの観点から、エンドユーザーのリダイレクトに基づく標準的なフェデレーションログインストラテジーが適用できない高度な統合シナリオには、カスタムトークン交換をソリューションとして使用できます。ユースケースに提供するコードは不完全なものであり、対処するコードを論理的に進められるように説明することのみを意図しています。より詳しいサンプルコードについては、「サンプルコード」を参照してください。

このセクションでは、ユースケースの例と該当するシナリオの実装に推奨されるサンプルコードを紹介します。ユースケースを説明するために、GearUpという架空のレンタカー会社を想定します。

ユースケース:Auth0へシームレスに移行する

GearUpには何百万人もが使用するモバイルアプリがあり、アイデンティティソリューションの近代化が必要なため、Auth0を使用することに決めました。ただし、レガシーIDプロバイダー(IdP)からの移行に関してユーザーエクスペリエンスでの摩擦を懸念して、ユーザーに再認証を強制することは避けたいと考えています。

これを解消し、リスクを制限するために、GearUpは段階的に移行しています。それぞれのユーザーについて、レガシーIdPからのリフレッシュトークンをAuth0のアクセストークン、リフレッシュトークン、IDトークンのセットに交換することを希望しています。そうすることで、アプリが速やかにAuth0をIdPとして使い始め、Auth0発行のトークンを用いてGearUp APIを使用できるようになります。すべてのユーザーに交換が完了したら、エンドユーザーとGearUpのビジネスに影響することなく、アプリは完全に移行され、古いIdPが切断されます。

前提条件として、GearUpはAuth0テナントにユーザーの一括インポートを行い、モバイルアプリには移行するユーザーの有効なレガシーリフレッシュトークンがあります。

  1. モバイルアプリがAuth0に対してレガシーのリフレッシュトークンの交換を要求します。その際にはレガシーのリフレッシュトークンをサブジェクトトークンとして設定します。

  2. 該当するカスタムトークン交換プロファイルのアクションが実行されます。リフレッシュトークンをレガシーIdPで照会して、ユーザープロファイルから外部のユーザーIDを取得します。そして、必要な認可ポリシーを適用し、最終にユーザーを設定します。

  3. Auth0がAuth0のアクセストークン、IDトークン、リフレッシュトークンを含めて応答します。

  4. これで、ユーザーが再認証することなく、モバイルアプリがAuth0のトークンを使って顧客のAPIを使用できるようになりました。

以下のサンプルコードは、カスタムトークン交換アクションでこれを実装する方法を示しています。このユースケースではユーザープロファイルがすでにAuth0データベース接続にインポートされているため、以下が当てはまります。

  • Auth0はユーザーを作成しません。

  • Auth0はユーザープロファイルを更新しません。

Auth0は外部IdPのユーザーIDを使用して、該当する接続でユーザーを設定します。

/**
* Handler to be executed while executing a custom token exchange request
* @param {Event} event - Details about the incoming token exchange request.
* @param {CustomTokenExchangeAPI} api - Methods and utilities to define token exchange process.
*/
exports.onExecuteCustomTokenExchange = async (event, api) => {

 // 1. VALIDATE the refresh_token received in the subject_token by using it to get
 // the UserProfile from the external IdP
 const { isValid, user } = await getUserProfile(
   event.transaction.subject_token,
   event.secrets.CLIENT_SECRET,
 );

 if (!isValid) {
   // Mark the subject token as invalid and fail the transaction.
   api.access.rejectInvalidSubjectToken("Invalid subject_token");
 } else {
   // 2. Apply your AUTHORIZATION POLICY as required to determine if the request is valid.
   // Use api.access.deny() to reject the transaction in those cases.

   // 3. When we have the profile, we SET THE USER in the target connection
   api.authentication.setUserByConnection(
     connectionName,
     {
       // only the user_id in the connection is needed, as we are not
       // creating nor updating the user
       user_id: user.sub,
     },
     {
       creationBehavior: "none",
       updateBehavior: "none",
     },
   );
 }
};

/**
* Exchange the refresh token and load the user profile from the legacy IdP
* @param {string} refreshToken
* @param {string} clientSecret
* @returns {Promise<{ isValid: boolean, user?: object }>} If the refresh token was exchanged successfully, returns the user profile
*/
async function getUserProfile(refreshToken, clientSecret) {
 // Add your code here. REFER TO CODE SAMPLES FOR DETAILED EXAMPLES
}

Was this helpful?

/

不透明なリフレッシュトークンをレガシーIdPで検証する方法の詳しい例については、「サンプルコード」をお読みください。

ユースケース:外部の認証プロバイダーを再利用する

別のユースケースでは、GearUpがAir0という大手旅行会社と提携し、Air0のシングルページアプリケーション内でレンタカーサービスを直接提供します。GearUpは自社APIの使用をカプセル化したJavaScriptライブラリーを提供します。そうすることで、レンタカーサービスを提供するAir0のWebサイトがGearUpのAPIを手軽に使用できるようになります。

今回も、GearUpへの再認証を避けて、ソリューションがエンドユーザーから見えないようにする必要があります。これを実現するために、GearUpのJavaScriptライブラリーは外部のAir0 IDトークンを入力として使用し、トークン交換を処理できます。その結果、Auth0のアクセストークンが生成され、メールアドレスを基に所定のGearUpユーザーと関連付けられます。GearUpライブラリーがアクセストークンを取得したら、Air0のWebサイトで直接レンタカーサービスを提供するために、GearUpのAPIを使い始めることができます。

前提条件として、GearUpはAir0 IdPをフェデレーションのエンタープライズ接続またはソーシャル接続としてセットアップし、ユーザー認証をフェデレーションログインで行うか、以下のようにカスタムトークン交換で行えるようにします。

  1. ユーザーを認証したら、シングルページアプリが外部IdPからIDトークンを取得します。

  2. シングルページアプリがIDトークンをサブジェクトトークンとして設定し、トークンの交換を要求します。

  3. 該当するカスタムトークン交換プロファイルのアクションが実行されます。IDトークンを検証し、トークンからユーザーIDと他のプロファイル属性を取得します。そして、必要な認可ポリシーを適用し、最終にユーザーを設定します。

  4. Auth0がAuth0のアクセストークン、IDトークン、リフレッシュトークンを含めて応答します。

  5. これで、ユーザーが再認証することなく、SPAで実行中のJavascriptコードがAuth0のトークンを使って顧客のAPIを使用できるようになりました。

以下のサンプルコードは、カスタムトークン交換アクションでこれを実装する方法を示しています。このユースケースでは以下が当てはまります。

  • Auth0は外部IdPのユーザーIDを使用して、該当する接続でユーザーを設定します。

  • ユーザーが存在しない場合にはAuth0が作成します。

  • ユーザーがすでに存在する場合のために、より完全な属性のセットがフェデレーションログインで取得できるのであれば、Auth0はユーザープロファイルを置換しません。

  • Auth0はユーザーの作成時にメールを検証しません。

const jwksUri = "https://example.com/.well-known/jwks.json";

/**
 * Handler to be executed while executing a custom token exchange request
 * @param {Event} event - Details about the incoming token exchange request.
 * @param {CustomTokenExchangeAPI} api - Methods and utilities to define token exchange process.
 */
exports.onExecuteCustomTokenExchange = async (event, api) => {

  // 1. VALIDATE the id_token received in the subject_token
  const { isValid, payload } = await validateToken(
    event.transaction.subject_token,
  );

  if (!isValid) {
    // Mark the subject token as invalid and fail the transaction.
    api.access.rejectInvalidSubjectToken("Invalid subject_token");
  } else {
    // 2. Apply your AUTHORIZATION POLICY as required to determine if the request is valid.
    // Use api.access.deny() to reject the transaction in those cases.

    // 3. SET THE USER in the target connection.
    // We don't want to verify emails when users are created
    // This example assumes subject_token (id_token) contains standard OIDC claims. Other custom mappings
    // are also possible.
    api.authentication.setUserByConnection(
      'Enterprise-OIDC',
      {
          user_id: formattedUserId,
          email: subject_token.email,
          email_verified: subject_token.email_verified,
          phone_number: subject_token.phone_number,
          phone_verified: subject_token.phone_number_verified,
          username: subject_token.preferred_username,
          name: subject_token.name,
          given_name: subject_token.given_name,
          family_name: subject_token.family_name,
          nickname: subject_token.nickname,
          verify_email: false
      },
      {
          creationBehavior: 'create_if_not_exists',
          updateBehavior: 'none'
      }
    );
  }

  /**
   * Validate the subject token
   * @param {string} subjectToken
   * @returns {Promise<{ isValid: boolean, payload?: object }>} Payload of the token
   */
  async function validateToken(subjectToken) {
    // Add your code here. REFER TO CODE SAMPLES FOR DETAILED EXAMPLES
  }
};

Was this helpful?

/

JWTを安全に検証する方法の詳しい例については、「サンプルコード」をお読みください。

ユースケース:別のオーディエンスのAuth0トークンを取得する

GearUpはAPI要求を処理するために、内部のマイクロサービス間で呼び出しの認可方法を改善したいと考えています。それぞれのサービスに使用可能なリソースを制御するために、ポリシーの一元管理を希望しています。これもトークン交換を使用して実現できます。

まず、API要求がサービスAに到達すると、受け取ったアクセストークンを、新しいオーディエンスとしてサービスBの使用を許可する新しいアクセストークンに交換します。トークン交換を管理する認可ポリシーが許す場合、サービスAが新しいトークンを取得して、サービスBが使用できるようになります。ユーザーIDは新しいトークンで変わらずに維持されるため、ユーザーの正しいコンテキストがプロセス全体で維持されます。

GearUpアプリケーションは当初、API Aをユーザーに代わって使用するために、アクセストークンを取得します。

  1. アプリが要求に当初のアクセストークンを含めてAPI Aに送信します。

  2. API Aのバックエンドサービスがアクセストークンを検証し、それをサブジェクトトークンとして設定して、API Bを使用するための新しいアクセストークンとの交換を要求します。

  3. 該当するカスタムトークン交換プロファイルのアクションが実行されます。IDトークンを検証し、トークンからAuth0ユーザーIDを取得します。そして、必要な認可ポリシーを適用し、最終にユーザーを設定します。

  4. Auth0がAPI Bオーディエンスを使用するためのAuth0のアクセストークンを含めて応答します。

  5. API Aのバックエンドサービスが新しいアクセストークンを使用してAPI Bを呼び出します。このアクセストークンは引き続き同じユーザーに関連付けられています。

以下のサンプルコードは、カスタムトークン交換アクションでこれを実装する方法を示しています。このユースケースでは以下が当てはまります。

  • Auth0がAuth0のユーザーIDを使用してユーザーを設定するため、接続のスコープにこれを設定する必要はありません。

  • Auth0はユーザーの作成や更新を行いません。

このユースケースに関する詳しいサンプルコードについては、「非対称鍵で署名されたJWTを検証する」を参照してください。

const jwksUri = "https://example.com/.well-known/jwks.json";

/**
 * Handler to be executed while executing a custom token exchange request
 * @param {Event} event - Details about the incoming token exchange request.
 * @param {CustomTokenExchangeAPI} api - Methods and utilities to define token exchange process.
 */
exports.onExecuteCustomTokenExchange = async (event, api) => {
  // 1. VALIDATE the access_token received in the subject_token
  const { isValid, payload } = await validateToken(
    event.transaction.subject_token,
  );

  if (!isValid) {
    // Mark the subject token as invalid and fail the transaction.
    api.access.rejectInvalidSubjectToken("Invalid subject_token");
  } else {
    // 2. Apply your AUTHORIZATION POLICY as required to determine if the request is valid.
    // Use api.access.deny() to reject the transaction in those cases.

    // 3. SET THE USER
    api.authentication.setUserById(payload.sub);
  }

  /**
   * Validate the subject token
   * @param {string} subjectToken
   * @returns {Promise<{ isValid: boolean, payload?: object }>} Payload of the token
   */
  async function validateToken(subjectToken) {
    // Add your code here. REFER TO CODE SAMPLES FOR DETAILED EXAMPLES
  }
};

Was this helpful?

/

JWTを安全に検証する方法の詳しい例については、「サンプルコード」をお読みください。

サンプルコード

以下のサンプルコードは、受け取ったサブジェクトトークンを安全で効率よく検証するために、シナリオ共通のベストプラクティスを示しています。

Auth0とシークレットを共有する必要がない場合は、できる限り非対称のアルゴリズムと鍵を使用します。これは、適用できるk公開鍵を公表するためにJWKS URIエンドポイントを公開する場合などで、鍵のローテーションも簡素化します。

非対称鍵で署名されたJWTを検証する

以下の推奨事項を検討してください。

  • Actionsのapi.cacheメソッドを使用し、トランザクションごとに署名鍵を取得しないようにします。

  • RFC8725のベストプラクティスに従います。

  • RS*、PS*、ES*、またはEd25519のアルゴリズムを使用します。

  • noneアルゴリズムを使用したり、受け入れたりしてはいけません。

  • 最小長2048ビットのRSAを使用します。

const { jwtVerify } = require("jose");

const jwksUri = "https://example.com/.well-known/jwks.json";
const fetchTimeout = 5000; // 5 seconds

const validIssuer = "urn:my-issuer"; // Replace with your issuer

/**
 * Handler to be executed while executing a custom token exchange request
 * @param {Event} event - Details about the incoming token exchange request.
 * @param {CustomTokenExchangeAPI} api - Methods and utilities to define token exchange process.
 */
exports.onExecuteCustomTokenExchange = async (event, api) => {
  const { isValid, payload } = await validateToken(
    event.transaction.subject_token,
  );

  // Apply your authorization policy as required to determine if the request is valid.
  // Use api.access.deny() to reject the transaction in those cases.

  if (!isValid) {
    // Mark the subject token as invalid and fail the transaction.
    api.access.rejectInvalidSubjectToken("Invalid subject_token");
  } else {
    // Set the user in the current request as authenticated, using the user ID from the subject token.
    api.authentication.setUserById(payload.sub);
  }

  /**
   * Validate the subject token
   * @param {string} subjectToken
   * @returns {Promise<{ isValid: boolean, payload?: object }>} Payload of the token
   */
  async function validateToken(subjectToken) {
    try {
      const { payload, protectedHeader } = await jwtVerify(
        subjectToken,
        async (header) => await getPublicKey(header.kid),
        {
          issuer: validIssuer,
        },
      );

      // Perform additional validation on the token payload as required

      return { isValid: true, payload };
    } catch (/** @type {any} */ error) {
      if (error.message === "Error fetching JWKS") {
        throw new Error("Internal error - retry later");
      } else {
        console.log("Token validation failed:", error.message);
        return { isValid: false };
      }
    }
  }

  /**
   * Get the public key to use for key verification. Load from the actions cache if available, otherwise
   * fetch the key from the JWKS endpoint and store in the cache.
   * @param {string} kid - kid (Key ID) of the key to be used for verification
   * @returns {Promise<Object>}
   */
  async function getPublicKey(kid) {
    const cachedKey = api.cache.get(kid);
    if (!cachedKey) {
      console.log(`Key ${kid} not found in cache`);
      const key = await fetchKeyFromJWKS(kid);
      api.cache.set(kid, JSON.stringify(key), { ttl: 600000 });
      return key;
    } else {
      return JSON.parse(cachedKey.value);
    }
  }

  /**
   * Fetch public signing key from the provided JWKS endpoint, to use for token verification
   * @param {string} kid - kid (Key ID) of the key to be used for verification
   * @returns {Promise<object>}
   */
  async function fetchKeyFromJWKS(kid) {
    const controller = new AbortController();
    setTimeout(() => controller.abort(), fetchTimeout);

    /** @type {any} */
    const response = await fetch(jwksUri);

    if (!response.ok) {
      console.log(`Error fetching JWKS. Response status: ${response.status}`);
      throw new Error("Error fetching JWKS");
    }
    const jwks = await response.json();
    const key = jwks.keys.find((key) => key.kid === kid);
    if (!key) {
      throw new Error("Key not found in JWKS");
    }
    return key;
  }
};

Was this helpful?

/

対称鍵で署名されたJWTを検証する

以下の推奨事項を検討してください。

  • Actionsシークレットを使用して、対称シークレットを安全に保管します。

  • RFC8725のベストプラクティスに従います。

  • HS256などのセキュリティ保護されたアルゴリズムと、高エントロピーでランダムなシークレット(256ビッドの最小長など)を使用します。

const { jwtVerify } = require("jose");

const validIssuer = "urn:my-issuer"; // Replace with your issuer

/**
 * Handler to be executed while executing a custom token exchange request
 * @param {Event} event - Details about the incoming token exchange request.
 * @param {CustomTokenExchangeAPI} api - Methods and utilities to define token exchange process.
 */
exports.onExecuteCustomTokenExchange = async (event, api) => {
  // Initialize the shared symmetric key from Actions Secrets
  const encoder = new TextEncoder();
  const symmetricKey = encoder.encode(event.secrets.SHARED_SECRET);

  const { isValid, payload } = await validateToken(
    event.transaction.subject_token,
    symmetricKey,
  );

  // Apply your authorization policy as required to determine if the request is valid.
  // Use api.access.deny() to reject the transaction in those cases.

  if (!isValid) {
    // Mark the subject token as invalid and fail the transaction.
    api.access.rejectInvalidSubjectToken("Invalid subject_token");
  } else {
    // Set the user in the current request as authenticated, using the user ID from the subject token.
    api.authentication.setUserById(payload.sub);
  }
};

/**
 * Validate the subject token
 * @param {string} subjectToken
 * @param {Uint8Array} symmetricKey
 * @returns {Promise<{ isValid: boolean, payload?: object }>} Payload of the token
 */
async function validateToken(subjectToken, symmetricKey) {
  try {
    // Validate token is correctly signed with the shared symmetric key
    // It also checks it is not expired as long as it includes an 'exp' attribute.
    const { payload, protectedHeader } = await jwtVerify(
      subjectToken,
      symmetricKey,
      {
        issuer: validIssuer,
      },
    );

    return { isValid: true, payload };
  } catch (/** @type {any} */ error) {
    console.log("Token validation failed:", error.message);
    return { isValid: false };
  }
}

Was this helpful?

/

外部サービスで不透明なトークンを検証する

Actionsシークレットを使用して、外部IdPのクライアントシークレットを安全に保管します。

const tokenEndpoint = "EXTERNAL_TOKEN_ ENDPOINT";
const userInfoEndpoint = "EXTERNAL_USER_INFO_ENDPOINT";
const clientId = "EXTERNAL_CLIENT_ID";
const connectionName = "YOUR_CONNECTION_NAME";
const fetchTimeout = 5000; // 5 seconds

/**
 * Handler to be executed while executing a custom token exchange request
 * @param {Event} event - Details about the incoming token exchange request.
 * @param {CustomTokenExchangeAPI} api - Methods and utilities to define token exchange process.
 */
exports.onExecuteCustomTokenExchange = async (event, api) => {
  const { isValid, user } = await getUserProfile(
    event.transaction.subject_token,
    event.secrets.CLIENT_SECRET,
  );

  if (!isValid) {
    // Mark the subject token as invalid and fail the transaction.
    api.access.rejectInvalidSubjectToken("Invalid subject_token");
    return;
  }

  // Apply your authorization policy as required to determine if the request is valid.
  // Use api.access.deny() to reject the transaction in those cases.

  // When we have the profile, we set the user in the target connection
  api.authentication.setUserByConnection(
    connectionName,
    {
      // only the user_id in the connection is needed, as we are not
      // creating nor updating the user
      user_id: user.sub,
    },
    {
      creationBehavior: "none",
      updateBehavior: "none",
    },
  );
};

/**
 * Exchange the refresh token and load the user profile from the legacy IdP
 * @param {string} refreshToken
 * @param {string} clientSecret
 * @returns {Promise<{ isValid: boolean, user?: object }>} If the refresh token was exchanged successfully, returns the user profile
 */
async function getUserProfile(refreshToken, clientSecret) {
  const { isValid, accessToken } = await refreshAccessToken(
    refreshToken,
    clientSecret,
  );
  if (!isValid) {
    return { isValid: false };
  }

  const controller = new AbortController();
  setTimeout(() => controller.abort(), fetchTimeout);

  /** @type {any} */
  const response = await fetch(userInfoEndpoint, {
    method: "GET",
    headers: {
      Authorization: `Bearer ${accessToken}`,
      "Content-Type": "application/json",
    },
  });

  if (!response.ok) {
    console.log(`Failed to fetch user info. Status: ${response.status}`);
    throw new Error("Error fetching user info");
  }

  const userProfile = await response.json();

  return { isValid: true, user: userProfile };
}

/**
 * Use the Refresh Token with the legacy IdP to validate it and get an access token
 * @param {string} refreshToken
 * @param {string} clientSecret
 * @returns {Promise<{ isValid: boolean, accessToken?: string }>} If the refresh token was exchanged successfully, returns the access token
 */
async function refreshAccessToken(refreshToken, clientSecret) {
  const controller = new AbortController();
  setTimeout(() => controller.abort(), fetchTimeout);

  /** @type {any} */
  let response;

  try {
    response = await fetch(tokenEndpoint, {
      method: "POST",
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
      },
      body: new URLSearchParams({
        grant_type: "refresh_token",
        refresh_token: refreshToken,
        client_id: clientId,
        client_secret: clientSecret,
      }).toString(),
    });
  } catch (error) {
    console.error("Error refreshing token");
    throw error;
  }

  if (!response.ok) {
    const errorBody = await response.json();
    console.error("Error refreshing token:", errorBody.error);

    // If we receive an error indicating the refresh token is invalid (for example, an invalid_grant error),
    // then we should explicitly indicate an invalid token using api.access.rejectInvalidSubjectToken
    // to prevent against brute force attacks on the refresh token by activating Suspicious IP Throttling.
    // For other errors which indicate a generic error making the request to the IdP, we should throw
    // an error to indicate a transient failure.
    if (errorBody.error === "invalid_grant") {
      return { isValid: false };
    } else {
      throw new Error("Error refreshing token");
    }
  }

  // Parse the response, in the form { access_token: "...", expires_in: ..., }
  const data = await response.json();
  console.log("Successfully exchanged refresh token");
  return { isValid: true, accessToken: data.access_token };
}

Was this helpful?

/

制限事項

これは早期アクセス機能であるため、いくつかの制限があり、他のAuth0機能との互換性はありません。

早期アクセス版のカスタムトークン交換は以下の機能には対応していません(または正しく動作しません)。

  • Organizations

  • MFA:ログイン後アクションのapi.authentication.challengeWith()およびapi.multifactor.enable()コマンドはカスタムトークン交換に未対応であるため、トランザクションが回復不可能なエラーで失敗します。同様に、テナントのポリシーとしてMFAが構成されている場合にも、トランザクションが失敗します。

  • カスタムデータベース接続

  • 特有のなりすまし対応(アクタートークンやアクタークレームなど)

  • サードパーティやOIDC非準拠のクライアント

レート制限

/oauth/tokenエンドポイントに対するカスタムトークン交換要求には、該当するパフォーマンスレベルにおいて、Authentication APIのグローバルレート制限の10%というレート制限が適用されます。

パフォーマンスレベル グローバルのAuthentication API制限(RPS) カスタムトークン交換制限(RPS)
Enterprise 100 10
プライベートクラウドベーシック(1x) 100 10
プライベートクラウドパフォーマンス(5x) 500 50
プライベートクラウドパフォーマンス(15x) 1500 150
プライベートクラウドパフォーマンス(30x) 3000 300
プライベートクラウドパフォーマンス(60x) 6000 600
プライベートクラウドパフォーマンス(100x) 10000 1000

api/v2/token-exchange-profilesエンドポイントでの読み出し要求にも以下のレート制限が適用されます。

パフォーマンスレベル カスタムトークン交換の制限(RPS) カスタムトークン交換の制限(RPM)
エンタープライズ 20 200
プライベートクラウドベーシック(1x) 20 200
プライベートクラウドパフォーマンス(5x) 100 300
プライベートクラウドパフォーマンス(15x) 300 3000
プライベートクラウドパフォーマンス(30x) 600 6000
プライベートクラウドパフォーマンス(60x) 1200 12000
プライベートクラウドパフォーマンス(100x) 2000 20000

エンティティ制限

テナントごとに最大100のカスタムトークン交換プロファイルを作成できます。

アクションの総数もAuth0プランに応じて制限されます。詳細については、「Auth0の価格設定」ページを参照してください。

トラブルシューティング

「同意が必要」応答

/oauth/tokenエンドポイントを呼び出したときに、consent_requiredのエラー説明を含むinvalid_requestエラーを受け取ることがあります。

この問題を解消するには、Auth0 Dashboardを使用してAPIに[Allow Skipping User Consent(ユーザー同意のスキップを許可する)]オプションを有効にします。