Amazon Cognito user poolの外部IdPとしてAzure ADを設定してみた(AWS CDK)

2022.03.31

こんにちは、CX事業本部 IoT事業部の若槻です。

前回のエントリで、Amazon Cognito user poolの外部IdPとしてMicrosoft Azure ADを設定した構成を作ってみました。

今回は、同様の構成をAWS CDKを使用して作ってみました。

やってみた

AWS側の設定をAWS CDKで行い、Azure側の設定は前回と同様に手動で行います。(構築手順の都合上CDK Deployを2回に分けて行います。)

AWSへCognito User PoolおよびDomainの作成

AWS CDKで次のようにCognito User PoolおよびDomainを作成します。

bin/aws-cdk-v2-app.ts

#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { AwsCdkV2AppStack } from '../lib/aws-cdk-v2-app-stack';

const app = new cdk.App();
new AwsCdkV2AppStack(app, 'AwsCdkV2AppStack', {
  callbackUrls: ['https://classmethod.jp/'],
  logoutUrls: ['https://classmethod.jp/'],
  domainPrefix: process.env.DOMAIN_PREFIX!,
});

lib/aws-cdk-v2-app-stack.ts

import { Construct } from 'constructs';
import { Stack, StackProps, RemovalPolicy, aws_cognito } from 'aws-cdk-lib';

export interface AwsCdkV2AppStackProps extends StackProps {
  callbackUrls: string[];
  logoutUrls: string[];
  domainPrefix: string;
}

export class AwsCdkV2AppStack extends Stack {
  constructor(scope: Construct, id: string, props: AwsCdkV2AppStackProps) {
    super(scope, id, props);

    // User Pool
    const userPool = new aws_cognito.UserPool(this, 'userPool', {
      userPoolName: 'testUserPool',
      selfSignUpEnabled: false,
      standardAttributes: {
        email: { required: true, mutable: true },
        phoneNumber: { required: false },
      },
      signInCaseSensitive: false,
      autoVerify: { email: true },
      signInAliases: { email: true },
      accountRecovery: aws_cognito.AccountRecovery.EMAIL_ONLY,
      removalPolicy: RemovalPolicy.DESTROY,
    });

    // User Pool Domain
    userPool.addDomain('domain', {
      cognitoDomain: { domainPrefix: props.domainPrefix },
    });
  }
}

CDK Deployします。

$ export DOMAIN_PREFIX=<DOMAIN_PREFIX>
$ cdk deploy

作成したCognito user poolおよびDomainの情報を使用して、Identifier(Entity ID)およびReply URLの値を作成しておきます。これらは次節で使用します。

# Identifier(Entity ID)作成
$ USER_POOL_ID=<作成したUser PoolのID>
$ echo "urn:amazon:cognito:sp:${USER_POOL_ID}"
# Reply URL
$ AWS_REGION=ap-northeast-1
$ echo "https://${DOMAIN_PREFIX}.auth.${AWS_REGION}.amazoncognito.com/saml2/idpresponse"

Azure ADへAmazon CognitoをEnterprise Applicationとして追加

Amazon Cognito user poolをAzure ADにアプリケーションとして追加することにより、両者の間の信頼関係を確立します。

AzureポータルでHome > Azure Active Directory > Enterprise applicationsで、[New application]をクリック。

[Create your own application]をクリック。

[What’s the name of your app?]でアプリ名を指定し、Integrate any other application you don’t find in the gallery (Non-gallery)を選択して、[Create]をクリック。

アプリが追加され、追加されたアプリケーションのOverviewページにリダイレクトされます。[2. Set up single sign on]の[Get started]をクリック。

[Select a single sign-on method]よりSAMLを選択。

[Set up Single Sign-On with SAML]で[Basic SAML Configuration]の[Edit]をクリック。

[Basic SAML Configuration]ペインが開くので、前節で控えた[Identifier (Entity ID)]と[Reply URL]をそれぞれ指定します。[Save]をクリックして保存します。

続いて[Attributes & Claims]の[Edit]をクリック。

[Add a group claim]をクリック。

[Group Claims]ペインが開くので、[Which groups associated with the user should be returned in the claim?]でGroups assigned to the applicationを選択し、[Save]をクリックして保存します。

