ウェブIDフェデレーションするときCognito IDプールではIDプロバイダURLに「/」を入れてしまうとIssuer検証時にURL末尾の「/」がダブってしまい必ず検証に失敗してしまう

TL;DR

  • Cognito IDプールではIDプロバイダURLに「/」を入れてしまうとIssuer検証時にURL末尾の「/」がダブってしまい必ず検証に失敗してしまう。
  • OpenID Connect (OIDC) IDプロバイダを利用して一時認証キーを発行するとき、同一IDプロバイダを使う場合であってもCognito IDプールとSTSを直接使う方法で別々のIDプロバイダを追加しなければならない。

OIDCプロバイダで認証したユーザーに対してIAM一時認証キーを発行する

Auth0などのOpenID Connect ID プロバイダーで認証されたユーザーに対してAWSリソースへのアクセスキーを発行する方法として、Congito IDプールを使う方法と、STS (Security Token Service)を使う方法の2つがあります。

Cognito IDプールを利用する場合でも内部ではSTSを呼んでいるので大まかな処理は変わりませんが、IDトークンの検証にはなぜか違いがあります。

Cognito IDプールの場合

ここでは認証にAuth0を使い、OpenID Connectで連携する例で説明します。 Auth0はIAMのIDプロバイダのページから以下のように登録します。

IAM IDプロバイダの設定例

  • プロバイダのタイプ: OpenID Connnect
  • プロバイダURL: Auth0のApplicationからDomainの設定をコピーします。
  • 対象者: Auth0のClient IDをコピーします。

プロバイダURLの末尾に「/」を入れないようにしてください。

次にCognito IDプールを作成するか、使いたいものを選び、「認証プロバイダー」→「OpenID」から上で設定したIDプロバイダを選んで有効化します。

実際にCognito IDプールから一時認証キーを取得するには ID プール (フェデレーティッドアイデンティティ) の認証フロー に従って取得します。

JavaScriptで書くと以下のようになります。

const client = new CognitoIdentityClient({ region: "us-east-1" });
const getIdCommand = new GetIdCommand({
  IdentityPoolId: "us-east-1:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
  Logins: {
    "AUTH0_DOMAIN.auth0.com": idToken,
  },
});
const cognitoId = await client.send(getIdCommand);
const getCredentialsForIdentityCommand = new GetCredentialsForIdentityCommand(
  {
    IdentityId: cognitoId.IdentityId,
    Logins: {
      "AUTH0_DOMAIN.auth0.com": idToken,
    },
  }
);
const awsCredentials = await client.send(getCredentialsForIdentityCommand);

awsCredentialsAWSにアクセス可能な一時認証キーが入ってきます。 IDプールの認証されたロールに紐づくポリシーで許可されたAWSリソースにアクセスすることができます。

STSから直接一時認証キーを取得する

STSから直接一時認証キーを取得するには AssumeRoleWithWebIdentity を使います。

Cognito IDプールを利用しない場合はAssumeRoleするロールが無いので、ウェブ ID または OpenID Connect フェデレーション用のロールの作成 (コンソール) - AWS Identity and Access Management の通りに作成しておきます。

JavaScriptで書いた場合は以下のようなコードになります。

const client = new STSClient({ region: "us-east-1" });
const command = new AssumeRoleWithWebIdentityCommand({
  RoleArn: "arn:aws:iam::XXXXXXXXXXXX:role/Auth0SampleRole",
  RoleSessionName: "Auth0AssumeRoleSession",
  WebIdentityToken: idToken,
});

IDプロバイダの設定も必要ですが、Cognito IDプールを使うところで設定したはずなので大丈夫なはずです。

さてこれを実行してみましょう。するとうまく...いかずに以下のエラーが発生してしまいます。

