ID tokenを使ってAmazon CognitoユーザーをAmazon QuickSightユーザーに紐付ける方法

Cognito ユーザーの ID token のクレーム(属性)を使用して、QuickSight ユーザーと連携する方法をご紹介します。
2023.03.17

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

お疲れさまです。とーちです。

Amazon Cognito(以下 Cognito) のユーザーで認証を行い、Amazon QuickSight(以下 QuickSight) を利用したい場合、QuickSight 上のリソースを操作するためには、QuickSight ユーザーの作成が必須です。

そのため、Cognito 上のユーザーと QuickSight ユーザーを適切に紐付ける必要があるのですが、どうやって紐付ければいいのかお悩みの方もいらっしゃるのではないでしょうか?

いくつかのアプローチが考えられますが、今回は Cognito ユーザーの ID token のクレーム(属性)を使用して、QuickSight ユーザーと連携する方法をご紹介します。

なおこの手法は、以下のワークショップで紹介されていたものになります。ワークショップで作成される AWS Lambda (以下 Lambda)等の解説もしつつご紹介できればと思います。

Amazon QuickSight - Embedding (Japanese)

システム概要

ワークショップで作成されるシステム構成は上記の通りです。

Amazon API Gateway(以下 API Gateway) + Lambda が静的 html 配信も兼ねているので、実際に本番環境システム等で使う場合は静的 html 配信の部分を Amazon CloudFront + Amazon S3 にしてもいいかもしれません。 その場合の構成図は下記のような形になります。

処理の流れ

処理の流れを順に説明します。

  1. クライアントは最初に API Gateway にアクセスします。Lambda 内に静的 html(+JavaScript) が格納されておりこれをレスポンスとして受け取ります。
  2. JavaScript 内処理により Cognito にサインイン済かどうかをチェックしサインインしていない場合は Cognito の hosted UI にリダイレクトされます。正常にサインインが出来た場合 ID token が返されます。
  3. ID token をつけて再度 API Gateway にアクセスします。Lambda が ID token を受け取ります。
  4. Lambda が ID token を使って sts:AssumeRole を試みます。なお、ここで取得した一時クレデンシャルは使いません。
    • ここで Assume するロールは Cognito ユーザープールを ID プロバイダーとして信頼しているものになっており、AssumeRole が成功することをもって ID token が対象の Cognito から発行されたことを確認しています。AssumeRole で確認するのが面白いですね。
    • なお、ここの処理は一般的な ID token 検証手段である JWKS を使った検証に置き換えることも可能です。
  5. sts:AssumeRole を行う際に IAM サービスが Cognito ユーザープールに ID token の検証を行います。
  6. ID token の検証が完了したら、Lambda が QuickSight の API を使って埋め込み URL の発行やダッシュボード一覧の取得を行います。
    • この際に QuickSight ユーザーの Arn で指定することにより 対象の QuickSight ユーザーが権限を持つ QuickSight リソースのみを取得するよう制限しています
    • この Lambda では QuickSight ユーザーが存在しなかったら作成する(自動プロビジョニング)という処理も実行しています。
  7. クライアントは取得した埋め込み URL を使って QuickSight のリソースにアクセスします。

以後、ポイントなる処理をピックアップして解説します。

ID token の検証

ID token の一般的な検証方法

Cognito ユーザープールで認証されるとブラウザは ID token を受け取ります。これを API Gateway+Lambda に送るのですが、Lambda から見るとこの ID token が信頼できるものなのかはわかりません。
そのため、必ず ID token を検証する必要があります。
ID token の検証方法は発行元(この場合 Cognito ユーザープール)から提供される JWKS(JSON Web Key Set)を用いてトークンの署名を検証し、有効期限やクレーム(属性)を確認するのが一般的ですが、この Lambda では STS を用いることでそれを間接的に実現しています。

IAM ロールのウェブ ID フェデレーション

IAM ロールを作成する際に「信頼されたエンティティタイプ」として「ウェブアイデンティティ」というタイプを選択することができます。これは、外部 ID プロバイダーで認証されていることを IAM ロールの引き受け条件とするタイプのもので、このタイプの IAM ロールに AssumeRole するには ID token をつけて AssumeRole を実行する必要があります。

IAM はここで指定された ID token を発行元(Cognito ユーザープール)に対して検証(おそらく JWKS を使った検証)することで、
正当な ID token であることが確認された場合に AssumeRole を許可します。そのため sts:AssumeRole を行うことが ID token の検証となるわけです。

実際のコード

具体的には Lambda の以下の部分で実現されています。

GetQuickSightResponse.py

  if authEvalMode == 'STS':
      stage = 'In STS block'
      sts = boto3.client('sts')
      assumedRole = sts.assume_role_with_web_identity(
          RoleArn = roleArn,
          RoleSessionName = userName,
          WebIdentityToken = openIdToken
      )
      return True

上記にも記載しましたがここの部分は一般的な JWKS 検証に置き換えることも可能です。IAM ロールのウェブアイデンティティがサポートする外部 ID プロバイダーは限られているので、サポートされていない外部 ID プロバイダーを使う場合は JWKS 検証 にする必要があるでしょう。

2023/3/18 追記

ウェブアイデンティティタイプのIAMロールで指定できるIDプロバイダーは限られていましたが、IAM OIDC ID プロバイダーであれば特に外部IDプロバイダーの制限はないので、どの外部IDプロバイダーでもsts:AssumeRoleで検証可能でした。

Cognito と QuickSight ユーザーの紐付け

Cognito の ID token のクレームについて

