Auth0で発行したアクセストークンで直接AWS STSを叩いて一時的な認証情報を取得できない

TL;DR

  • AWS STS (Security Token Service) で AssumeRoleWithWebIdentity するとき、Auth0が発行したアクセストークンに含まれるaudクレームをSTSが正しく解釈することができずにエラーになる
  • 解決法としてはIDトークンを用いるか、Lambda Authorizerを利用する

今回問題となるケース

ここではサンプルとして、ユーザー認証をAuth0で行い、AWSのリソースにアクセスして結果を返すAPIを作成します。

フロントエンドの構築

まずはAuth0のQuickstartsを読んでAuth0のアプリ設定とフロントエンドの構築をします。

auth0.com

このあと構築するAPIをフロントエンドから呼び出す場合は以下のドキュメントを参照してください。

auth0.com

Auth0 APIの設定

Auth0で認証したユーザーに対してAPIを公開したいので、Auth0のダッシュボードからAPIを作成します。

Auth0でのAPI設定

Identifierは好きなものを設定できますが、APIのエンドポイントを指定することが推奨されています。 ここでは https://example.com/ にします。 次のステップで使うので控えておきます。

AWSの設定

IDプロバイダの追加

このあとのステップでAuth0で発行したアクセストークンをSTSに渡すことになるため、AWSにAuth0をIDプロバイダとして使うことを登録します。

docs.aws.amazon.com

IAMのページからIDプロバイダを選び、以下のように登録します。

IAM IDプロバイダの設定

  • プロバイダのタイプ: OpenID Connnect
  • プロバイダURL: Auth0のApplicationからDomainの設定をコピーします。
  • 対象者: Auth0のAPI Audienceをコピーします。
一時認証キーで引き受けるロールを作成する

IDプロバイダの画面から「ロールの割り当て」を選び、新しいロールを作成します。 利用したいAWSリソースに対するポリシーを割り当てておきます。

docs.aws.amazon.com

信頼されたエンティティは下のような感じになります。

{
    "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により一時認証キーを発行し、そのキーを用いてアクセスする方法があります。

docs.aws.amazon.com

リクエスト時にアクセストークンをもらって、それをSTSAWSの一時認証キーに引き換え、それを用いて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を使って自分でやれば回避できます。

Secure AWS API Gateway Endpoints Using Custom Authorizers