サイトアイコン

toLog

CognitoのSDKでアドレス変更してもコード未確認状態で使えてしまう問題

  • 更新日:
  • 投稿日:
サムネイル

この記事は最終更新日から半年以上が経過しています。

(注意) 公式で対応されました 🎉

こちら公式で対応されて、変更前のアドレスを保てるようになったようです。詳しくは次のリンクを参照ください 🙏

はじめに

前述の通り公式で対応されたので、かつてはこのような対応でお茶を濁していた程度です 🙏

ここ最近、個人的に 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 の UpdateUserAttributesemail (ユーザー ID)を変更すると確認コードが受信されるが、Cognito へのコード検証を待たずに変更されたユーザー ID で Cognito への操作が可能
  • 受信された確認コードを Cognito へ送信して Confirm する関数が見当たらず

対応策

前提

  • 公式で推奨された方法ではありません苦肉の策です
  • 低レベル API の JavaScript-SDK で Cognito を操作(Amplify ではない)
  • SDK 操作は全て API Gateway に閉じ込めて実装、クライアントは API を叩くだけ

イメージ

本当に簡単に処理の流れをイメージ化。

image_cognito_change_address

流れ

  • アドレス(ユーザーネーム)を変更する 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 はこれからも付き合っていこうと思っているので早く修正されると嬉しいです。


プロフィール画像

canji

とにかく私的にサービスを作りたい発作を起こしている。お腹はペコペコ。

  • toLog Tools icon
  • dots icon