CDKでCognito UserPoolとClientを作成しトリガーやFederationを設定する

awsauth

CloudFormationでCognito UserPoolを作成すると、以前はドメインやFederationの設定などを手作業で行う必要があったが、 去年の10月に諸々のリソースが追加され、その必要がなくなった。

API GatewayでCognitoの認証をかけて必要ならログイン画面に飛ばす処理をGoで書く - sambaiz-net

今回はCDKの高レベルAPIを用いて、UserPoolとClientを作成し、トリガーやGoogleのFederationの設定を行って、特定のGoogleアカウントでのみ登録されるようにする。 全体のコードはGitHubにある。

Cognito UserPoolのPreSignUp時に呼ばれるLambdaで登録ユーザーを制限する - sambaiz-net

UserPool

事前にdomainPrefixを決めておき、OAuthのclient_idとsecretをSecretsManagerに置いておく。

standardAttributesと、ログイン時にusernameの代わりとなるsignInAliasesは後から変更できない。 変更してデプロイしてもreplaceではなくエラーになってしまう。 トリガーのruntimeはCDKと同じNodeにしても良いがバージョンのライフサイクルが早く追従するのが大変なのでGoにしている。

private createUserPool(userPoolName: string, domainPrefix: string, signUpAllowEmails: string[]): UserPool {
  const userPool = new UserPool(this, 'UserPool', {
    userPoolName,
    standardAttributes: {
      email: {
        required: true,
        mutable: true
      },
      fullname: {
        required: true,
        mutable: true
      },
    },
    signInAliases: { username: true, email: true },
    lambdaTriggers: {
      preSignUp: new Function(this, 'PreSignUpFunction', {
        runtime: Runtime.GO_1_X,
        code: Code.fromAsset(path.join(__dirname, 'userPoolTrigger')),
        handler: "preSignUp.main",
        environment: {
          ALLOW_EMAILS: signUpAllowEmails.join(",")
        }
      })
    },
  })
  userPool.addDomain("UserPoolDomain", {
    cognitoDomain: {
      // Create OAuth 2.0 Client ID for web application with
      // Authorized JavaScript origins: https://{domainPrefix}.auth.{region}.amazoncognito.com
      // Redirect URI: https://{domainPrefix}.auth.{region}.amazoncognito.com/oauth2/idpresponse
      // at https://console.developers.google.com/apis/credentials and put client_secret.json on SecretsManager
      domainPrefix: "sambaiz-google-auth-test",
    }
  })

  new cdk.CfnOutput(this, 'UserPoolArnOutput', {
    value: userPool.userPoolArn
  })
  new cdk.CfnOutput(this, 'UserPoolDomainOutput', {
    value: `${domainPrefix}.auth.${this.region}.amazoncognito.com`
  })
  return userPool
}

IdentityProviderGoogle

GoogleのFederationの設定を行う。 Googleのコンソールからダウンロードできるjsonの認証情報はネストしているためにうまくSecretsManagerのAPIで値が抽出できなかったので client_idとsecretのみをkey-valueで格納した。

private createIdentityProviderGoogle(userPool: UserPool, secretName: string): UserPoolIdentityProviderGoogle {
  const oauthClientSecret = Secret.fromSecretNameV2(this, "GoogleOAuthClientSecret", secretName)
  return new UserPoolIdentityProviderGoogle(this, "UserPoolIdentityProviderGoogle", {
    userPool,
    clientId: oauthClientSecret.secretValueFromJson('client_id').toString(),
    clientSecret: oauthClientSecret.secretValueFromJson('client_secret').toString(),
    scopes: ["profile", "email", "openid"],
    attributeMapping: {
      email: ProviderAttribute.GOOGLE_EMAIL,
      fullname: ProviderAttribute.GOOGLE_NAME,
    }
  })
}

Client

supportedIdentityProviders に含めたIdPの設定が既にないと失敗するので、addDependency()で先に作られるようにする必要がある。 callbackUrls にはALBであれば、https://<alb-domain>/oauth2/idpresponse を入れる。

LambdaとALBでCognito認証をかけて失敗したらログイン画面に飛ばす - sambaiz-net

private createUserPoolClient(userPool: UserPool, userPoolClientName: string, callbackUrls: string[]): UserPoolClient {
  const userPoolClient = userPool.addClient("UserPoolClient", {
    userPoolClientName,
    supportedIdentityProviders: [UserPoolClientIdentityProvider.GOOGLE],
    generateSecret: true,
    oAuth: {
      callbackUrls,
      flows: {
        authorizationCodeGrant: true,
      }
    }
  })

  new cdk.CfnOutput(this, `UserPoolClientIdOutput`, {
    value: userPoolClient.userPoolClientId
  })
  return userPoolClient
}

デプロイし、https://${props.domainPrefix}.auth.${this.region}.amazoncognito.com/authorize?client_id=${client.userPoolClientId}&redirect_uri=${encodeURIComponent(props.callbackUrls[0])}&response_type=code&identity_provider=Google にアクセスすると、Googleの認証の後、redirect_uriにcode付きで遷移する。

OAuth2.0のメモ - sambaiz-net