(注意) 公式で対応されました 🎉
こちら公式で対応されて、変更前のアドレスを保てるようになったようです。詳しくは次のリンクを参照ください 🙏
- Cognito: unable to login with email if user attempts to update email address > closed comment
- UserAttributeUpdateSettingsType
はじめに
前述の通り公式で対応されたので、かつてはこのような対応でお茶を濁していた程度です 🙏
ここ最近、個人的に Cognito を触っているのですが、アドレス変更に バグ? が見受けられます。
ユーザープール属性の email
を SDK で変更すると、変更前のアドレスに検証用コードが送信されこのコードを Cognito に返送することでアドレス変更が完了すると想定されるのですが、このコード検証が未完了のまま変更後のアドレスでユーザープールに対する操作ができてしまいました。
実は Github に 2018/06 から Issue が上がっているのですが、まだ対応されていないようです。
Cognito: unable to login with email if user attempts to update email address
Cognito 開発者もコメントしているのですが、私の拙い翻訳で解釈する限り、問題は認識しているが現状対応しきれていないらしいです、、、 ここまで放置するとそもそもリソースを割くつもりがないのではと勘ぐりたくなります。
これを回避するには、Coginito 側でコード検証を行ってくれないので、現状では自前で実装するしかありません。
先ほどの Issue で回避策が提案されているのですが、AWS Amplify と 電話番号でのコード検証を行っているので、今回はこれを JavaScript-SDK と メールアドレスでのコード検証で実装してみました。
問題点
- SDK の UpdateUserAttributes で
email
(ユーザー ID)を変更すると確認コードが受信されるが、Cognito へのコード検証を待たずに変更されたユーザー ID で Cognito への操作が可能 - 受信された確認コードを Cognito へ送信して
Confirm
する関数が見当たらず
対応策
前提
- 公式で推奨された方法ではありません、苦肉の策です
- 低レベル API の JavaScript-SDK で Cognito を操作(Amplify ではない)
- SDK 操作は全て API Gateway に閉じ込めて実装、クライアントは API を叩くだけ
イメージ
本当に簡単に処理の流れをイメージ化。
流れ
- アドレス(ユーザーネーム)を変更する API を呼び出し
- SDK の UpdateUserAttributes で属性変更を Cognito にリクエスト
- 属性変更時にカスタムメッセージ Lambda トリガーを Cognito から呼び出し 注意したいのがこの時点でアドレスは変更されていて、コード検証が未了なのですが変更後のアドレスでログイン等が行えてしまいます
- コード検証が未了なので変更後のアドレスを無効化します、Lambda トリガー内で変更前のアドレスに再び戻すことで変更後のアドレスを無効化
- 確認用コードが変更前のアドレスに送信されるので、コード確認と再びアドレスを変更する API を呼び出し
- SDK の VerifyUserAttribute にて確認用コードとアクセストークンで
email
(標準属性)の所有確認を行うことで擬似的にコード検証を実行 - 所有確認が成功したならば UpdateUserAttributes で再びアドレスを変更
- 属性変更に伴いカスタムメッセージ Lambda トリガーを呼び出し
- コード検証は完了しているのですが確認コードが再び送信されてしまうので、 カスタムメッセージ Lambda トリガーにて送信処理を阻害
実装内容
適当に作っているので詳細な解説は省きます。参考 URL を載せるので参照してみてください。また、適宜自身の環境に読み替えてください。
Cognito の CFn サンプルテンプレート
1AWSTemplateFormatVersion: 2010-09-09
2
3Parameters:
4 ServiceName:
5 Type: String
6 Default: "sample"
7 CustomMessageTriggerName:
8 Type: String
9 Default: sample-update-user-attributes-confirm
10
11Resources:
12 UserPool:
13 Type: AWS::Cognito::UserPool
14 Properties:
15 UserPoolName: !Sub ${ServiceName}-users
16 AdminCreateUserConfig:
17 AllowAdminCreateUserOnly: false
18 UsernameAttributes:
19 - email
20 AutoVerifiedAttributes:
21 - email
22 Policies:
23 PasswordPolicy:
24 MinimumLength: 8
25 AccountRecoverySetting:
26 RecoveryMechanisms:
27 - Name: verified_email
28 Priority: 1
29 LambdaConfig:
30 CustomMessage: !Sub "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${CustomMessageTriggerName}"
31 Schema:
32 - Name: email
33 AttributeDataType: String
34 DeveloperOnlyAttribute: false
35 Mutable: true
36 Required: true
37 - Name: validated_email
38 AttributeDataType: String
39 DeveloperOnlyAttribute: false
40 Mutable: true
41 Required: false
42
43 UserPoolClient:
44 Type: AWS::Cognito::UserPoolClient
45 Properties:
46 ClientName: !Sub ${ServiceName}-users-client
47 GenerateSecret: false
48 RefreshTokenValidity: 3
49 UserPoolId: !Ref UserPool
50 ExplicitAuthFlows:
51 - ADMIN_NO_SRP_AUTH
52 - USER_PASSWORD_AUTH
53
54 IdentityPool:
55 Type: AWS::Cognito::IdentityPool
56 Properties:
57 AllowUnauthenticatedIdentities: false
58 IdentityPoolName: !Sub ${ServiceName}-users
59 CognitoIdentityProviders:
60 - ClientId: !Ref UserPoolClient
61 ProviderName: !Sub cognito-idp.${AWS::Region}.amazonaws.com/${UserPool}
サンプル API の SAM テンプレート
1AWSTemplateFormatVersion: 2010-09-09
2Transform: AWS::Serverless-2016-10-31
3
4Parameters:
5 ServiceName:
6 Type: String
7 Default: "sample"
8 Environment:
9 Type: String
10 Default: "develop"
11 UserPoolId:
12 Type: String
13 Default: "sample-pool-id"
14 NoEcho: True
15 AppClientId:
16 Type: String
17 Default: "sample-app-client-id"
18 NoEcho: True
19
20Globals:
21 Function:
22 Timeout: 30
23 Environment:
24 Variables:
25 APP_CLIENT_ID: !Ref AppClientId
26
27Resources:
28 ServerlessApi:
29 Type: AWS::Serverless::Api
30 Properties:
31 Name: !Sub ${ServiceName}-api
32 StageName: !Ref Environment
33 Auth:
34 Authorizers:
35 CognitoAuthorizer:
36 UserPoolArn: !Sub arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/${UserPoolId}
37
38 UpdateUserAttributes:
39 Type: AWS::Serverless::Function
40 Properties:
41 FunctionName: !Sub ${ServiceName}-update-user-attributes
42 Description: "update user attributes"
43 Runtime: nodejs12.x
44 CodeUri: update-attributes/
45 Handler: app.handler
46 Events:
47 UpdateUserAttributesEvent:
48 Type: Api
49 Properties:
50 RestApiId: !Ref ServerlessApi
51 Path: update-attributes
52 Method: POST
53
54 UpdateUserAttributesConfirm:
55 Type: AWS::Serverless::Function
56 Properties:
57 FunctionName: !Sub ${ServiceName}-update-user-attributes-confirm
58 Description: "confirm updated user attributes"
59 Runtime: nodejs12.x
60 CodeUri: update-attributes-confirm/
61 Handler: app.handler
62 Events:
63 UpdateUserAttributesConfirmEvent:
64 Type: Api
65 Properties:
66 RestApiId: !Ref ServerlessApi
67 Path: update-attributes/confirm
68 Method: POST
アドレス変更(UpdateUserAttributes)
1"use strict";
2
3const AWS = require("aws-sdk");
4const cognito = new AWS.CognitoIdentityServiceProvider();
5
6exports.handler = async (event, context, callback) => {
7 let response = null;
8
9 try {
10 const accessToken = event["accessToken"];
11 if (typeof accessToken === "undefined") {
12 throw new Error("不正な操作が行われました");
13 }
14
15 const oldUserEmail = event["oldUserEmail"];
16 if (typeof oldUserEmail === "undefined") {
17 throw new Error("古いメールアドレスを入力してください");
18 }
19
20 const newUserEmail = event["newUserEmail"];
21 if (typeof newUserEmail === "undefined") {
22 throw new Error("新しいメールアドレスを入力してください");
23 }
24
25 const params = {
26 AccessToken: accessToken,
27 UserAttributes: [
28 {
29 Name: "email",
30 Value: newUserEmail,
31 },
32 {
33 Name: "custom:validated_email",
34 Value: oldUserEmail,
35 },
36 ],
37 };
38
39 const result = await cognito
40 .updateUserAttributes(params)
41 .promise()
42 .catch((error) => {
43 throw error;
44 });
45
46 response = {
47 statusCode: 200,
48 headers: {
49 "Content-Type": "application/json; charset=utf-8",
50 },
51 body: JSON.stringify({
52 status: "success",
53 }),
54 isBase64Encoded: false,
55 };
56 } catch (err) {
57 response = {
58 statusCode: 400,
59 headers: {
60 "Content-Type": "application/json; charset=utf-8",
61 },
62 body: JSON.stringify({
63 status: "failed",
64 message: err.message,
65 }),
66 isBase64Encoded: false,
67 };
68 }
69
70 return response;
71};
コード検証とアドレス変更(UpdateUserAttributesConfirm)
1"use strict";
2
3const AWS = require("aws-sdk");
4const cognito = new AWS.CognitoIdentityServiceProvider();
5
6exports.handler = async (event, context, callback) => {
7 let response = null;
8 let params = null;
9 let result = null;
10
11 try {
12 const accessToken = event["accessToken"];
13 if (typeof accessToken === "undefined") {
14 throw new Error("不正な操作が行われました");
15 }
16
17 const newUserEmail = event["newUserEmail"];
18 if (typeof newUserEmail === "undefined") {
19 throw new Error("不正な操作が行われました");
20 }
21
22 const confirmationCode = event["confirmationCode"];
23 if (typeof confirmationCode === "undefined") {
24 throw new Error("確認用コードを入力してください");
25 }
26
27 params = {
28 AccessToken: accessToken,
29 AttributeName: "email",
30 Code: confirmationCode,
31 };
32
33 result = await cognito
34 .verifyUserAttribute(params)
35 .promise()
36 .catch((error) => {
37 throw error;
38 });
39
40 if (!Object.keys(result).length) {
41 params = {
42 AccessToken: accessToken,
43 UserAttributes: [
44 {
45 Name: "email",
46 Value: newUserEmail,
47 },
48 {
49 Name: "custom:validated_email",
50 Value: newUserEmail,
51 },
52 ],
53 };
54
55 result = await cognito
56 .updateUserAttributes(params)
57 .promise()
58 .catch((error) => {
59 throw error;
60 });
61 }
62
63 response = {
64 statusCode: 200,
65 headers: {
66 "Content-Type": "application/json; charset=utf-8",
67 },
68 body: JSON.stringify({
69 status: "success",
70 }),
71 isBase64Encoded: false,
72 };
73 } catch (err) {
74 response = {
75 statusCode: 400,
76 headers: {
77 "Content-Type": "application/json; charset=utf-8",
78 },
79 body: JSON.stringify({
80 status: "failed",
81 message: err.message,
82 }),
83 isBase64Encoded: false,
84 };
85 }
86
87 return response;
88};
Lambda トリガーの SAM テンプレート
1AWSTemplateFormatVersion: 2010-09-09
2Transform: AWS::Serverless-2016-10-31
3
4Parameters:
5 ServiceName:
6 Type: String
7 Default: "sample"
8 UserPoolId:
9 Type: String
10 Default: "sample-user-pool-id"
11 NoEcho: True
12 AppClientId:
13 Type: String
14 Default: "sample-app-client-id"
15 NoEcho: True
16
17Globals:
18 Function:
19 Timeout: 30
20 Environment:
21 Variables:
22 USER_POOL_ID: !Ref UserPoolId
23
24Resources:
25 CustomMessage:
26 Type: AWS::Serverless::Function
27 Properties:
28 FunctionName: !Sub ${ServiceName}-custom-message
29 Description: "lambda trigger custom message."
30 Runtime: nodejs12.x
31 CodeUri: custom-message/
32 Handler: app.handler
33 Policies:
34 - Version: "2012-10-17"
35 Statement:
36 - Effect: Allow
37 Action: "*"
38 Resource: !Sub arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/${UserPoolId}
39
40 CustomMessagePermission:
41 Type: AWS::Lambda::Permission
42 Properties:
43 FunctionName: !GetAtt CustomMessage.Arn
44 Action: lambda:InvokeFunction
45 Principal: cognito-idp.amazonaws.com
46 SourceArn: !Sub arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/${UserPoolId}
カスタムメッセージ Lambda トリガー
1"use strict";
2
3const AWS = require("aws-sdk");
4const cognito = new AWS.CognitoIdentityServiceProvider();
5
6const USER_POOL_ID = process.env.USER_POOL_ID;
7
8exports.handler = async (event, context, callback) => {
9 if (event.userPoolId === USER_POOL_ID) {
10 if (event.triggerSource === "CustomMessage_UpdateUserAttribute") {
11 const validated_email =
12 event.request.userAttributes["custom:validated_email"];
13
14 const params = {
15 UserPoolId: event.userPoolId,
16 Username: event.userName,
17 UserAttributes: [
18 {
19 Name: "email_verified",
20 Value: "true",
21 },
22 {
23 Name: "email",
24 Value: validated_email,
25 },
26 ],
27 };
28
29 const result = await cognito
30 .adminUpdateUserAttributes(params)
31 .promise()
32 .catch((error) => {
33 throw error;
34 });
35
36 if (validated_email === event.request.userAttributes.email) {
37 throw new Error(
38 "failed to prevent sending unnecessary verification code"
39 );
40 }
41 }
42 }
43
44 callback(null, event);
45};
おわりに
ぺたぺたとサンプルを載せただけになっていますが、こんな問題が AWS でも起きるのだと衝撃を受けて共有した次第です。
にしてもこのコード未検証問題はいつ修正が入るのでしょうか、、、
Cognito はこれからも付き合っていこうと思っているので早く修正されると嬉しいです。