Auth0で発行したアクセストークンで直接AWS STSを叩いて一時的な認証情報を取得できない
TL;DR
- AWS STS (Security Token Service) で AssumeRoleWithWebIdentity するとき、Auth0が発行したアクセストークンに含まれるaudクレームをSTSが正しく解釈することができずにエラーになる
- 解決法としてはIDトークンを用いるか、Lambda Authorizerを利用する
今回問題となるケース
ここではサンプルとして、ユーザー認証をAuth0で行い、AWSのリソースにアクセスして結果を返すAPIを作成します。
フロントエンドの構築
まずはAuth0のQuickstartsを読んでAuth0のアプリ設定とフロントエンドの構築をします。
このあと構築するAPIをフロントエンドから呼び出す場合は以下のドキュメントを参照してください。
Auth0 APIの設定
Auth0で認証したユーザーに対してAPIを公開したいので、Auth0のダッシュボードからAPIを作成します。
Identifierは好きなものを設定できますが、APIのエンドポイントを指定することが推奨されています。
ここでは https://example.com/
にします。
次のステップで使うので控えておきます。
AWSの設定
IDプロバイダの追加
このあとのステップでAuth0で発行したアクセストークンをSTSに渡すことになるため、AWSにAuth0をIDプロバイダとして使うことを登録します。
IAMのページからIDプロバイダを選び、以下のように登録します。
- プロバイダのタイプ: OpenID Connnect
- プロバイダURL: Auth0のApplicationからDomainの設定をコピーします。
- 対象者: Auth0のAPI Audienceをコピーします。
一時認証キーで引き受けるロールを作成する
IDプロバイダの画面から「ロールの割り当て」を選び、新しいロールを作成します。 利用したいAWSリソースに対するポリシーを割り当てておきます。
信頼されたエンティティは下のような感じになります。
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::XXXXXXXXXXXX:oidc-provider/YOUR_AUTH0_DOMAIN.auth0.com" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "YOUR_AUTH0_DOMAIN.auth0.com:aud": "https://example.com/" } } } ] }
API構築
APIはLambdaで構築すれば、ポリシーをアタッチすることでAWSのリソースへアクセスすることができますが、 AWS以外の自分で建てたサーバや、GCPのCloud Functionsで構築するケースの場合はこれができません。
このような場合にAWSリソースへアクセスする方法としてAWS STSにより一時認証キーを発行し、そのキーを用いてアクセスする方法があります。
リクエスト時にアクセストークンをもらって、それをSTSでAWSの一時認証キーに引き換え、それを用いてAWSリソースにアクセスするサンプルは以下のようになります。(コードはめんどくさかったのでLambdaで書いています)
const jose = require("jose"); const { STSClient, AssumeRoleWithWebIdentityCommand, } = require("@aws-sdk/client-sts"); exports.handler = async (event, context) => { const accessToken = event.headers.authorization.split(" ")[1]; console.log("[Access Token] ", accessToken); const JWKS = jose.createRemoteJWKSet( new URL("https://YOUR_AUTH0_DOMAIN.auth0.com/.well-known/jwks.json") ); // アクセストークンの検証 const { payload, protectedHeader } = await jose.jwtVerify(accessToken, JWKS, { issuer: "https://YOUR_AUTH0_DOMAIN.auth0.com/", audience: "https://example.com/", }); console.log("[Protected Header] ", protectedHeader); console.log("[Payload] ", payload); // STSで一時認証キーを取得する const client = new STSClient({ region: "us-east-1" }); const command = new AssumeRoleWithWebIdentityCommand({ RoleArn: "arn:aws:iam::XXXXXXXXXXXX:role/Auth0SampleRole", RoleSessionName: "Auth0AssumeRoleSession", WebIdentityToken: accessToken, }); try { const awsCredentials = await client.send(command); console.log("[STS Credentials] ", awsCredentials); // DO Something with AWS Resource } catch (error) { // error handling. console.log("[STS Error] ", error); } return {}; };
エラーが発生する
このAPIを実行すると以下のような InvalidIdentityTokenException が発生します。
InvalidIdentityTokenException: Incorrect token audience { '$fault': 'client', '$metadata': { httpStatusCode: 400, requestId: 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX', extendedRequestId: undefined, cfId: undefined, attempts: 1, totalRetryDelay: 0 }, Error: { Type: 'Sender', Code: 'InvalidIdentityToken', Message: 'Incorrect token audience', message: 'Incorrect token audience' }, RequestId: 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX', xmlns: 'https://sts.amazonaws.com/doc/2011-06-15/' }
Incorrect token audience とメッセージが出ており、audienceが違うと言われています。
なぜこのエラーが発生するのか?
Auth0で発行したアクセストークンを JSON Web Tokens - jwt.io で見てみると、audクレームが以下のようになっています。
"aud": [ "https://example.com/", "https://YOUR_AUTH0_DOMAIN.auth0.com/userinfo" ],
audクレームはアクセストークンが誰に対して発行されたものであるかが記されています。 STSでAssumeRoleWithWebIdentityをするとき、STSはIAM IDプロバイダの対象者に登録した値と、アクセストークンの中のaudトークンを比較して値が一致するかどうかを検証しています。
通常 Incorrect token audience
のメッセージが出た場合は2つの値が一致していることを確認します。
しかし今回のケースではaudトークンの中にIDプロバイダーの対象者に設定した値が含まれているため、トークンが検証できるはずですがなぜか失敗してしまいます。
実はSTSはaudトークンが配列で指定されている場合には必ず検証に失敗します。
audトークンはRFC 7519で文字列か、文字列の配列が指定できることになっています*1が、STSでは文字列のものしか受け付けてくれません。 試しにIDトークンを投げつけてみると検証に成功することがわかると思います。
AWSサポートに問い合わせたところ、STSではaudクレームが配列のものを受け付けられない仕様とのことでした。 この動作はCognito IDプールでも同様です。
回避策
今回紹介したケースは、あまり遭遇しないかも知れないですが、回避策も紹介しておきます。 基本的にはIDトークンが利用できるような構成を取るのが良いようです。
IDトークンを用いる
フロントエンドと同じドメインでAPIが公開される場合はIDトークンを利用するのが良いでしょう。 Auth0のIDトークンはaudクレームがClient IDだけになるのでSTSでの検証をパスできます。
Lambda Authorizerを使う
アクセストークンの検証をLambda Authorizerを使って自分でやれば回避できます。