追加されたUser Group ClaimのClaim name(おそらくhttp://schemas.microsoft.com/ws/2008/06/identity/claims/groupsになるかと思います)を控えます。[X]で[Attributes & Claims]を閉じます。

[Set up Single Sign-On with SAML]で[SAML Signing Certificate]の[App Federation Metadata Url]をコピーして控えます。

Cognito User PoolへIdentity ProviderとClientを追加

CDKの記述を修正し、Cognito User PoolへIdentity ProviderとClientを追加します。

bin/aws-cdk-v2-app.ts

#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { AwsCdkV2AppStack } from '../lib/aws-cdk-v2-app-stack';

const app = new cdk.App();
new AwsCdkV2AppStack(app, 'AwsCdkV2AppStack', {
  callbackUrls: ['https://classmethod.jp/'],
  logoutUrls: ['https://classmethod.jp/'],
  domainPrefix: process.env.DOMAIN_PREFIX!,
  azureAppFederationMetaDataUrl:
    process.env.AZURE_APP_FEDERATION_META_DATA_URL!,
  azureAppUserGroupClaimName: process.env.AZURE_APP_USER_GROUP_CLAIM_NAME!,
  azureAppEmailAddressClaimName:
    'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress',
});

lib/aws-cdk-v2-app-stack.ts

import { Construct } from 'constructs';
import { Stack, StackProps, RemovalPolicy, aws_cognito } from 'aws-cdk-lib';

export interface AwsCdkV2AppStackProps extends StackProps {
  callbackUrls: string[];
  logoutUrls: string[];
  domainPrefix: string;
  azureAppFederationMetaDataUrl: string;
  azureAppUserGroupClaimName: string;
  azureAppEmailAddressClaimName: string;
}

export class AwsCdkV2AppStack extends Stack {
  constructor(scope: Construct, id: string, props: AwsCdkV2AppStackProps) {
    super(scope, id, props);

    const attrForIdpRelationName = 'attrForIdpRelation';

    const userPoolCustomAttributes: {
      [key: string]: aws_cognito.ICustomAttribute;
    } = {};

    userPoolCustomAttributes[attrForIdpRelationName] =
      new aws_cognito.StringAttribute();

    // User Pool
    const userPool = new aws_cognito.UserPool(this, 'userPool', {
      userPoolName: 'testUserPool',
      selfSignUpEnabled: false,
      standardAttributes: {
        email: { required: true, mutable: true },
        phoneNumber: { required: false },
      },
      customAttributes: userPoolCustomAttributes,
      signInCaseSensitive: false,
      autoVerify: { email: true },
      signInAliases: { email: true },
      accountRecovery: aws_cognito.AccountRecovery.EMAIL_ONLY,
      removalPolicy: RemovalPolicy.DESTROY,
    });

    // User Pool Domain
    userPool.addDomain('domain', {
      cognitoDomain: { domainPrefix: props.domainPrefix },
    });

    // User Pool Identity Provider
    const userPoolProvider = new aws_cognito.CfnUserPoolIdentityProvider(
      this,
      'userPoolProvider',
      {
        providerName: 'testUserPoolProvider',
        providerType: 'SAML',
        userPoolId: userPool.userPoolId,
        providerDetails: {
          MetadataURL: props.azureAppFederationMetaDataUrl,
        },
        attributeMapping: {
          groups: props.azureAppUserGroupClaimName,
          email: props.azureAppEmailAddressClaimName,
        },
      }
    );

    // User Pool Client
    userPool.addClient('client', {
      userPoolClientName: 'testUserPoolClient',
      generateSecret: false,
      oAuth: {
        callbackUrls: props.callbackUrls,
        logoutUrls: props.logoutUrls,
        flows: { authorizationCodeGrant: true },
        scopes: [
          aws_cognito.OAuthScope.EMAIL,
          aws_cognito.OAuthScope.OPENID,
          aws_cognito.OAuthScope.PROFILE,
        ],
      },
      supportedIdentityProviders: [
        aws_cognito.UserPoolClientIdentityProvider.custom(
          userPoolProvider.providerName
        ),
      ],
    });
  }
}
  • User Pool Identity ProviderのConstruct Classは、High Level ConstructのUserPoolIdentityProviderがなぜかResourceの作成ができなかったため、仕方なくCfnUserPoolIdentityProviderを使用してLow Levelで作成しています。しかしuserPool.addClient()supportedIdentityProvidersではHigh Level ConstructでIdentity Providerを指定する必要があるため、上手いことLowからHighへ変換しています。
  • 上記ではUser Pool Clientの新規作成時にIdPを設定していますが、既存のClientに対してIdPを設定する場合でもCDK Deployは問題ありませんでした。

前節で取得したClaim nameとApp Federation Metadata Urlを環境変数に指定し、CDK Deoloyします。

$ export AZURE_APP_USER_GROUP_CLAIM_NAME=<AZURE_APP_USER_GROUP_CLAIM_NAME>
$ export AZURE_APP_FEDERATION_META_DATA_URL=<AZURE_APP_FEDERATION_META_DATA_URL>
$ cdk deploy

Azure ADアプリケーションへのユーザーのアサイン

Azure ADアプリケーションを使用してサインインをさせたいユーザーをアサインします。

AzureポータルでHome > Azure Active Directory > Enterprise applicationsで、作成したアプリケーションを選択して開きます。

[Assign users and groups]をクリック。

[Add user/group]をクリック。

[Add Assignment]-[Users]で[None Selected]をクリックし、アプリケーションを使用してサインインさせたいユーザーを一覧から選択し、[Select]をクリック。

[Assign]をクリック。

これでユーザーをアサインできました。

これを忘れるとユーザーサインイン時にAADSTS50105エラーが発生します。

動作確認

AzureADによるSAML認証の動作確認をHosted UIのURLにアクセスして行ってみます。

Hosted UIはUser Pool Clientのダッシュボードから開けますが、URLを手動作成することも可能です。

$ DOMAIN_PREFIX=<DOMAIN_PREFIX>
$ CLIENT_ID=<作成したUser Pool CliantのID>
$ REDIRECT_URI=https%3A%2F%2Fclassmethod.jp%2F //Callback URLのURLエンコード
$ AWS_REGION=ap-northeast-1
$ ENDPOINT=${DOMAIN_PREFIX}.auth.${AWS_REGION}.amazoncognito.com/oauth2/authorize

$ echo "https://${ENDPOINT}?response_type=code&scope=email+openid&client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}"

作成したHosted UIのURLにアクセスすると、Azure ADの認証画面が開きました。前節でアサインしたユーザーのIDを指定して次へ進みます。

パスワードを指定して次へ進みます。

corporate IDでのサインインを求められた場合は、Cognito User Poolに登録したIdentity Providerを選択します。

認証が成功し、Callback URLにリダイレクトされました!

おわりに

AWS CDKを使って、Amazon Cognito user poolの外部IdPとしてAzure ADを設定してみました。

CDKでUser Pool Identity Providerを扱うことなど今まで無かったので試行錯誤がありましたがなんとか出来てよかったです。

参考

以上