{
    "errorType": "InvalidIdentityTokenException",
    "errorMessage": "No OpenIDConnect provider found in your account for https://AUTH0_DOMAIN.auth0.com/",
    "name": "InvalidIdentityTokenException",
    "$fault": "client",
    "$metadata": {
        "httpStatusCode": 400,
        "requestId": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
        "attempts": 1,
        "totalRetryDelay": 0
    },
    "Error": {
        "Type": "Sender",
        "Code": "InvalidIdentityToken",
        "Message": "No OpenIDConnect provider found in your account for https://AUTH0_DOMAIN.auth0.com/",
        "message": "No OpenIDConnect provider found in your account for https://AUTH0_DOMAIN.auth0.com/"
    },
    ...snip...  
}

https://AUTH0_DOMAIN.auth0.com/」という名前のOpenIDConnect providerが見つからないと言われているようです。IDプロバイダは設定したはずで、Cognito IDプールでは動いていたはずなのでおかしいですね。

答え合わせ

実はこれCognito IDプールとは別のIDプロバイダを追加する必要があります。 以下のようにIDプロバイダを追加してみてください。プロバイダのURLに「/」をつけるのを忘れないでください。

STS向けIAM IDプロバイダの設定

IDプロバイダを変更したのでロールも編集します。「/」がついているところが変わっていますのでよくみて変更してください。うまくいかない場合はロールを再作成した方がよいでしょう。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "arn:aws:iam:XXXXXXXXXXXX:oidc-provider/dev-AUTH0_DOMAIN.auth0.com/"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringEquals": {
                    "AUTH0_DOMAIN.auth0.com/:aud": "AUTH0_CLIENT_ID"
                }
            }
        }
    ]
}

するとなんと検証に成功します。

逆にCognito IDプールを使う場合、プロバイダURLに「/」を付けてしまった場合はどうなるでしょうか。

{
    "errorType": "NotAuthorizedException",
    "errorMessage": "Token is not from a supported provider of this identity pool.",
    "name": "NotAuthorizedException",
    "$fault": "client",
    "$metadata": {
        "httpStatusCode": 400,
        "requestId": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
        "attempts": 1,
        "totalRetryDelay": 0
    },
    "__type": "NotAuthorizedException",
    ...snip...
}

"Token is not from a supported provider of this identity pool." はIDプールに紐付いた認証プロバイダ名と、GetIdをしたときのLoginsオプションが違うと言われているようです。

コードを以下のように変更して/を付けて実行してみます。

const getIdCommand = new GetIdCommand({
  IdentityPoolId: "us-east-1:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
  Logins: {
-    "AUTH0_DOMAIN.auth0.com": idToken,
+    "AUTH0_DOMAIN.auth0.com/": idToken,
  },
});

const getCredentialsForIclient. Sendand = new GetCredentialsForIdentityCommand(
  {
    IdentityId: cognitoId.IdentityId,
    Logins: {
-    "AUTH0_DOMAIN.auth0.com": idToken,
+    "AUTH0_DOMAIN.auth0.com/": idToken,
    },
  }
);

すると以下のようなエラーになります。

{
    "errorType": "NotAuthorizedException",
    "errorMessage": "Invalid login token. Issuer doesn't match providerName",
    "name": "NotAuthorizedException",
    "$fault": "client",
    "$metadata": {
        "httpStatusCode": 400,
        "requestId": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
        "attempts": 1,
        "totalRetryDelay": 0
    },
    "__type": "NotAuthorizedException",
    ...snip...
}

今度は認証プロバイダを見つけられたようですが、/を付けてしまったことによりIDトークン中のIssuerと一致しなくなってしまったようです。 ちなみにAuth0が発行するIDトークンのIssuerは以下のようになっており、「/」まで含まれています。

{
  "iss": "https://AUTH0_DOMAIN.auth0.com/",
}

どうやらCognito IDプールでは https://${IDプロバイダのプロバイダURL}/ とIDトークンのissクレームを比較しているようで、IDプロバイダ名に「/」を付けてしまうと末尾の「/」がダブって検証に失敗してしまうようです。これはかなりバグっぽい挙動なので直してほしいですね。issクレームのURLに/が付かないIDプロバイダの場合はまた挙動が変わるかもしれません。

上記のエラーが出た場合はここまでの情報でデバッグできると思うのでがんばって直してみてください。