Amazon CognitoでEntra IDなどの外部IdPユーザーにグループを自動的に紐づける

こんにちは、ひるたんぬです。

最近電車に乗る機会が増えており、電車内でスマホを凝視するとすぐに酔ってしまう私は、電車の中吊り広告を眺めることが多いです。
そんな中で、ふと「〇〇が!?」というような表現が目に留まりました。そういえば「!」「?」の順番って大体「!?(感嘆符→疑問符)」のような…
と同時に、英語の書物では「?!(疑問符→感嘆符)」と、逆であることが多いような気もしてきました。

絵文字では「⁉️(感嘆符→疑問符)」のみしか存在しないようですが、Unicode上では「⁉(U+2049)」「⁈(U+2048)」のどちらも存在します。
調べる中で「インテロハング | ‽(U+203D)」という順番も気にしなくて良い記号があることも知りました。

順番のルールは調べてみましたが、明確なルールは存在せず、「デザイン・バランス上の都合で日本は!?が多い」という意見が多かったように見受けられました。
この点について、理由をご存じの方はぜひご教示いただきたいです。

さて、今回は前回の記事にも関連するところがありますが、Microsoft Entra IDなどの外部IdPをAmazon Cognitoに連携した際に、そのユーザーを自動的にグループに所属させる方法をご紹介します。前回の記事も興味がありましたらご覧ください。

なお、外部IdP経由でアクセスしたユーザーについて、Cognito上では自動的に一意のグループに所属されます。

Amazon Cognito は、ユーザープールに追加する OIDC、SAMl、ソーシャル ID プロバイダー (IdP) ごとにユーザーグループを作成します。グループ名の形式は [user pool ID]_[IdP name] です (例: us-east-1_EXAMPLE_MYSSO、us-east-1_EXAMPLE_Google)。自動生成された一意の各 IdP ユーザープロファイルは、このグループに自動的に追加されます。
引用:AWS 「ユーザープールにグループを追加する」

今回の記事では、それ以外の(自分で作成した)グループに自動的に所属させたい場合の内容となっております。

事前準備

既にユーザをCognito以外の別IDプロバイダーで管理していることを想定しています。
今回はEntra IDと連携しています。OIDC・SAMLどちらでも動作確認はしておりますので、要件に合わせて設定してください。

設定が完了したら、以下のようになっていることを確認します。

▼ SAMLで設定した場合

▼ OIDCで設定した場合

手順

今回の肝となる機能はCognitoのLambdaトリガーです。個人的には「Cognito用のEventBridge」という感覚です。
Cognitoのサインアップ時やトークン作成時などの各種処理イベントに対してトリガーを設定でき、それに対応するLambda関数を実行できる機能です。

一部トリガーを除き、Lambda関数の実行時間は5秒以内に制限されています。
こちらは緩和不可の制限となるのでお気をつけください。
今回はこのトリガーの中でも「インバウンドフェデレーション Lambda トリガー」を使用します。
Amazon Cognito ユーザープールでユーザーを作成または更新する前に、フェデレーティッドユーザー属性を変換します
引用:AWS 「Lambda トリガーを使用したユーザープールワークフローのカスタマイズ」
今回の要件にぴったりですね。
後述しますが、実は「ぴったり」とは言い切れない事情があります。。

Lambda関数の準備

今回は、「外部IdPユーザに対して、すべてのグループに所属させる」というLambda関数を準備しました。
特定のグループにのみ所属させたい場合などは、適宜コードを修正してください。

上記にもあるように、今回は「すべてのグループに所属させる」ことを目的としているため、「インバウンドフェデレーション Lambda トリガー」を使用します。このトリガーは初回アクセス(サインアップ)時だけでなく、二回目以降のサインインでも発動するため、グループが増加した場合などにも対応可能となっています。
一方、決まったグループにのみ所属させるのみで良い場合は、本トリガーは最適解でないと思われますので、設定・コードなどは適宜変更してください。
import os
import logging
import boto3
from botocore.exceptions import ClientError

logging.basicConfig(level=os.getenv("LOG_LEVEL", "INFO"))
logger = logging.getLogger(__name__)
cognito = boto3.client("cognito-idp")

PROVIDER_NAME = os.getenv("PROVIDER_NAME").lower()
def list_all_groups(user_pool_id: str) -> list[str]:
    groups: list[str] = []
    paginator = cognito.get_paginator("list_groups")
    for page in paginator.paginate(UserPoolId=user_pool_id):
        groups.extend(
            g["GroupName"]
            for g in page.get("Groups", [])
            if "GroupName" in g
        )
    logger.info("list_all_groups: total=%d", len(groups))
    return groups

def lambda_handler(event, context):
    user_pool_id = event["userPoolId"]
    trigger_source = event.get("triggerSource")
    event_username = event.get("userName", "")
    logger.info(
        "handler start request_id=%s trigger=%s userPoolId=%s userName=%s",
        getattr(context, "aws_request_id", None),
        trigger_source,
        user_pool_id,
        event_username,
    )
    # InboundFederation のみ対象(別トリガーの場合は適宜変更・削除してください)
    if trigger_source != "InboundFederation_ExternalProvider":
        return event
    # Cognito 実 username に正規化
    prefix = f"{PROVIDER_NAME}_"
    username = (
        event_username
        if event_username.lower().startswith(prefix)
        else prefix + event_username
    )
    group_names = list_all_groups(user_pool_id)
    success = failed = 0
    # 全グループへの追加処理を実行
    for group in group_names:
        try:
            cognito.admin_add_user_to_group(
                UserPoolId=user_pool_id,
                Username=username,
                GroupName=group,
            )
            success += 1
        except ClientError as e:
            failed += 1
            logger.error(
                "add group failed username=%s group=%s error=%s",
                username,
                group,
                e,
                exc_info=True,
            )
    logger.info(
        "handler done username=%s success=%d failed=%d total=%d",
        username,
        success,
        failed,
        len(group_names),
    )
    return event