Cognito と QuickSight ユーザーの紐付けには ID token のクレームを使用します。
ID token はヘッダ、ペイロード、署名からなる json データをエンコードしたものとなっており、ペイロード部分には Cognito で認証されたユーザのクレーム(属性)が入っています。 具体的には以下のようなデータになっています。

{
   "sub":"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
   "cognito:groups":[
      "my-test-group"
   ],
   "email_verified":true,
   "cognito:preferred_role":"arn:aws:iam::111122223333:role/my-test-role",
   "iss":"https://cognito-idp.us-west-2.amazonaws.com/us-west-2_example",
   "cognito:username":"my-test-user",
   "middle_name":"Jane",
   "nonce":"abcdefg",
   "origin_jti":"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
   "cognito:roles":[
      "arn:aws:iam::111122223333:role/my-test-role"
   ],
   "aud":"xxxxxxxxxxxxexample",
   "event_id":"64f513be-32db-42b0-b78e-b02127b4f463",
   "token_use":"id",
   "auth_time":1676312777,
   "exp":1676316377,
   "iat":1676312777,
   "jti":"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
   "email":"my-test-user@example.com"
}

QuickSight ユーザ Arn の作成

上記のクレームのうちcognito:username の部分を Lambda 内で取り出し以下のような文字列を作成します。

'arn:aws:quicksight:' + <リージョン名> + ':' + <AWSアカウントID> + ':user/default/' + <AssumeRoleに使用するロール名> + '/' + <cognito:usernameの値>

上記の文字列は、QuickSight ユーザー Arn の命名規則に従っています。

ご参考:Amazon QuickSight におけるシングルサインオンの設計と実装 P.78

AssumeRole に使用するロールは、ID token の検証のためにのみ使用され、1 つの Cognito ユーザープールに対して 1 つだけ用意する必要があります。そのため、IAM ロールの Arn は常に固定値を使用し、Lambda の環境変数として設定します。

さらに、リージョン名や AWS アカウント ID も 1 つのユーザープール内で固定されるため、Cognito ユーザープールと QuickSight アカウントが 1 対 1 の関係であれば、cognito:username の値を用いて Cognito ユーザ名と QuickSight ユーザ名を 1 対 1 で対応付けることができます。

QuickSight のユーザーは、事前に作成しておくことも、Lambda 内でユーザーが存在しない場合に自動プロビジョニングを行うことも可能です。事前に作成する場合は、Cognito ユーザ名と一致させるよう注意してください。個人的には、自動プロビジョニングの方が人為的ミス(例:ユーザ名の間違い)を防止できるため、良いのではないかと考えています。

QuickSight 埋め込み URL の発行

QuickSight の埋め込み URL を発行するには、generate_embed_url_for_registered_user などの API を使用します。API を実行すると、埋め込み用の URL がレスポンスとして返されます。
ここで注目すべきポイントは、UserArn の部分です。
UserArn には、IDtoken に基づく QuickSight ユーザー名を指定することで、対象の QuickSight ユーザーが権限を持つリソースのみにアクセス制限がかかります。これにより、セキュリティが確保され、ユーザーは自分に適切な権限でリソースを利用できるようになります。

GetQuickSightResponse.py

  if urlType == 'dashboard':
      #Generate embed url for dashboard
      #We are using the new generate_embed_url_for_registered_user API to generate the dashboard embed url.
      #Older variant - get_dashboard_embed_url - will continue to be available. However, newer features will be getting added only to the new API.
      response = quickSight.generate_embed_url_for_registered_user(
                      AwsAccountId = awsAccountId,
                      UserArn = 'arn:aws:quicksight:'+ identityRegion + ':' + awsAccountId + ':user/default/' + roleName + '/' + userName,
                      SessionLifetimeInMinutes = adjustedSessionDuration,
                      ExperienceConfiguration = {'Dashboard':{'InitialDashboardId': 'non-existent-id'}}
                  )
      repackedResponse['DashboardEmbedUrl'] = response['EmbedUrl']

  if urlType == 'session':
      #Generate embed url for session
      #We are using the new generate_embed_url_for_registered_user API to generate the session embed url as well.
      #Older variant - get_session_embed_url - will continue to be available. However, as mentioned above, newer features will be getting added only to the new API.
      response = quickSight.generate_embed_url_for_registered_user(
                      AwsAccountId = awsAccountId,
                      UserArn = 'arn:aws:quicksight:'+ identityRegion + ':' + awsAccountId + ':user/default/' + roleName + '/' + userName,
                      SessionLifetimeInMinutes = adjustedSessionDuration,
                      ExperienceConfiguration = {'QuickSightConsole':{'InitialPath': '/start/favorites'}}
                  )
      repackedResponse['SessionEmbedUrl'] = response['EmbedUrl']

試してみる

QuickSight ワークショップ内の CloudFormation コードにて上記のシステム構成が構築可能です。

前提として QuickSight エンタープライズ アカウントが必要になりますが、それさえあれば上記の CloudFormation を起動するだけです。

作成されると以下のように QuickSight 埋め込みされたサイトが確認できます。

まとめ

ID token のクレーム(属性)を使用して、QuickSight ユーザーと連携する方法のご紹介でした。 Cognito で認証して QuickSight にログインする方法はいくつかあると思いますが、個人的には上記でご紹介した形が一番シンプルで良いと思っています。

この記事が誰かのお役に立てばなによりです。
以上、とーちでした。

参考

Amazon QuickSight におけるシングルサインオンの設計と実装
QuickSight ワークショップ