また、このコードでは環境変数「PROVIDER_NAME」を使用しています。
Lambda内の環境変数に追加してください。値にはCognitoに外部IdPを連携させたときの「プロバイダー名」を設定します。

Lambdaトリガーの設定

先ほど作成したLambda関数に対して、CognitoのLambdaトリガーを設定します。

Cognitoのユーザープール内にある「認証 – 拡張機能」をクリックします。
するとLambdaトリガーの画面が出てくるので「Lambda トリガーを追加」をクリックします。

トリガータイプを「フェデレーション – インバウンドフェデレーション トリガー」になるように選択し、先ほど作成したLambda関数を選択します。
設定が完了したら、「Lambda トリガーを追加」をクリックします。

Lambdaトリガーが追加されていることを確認します。

動作確認

今回は3つのグループを用意し、その全てに外部IdPユーザが所属されるかを確認します。

「ap-northeast-1_Sq…」のグループは、外部IdPユーザを管理するためにAWS側で自動的に作成されたグループです。
外部IdP経由でサインインを行うことで、Cognito上にユーザーが作成されます。
ユーザーの中を確認し、グループの所属状況を確認すると…

あれ、所属されてないじゃないですか!
…そうなんです。これが上述した「トリガーがぴったりではない」という理由です。
こちらの回避方法ですが、もう一度外部IdPでのサインインを行うと解消します。
再度サインインを実行し、グループの所属状況を確認すると…

問題なくすべてのグループに所属できていますね。

原因究明

このようになってしまう要因を探るために、Lambdaのログを確認してみます。
すると、一回目のアクセス時に動いているLambda内で、以下のようなエラーが出ていることが確認できました。

[ERROR] 2026-06-15T01:35:40.598Z 473f080d-e68d-47fb-9944-XXXXXXXXXXXXXX add group failed username=entraid-saml_sample.taro@exsample.onmicrosoft.com group=Group-B error=An error occurred (UserNotFoundException) when calling the AdminAddUserToGroup operation: User does not exist.
Traceback (most recent call last):
File "/var/task/cognito-idp-group-attach.py", line 64, in lambda_handler
cognito.admin_add_user_to_group(
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
UserPoolId=user_pool_id,
^^^^^^^^^^^^^^^^^^^^^^^^
Username=username,
^^^^^^^^^^^^^^^^^^
GroupName=group,
^^^^^^^^^^^^^^^^
)
^
File "/var/lang/lib/python3.13/site-packages/botocore/client.py", line 602, in _api_call
return self._make_api_call(operation_name, kwargs)
~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^
File "/var/lang/lib/python3.13/site-packages/botocore/context.py", line 123, in wrapper
return func(*args, **kwargs)
File "/var/lang/lib/python3.13/site-packages/botocore/client.py", line 1078, in _make_api_call
raise error_class(parsed_response, operation_name)
botocore.errorfactory.UserNotFoundException: An error occurred (UserNotFoundException) when calling the AdminAddUserToGroup operation: User does not exist.

「User does not exist.」と言われていますね。
ここで、改めて今回設定したトリガーのドキュメントを確認してみます。

インバウンドフェデレーショントリガーは、外部 ID プロバイダーによる認証プロセス中にフェデレーションユーザー属性を変換します。
…(中略)…
このトリガーを使用して、新しいユーザーを作成する前、または既存のフェデレーティッドユーザープロファイルを更新する前に、属性を追加、上書き、または抑制します。
引用:AWS 「インバウンドフェデレーション Lambda トリガー」

答えが書かれていましたね。このトリガーはCognito上にユーザーを作成する前に動作をするものなので、初回アクセス時はユーザーが作成されていない、つまりCognito上にユーザーが存在しないということになります。

おわりに

今回はCognitoで外部IdPを連携させる際に、外部IdPユーザーをグループに自動的に所属させるための方法をご紹介しました。
初回アクセス時の課題は残っていますが、この記事がどなたかの参考になりましたら幸いです。

著者について

2024 Japan AWS Jr. Champions
ANGEL Dojo 2024 | ベストアーキテクチャ賞 2位

ひとこと:
同期に「おじさん」とよく言われます。なぜなのでしょうか…

ひるたんぬ | 蛭田 悠介をフォローする

クラウドに強いによるエンジニアブログです。

SCSKクラウドサービス(AWS)は、企業価値の向上につながるAWS 導入を全面支援するオールインワンサービスです。AWS最上位パートナーとして、多種多様な業界のシステム構築実績を持つSCSKが、お客様のDX推進を強力にサポートします。


クラウドに強いによるエンジニアブログです。

SCSKクラウドサービス(Azure)は、Azureを最大限活用するためのオールインワンサービスです。
Microsoftパートナーとして30年以上にわたり培ってきた知見と、様々なシステム構築・運用実績に基づく業界理解、Azure構築ナレッジを強みに、クラウドへの移行から運用・活用までをトータルでサポートします。

AWSAzureクラウド
シェアする
×
タイトルとURLをコピーしました