マイナビニュースマイナビ

Amazon CognitoとSpring Sercurityを使ったOAuth2 Loginの実装(6)

【連載】

AWSで作るマイクロサービス

【第17回】Amazon CognitoとSpring Sercurityを使ったOAuth2 Loginの実装(6)

[2021/05/24 08:00]川畑 光平 ブックマーク ブックマーク

本連載では、以下のイメージのようにOAuth2 Loginをベースとしたアーキテクチャを想定した環境構築を進めています。

構成図

前回は、CloudFormationで構築したAmazon Cognitoのアプリクライアントからクライアントシークレットを取得し、Parameter Storeへ設定するLambdaファンクションを実装しました。今回は引き続き、Cognitoへユーザーを登録/サインアップしてステータスを更新するLambdaファンクションの実装方法を解説していきます。

なお、実際のソースコードはGitHub上にコミットしています。以降のソースコードでは、本質的でない記述を省略している部分があるので、実行コードを作成する際は、必要に応じて適宜GitHubにあるソースコードも参照してください。

Cognitoにユーザーを登録するLambdaファンクションの実装

Cognitoユーザープールへユーザーを追加するLambdaファンクションを実装します。ファンクションクラスの基本的な構成は、前回実装したParameter Storeへクライアントシークレットを設定する際の構成と同じです。

package org.debugroom.mynavi.sample.aws.microservice.lambda.app.function;

// omit

@Slf4j
public class AddCognitoUserFunction implements Function<Map<String, Object>, Message<String>> { //(A)

    @Autowired
    ServiceProperties serviceProperties; //(B)

    @Autowired
    CloudFormationStackResolver cloudFormationStackResolver; //(C)

    @Autowired
    AWSCognitoIdentityProvider awsCognitoIdentityProvider;

    @Override
    public Message apply(Map stringObjectMap) {
        log.info(this.getClass().getName() +  "  has started!");
        String userPoolId = cloudFormationStackResolver.getExportValue(
          serviceProperties.getCloudFormation().getCognito().getUserPoolId()); //(D)
        ListUsersRequest listUsersRequest = new ListUsersRequest().withUserPoolId(userPoolId);
        ListUsersResult listUsersResult = awsCognitoIdentityProvider.listUsers(
          listUsersRequest);

       int numberOfCognitoUser = listUsersResult.getUsers().size(); //(E)
       List attributeTypes = new ArrayList<>();
       AdminCreateUserRequest adminCreateUserRequest = new AdminCreateUserRequest()
          .withUserPoolId(userPoolId)
          .withTemporaryPassword("test01");
       attributeTypes.add(new AttributeType().withName("family_name").withValue("mynavi"));
       attributeTypes.add(new AttributeType().withName("given_name").withValue("taro"));
       attributeTypes.add(new AttributeType().withName("custom:isAdmin").withValue("1"));
       if(numberOfCognitoUser != 0){
           adminCreateUserRequest.withUsername("taro.mynavi" + Integer.toString(numberOfCognitoUser));
           attributeTypes.add(new AttributeType()
              .withName("custom:loginId").withValue("taro.mynavi" + Integer.toString(numberOfCognitoUser)));//(F)
       }else {
           adminCreateUserRequest.withUsername("taro.mynavi");
           attributeTypes.add(new AttributeType().withName("custom:loginId").withValue("taro.mynavi"));
       }
           adminCreateUserRequest.withUserAttributes(attributeTypes);
           AdminCreateUserResult adminCreateUserResult = awsCognitoIdentityProvider.adminCreateUser(
               adminCreateUserRequest); //(G)
           return MessageBuilder.withPayload("Complete!").build();
       }

}

実装のポイントは以下の通りです。

項番 説明
A java.util.function.Functionを実装します。Input型としてハンドラクラスで生成したMapオブジェクトクラスを、Output型としてorg.springframework.messaging.Messageを指定します
B CloudFormationのスタックから取得するためのエクスポート名をプロパティクラスに保持します。実際のエクスポート名は設定ファイルである「applicaiton.yml」に記載します。設定ファイルの記載については連載「AWSで作るクラウドネイティブアプリケーションの応用」の第7回も参考にしてください
C CloudFormationのスタックからOutput要素で出力した値を取得するユーティリティクラスをインジェクションします。具体的な実装はGitHubにコミットしてあるように、CloudFormationのSDKクライアントを使ってエクスポート値を取得します
D ユーザープールIDをCを使って取得します
E ユーザープールに既に登録されているユーザーの数を取得します
F Eの数に応じて、ログインIDや、ユーザーIDに相当するusernameが重複しないように設定します
G awsCognitoIdentityProvider.adminCreateUser()メソッドを実行して、ユーザーを登録します

サインアップステータスを変更するLambdaファンクションの実装

続いて、Cognitoユーザー作成後にサインアップステータスを変更するLambdaファンクションを実装します。前節と同様、基本的なLambdaファンクション実装の構成は同じです。

package org.debugroom.mynavi.sample.aws.microservice.lambda.app.function;

import java.nio.charset.StandardCharsets;
import java.util.Base64;

// omit

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

@Slf4j
public class ChangeCognitoUserStatusFunction implements Function<Map<String, Object>, Message<String>> {

    @Autowired
    ServiceProperties serviceProperties;

    @Autowired
    CloudFormationStackResolver cloudFormationStackResolver;

    @Autowired
    AWSCognitoIdentityProvider awsCognitoIdentityProvider;

    @Autowired
    AWSSimpleSystemsManagement awsSimpleSystemsManagement;

    @Override
    public Message<String> apply(Map<String, Object> stringObjectMap) {
        log.info(this.getClass().getName() +  "  has started!");

        String userPoolId = cloudFormationStackResolver.getExportValue(
          serviceProperties.getCloudFormation().getCognito().getUserPoolId());
        String appClientId = cloudFormationStackResolver.getExportValue(
          serviceProperties.getCloudFormation().getCognito().getAppClientId());
        String clientSecret = getParameterFromParameterStore(
          serviceProperties.getSystemsManagerParameterStore().getCognito().getAppClientSecret(), true);
        ListUsersRequest listUsersRequest = new ListUsersRequest().withUserPoolId(userPoolId);
        ListUsersResult listUsersResult = awsCognitoIdentityProvider.listUsers(
          listUsersRequest); //(A)

        listUsersResult.getUsers().stream()
          .filter(userType -> Objects.equals(userType.getUserStatus(), "FORCE_CHANGE_PASSWORD")) //(B)
          .forEach(userType -> {
              AdminInitiateAuthRequest adminInitiateAuthRequest
                      = adminInitiateAuthRequest(userType.getUsername(), userPoolId, appClientId, clientSecret); //(C)
              AdminInitiateAuthResult  adminInitiateAuthResult =
                      awsCognitoIdentityProvider.adminInitiateAuth(adminInitiateAuthRequest); //(D)

              if(Objects.equals(adminInitiateAuthResult.getChallengeName(), "NEW_PASSWORD_REQUIRED")){ //(E)
                  Map<String, String> challengeResponses = new HashMap<>();
                  challengeResponses.put("USERNAME", userType.getUsername());
                  challengeResponses.put("NEW_PASSWORD", "test02"); //(F)
                  challengeResponses.put("userAttributes.given_name",
                          userType.getAttributes().stream().filter(attributeType ->
                              Objects.equals(attributeType.getName(), "given_name")).findFirst().get().getValue()); //(G)
                  challengeResponses.put("SECRET_HASH", calculateSecretHash(appClientId, clientSecret, userType.getUsername())); //(H)

                  AdminRespondToAuthChallengeRequest adminRespondToAuthChallengeRequest =
                          new AdminRespondToAuthChallengeRequest()
                          .withChallengeName(adminInitiateAuthResult.getChallengeName())
                          .withUserPoolId(userPoolId)
                          .withClientId(appClientId)
                          .withSession(adminInitiateAuthResult.getSession()) //(I)
                          .withChallengeResponses(challengeResponses); //(J)

                  AdminRespondToAuthChallengeResult adminRespondToAuthChallengeResult =
                          awsCognitoIdentityProvider.adminRespondToAuthChallenge(adminRespondToAuthChallengeRequest); //(K)
              }

          });

        return MessageBuilder.withPayload("Complete!").build();
    }

    private AdminInitiateAuthRequest adminInitiateAuthRequest(
      String userName, String userPoolId, String appClientId, String clientSecret){
        AdminInitiateAuthRequest adminInitiateAuthRequest = new AdminInitiateAuthRequest();
        adminInitiateAuthRequest.setAuthFlow(AuthFlowType.ADMIN_USER_PASSWORD_AUTH); //(L)
        adminInitiateAuthRequest.setUserPoolId(userPoolId);
        adminInitiateAuthRequest.setClientId(appClientId);
        Map<String, String> authParameters = new HashMap<>();
        authParameters.put("USERNAME", userName);
        authParameters.put("PASSWORD", "test01");
        authParameters.put("SECRET_HASH", calculateSecretHash(appClientId, clientSecret, userName)); //(M)
        adminInitiateAuthRequest.setAuthParameters(authParameters);
        return adminInitiateAuthRequest;
    }

    private static String calculateSecretHash(String userPoolClientId,
        String userPoolClientSecret, String userName) { //(N)
        final String HMAC_SHA256_ALGORITHM = "HmacSHA256";

        SecretKeySpec signingKey = new SecretKeySpec(
          userPoolClientSecret.getBytes(StandardCharsets.UTF_8),
          HMAC_SHA256_ALGORITHM);
        try {
            Mac mac = Mac.getInstance(HMAC_SHA256_ALGORITHM);
            mac.init(signingKey);
            mac.update(userName.getBytes(StandardCharsets.UTF_8));
            byte[] rawHmac = mac.doFinal(userPoolClientId.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(rawHmac);
        } catch (Exception e) {
            throw new RuntimeException("Error while calculating ");
        }
    }

    private String getParameterFromParameterStore(String paramName, boolean isEncripted){
        GetParameterRequest request = new GetParameterRequest();
        request.setName(paramName);
        request.setWithDecryption(isEncripted);
        return awsSimpleSystemsManagement.getParameter(request).getParameter().getValue();
    }

}

実装のポイントは以下の通りです。

項番 説明
A ユーザープールID、アプリクライアントIDをCloudFormationのスタック情報から、クライアントシークレットをSystems Manager Parameter Storeから取得し、ユーザープールに存在するユーザーの一覧を取得します
B 取得したユーザーの中で、ステータスが「FORCE_CHANGE_PASSWORD」である初期ユーザーを抽出します
C Cognitoユーザーをサーバ側でサインアップ処理を完了する場合、公式ガイド「ユーザープール認証フロー」の「サーバ側の認証フロー」に従って、ユーザーを認証するために、AdminInitiateAuthRequestを生成します
D AdminInitiateAuth APIを呼び出し、AdminRespondToAuthChallengeRequestの生成に必要なセッションパラメータを含むAdminInitiateAuthResultを取得します
E AdminInitiateAuthResultのチャレンジ名が「NEW_PASSWORD_REQUIRED」だった場合に、チャレンジレスポンスを生成し、AdminRespondToAuthChallenge APIを呼び出します
F 新たなパスワードを「test02」に変更します
G チャレンジレスポンスに「userAttributes.given_name」を含めておきます。これはユーザープールで自身が必須定義した内容に応じて必要となるパラメータです
H チャレンジレスポンスに「SECRET_HASH」を含めておく必要があります。SECRET_HASHの実装の詳細はNを参照してください
I AdminRespondToAuthChallengeRequestにAdminInitiateAuthResultが持つ「セッションパラメータ」が必要になります。詳細は、API Referenceの「AdminRespondToAuthChallenge Session」を参照してください
J AdminRespondToAuthChallengeRequestにチャレンジレスポンスを設定します
K AdminRespondToAuthChalleng APIを呼び出して、ユーザーのステータスを変更します
L AdminInitiateAuthRequestでは、認証フローを「ADMIN_USER_PASSWORD_AUTH」で設定します
M AdminInitiateAuthRequestにおいても、「SECRET_HASH」を含めておきます。SECRET_HASHの実装の詳細は(N)を参照してください
N Amazon CognitoのユーザープールAPIは、ユーザープールIDやアプリクライアントID、クライアントシークレットを元にSecretHash値が必要なものがあります。SecretHash値が必要なAPIおよびその計算方法は公式ガイドの「ユーザーアカウントのサインアップと確認」にある「SecretHash値の計算」を参照してください

* * *

今回は、Cognitoユーザープールにユーザーを追加し、サインアップステータスを変更するLambdaファンクションを実装しました。次回は、前回実装したクライアントシークレットをParameter Storeへ登録するLambdaファンクションを含めて、カスタムリソースとして実行するCloudFormationテンプレートを作成し、実行してみます。

著者紹介


川畑 光平(KAWABATA Kohei) - NTTデータ
エグゼクティブ ITスペシャリスト ソフトウェアアーキテクト・デジタルテクノロジーストラテジスト(クラウド)

金融機関システム業務アプリケーション開発・システム基盤担当、ソフトウェア開発自動化関連の研究開発を経て、デジタル技術関連の研究開発・推進に従事。

Red Hat Certified Engineer、Pivotal Certified Spring Professional、AWS Certified Solutions Architect Professional等の資格を持ち、アプリケーション基盤・クラウドなど様々な開発プロジェクト支援にも携わる。AWS Top Engineers & Ambassadors選出。

本連載の内容に対するご意見・ご質問は Facebook まで。

※ 本記事は掲載時点の情報であり、最新のものとは異なる場合がございます。予めご了承ください。

一覧はこちら

連載目次

もっと知りたい!こちらもオススメ

【連載】Kubernetes入門 [15] CI/CDにおける潮流 - CIOpsからGitOpsへ

【連載】Kubernetes入門 [15] CI/CDにおける潮流 - CIOpsからGitOpsへ

今回は、CI/CDツールについてGitOpsやIaC(Infrastructure as Code)など、さまざまな側面から紹介することで、位置付けをより深く理解していただきます。なお、個別のCI/CD系のツールの使い方などの詳細については次回以降紹介します。

関連リンク

この記事に興味を持ったら"いいね!"を Click
Facebook で TECH+ の人気記事をお届けします
注目の特集/連載
[解説動画] Googleアナリティクス分析&活用講座 - Webサイト改善の正しい考え方
Slackで始める新しいオフィス様式
Google Workspaceをビジネスで活用する
ニューノーマル時代のオウンドメディア戦略
ミッションステートメント
次世代YouTubeクリエイターの成長戦略
教えてカナコさん! これならわかるAI入門
AWSではじめる機械学習 ~サービスを知り、実装を学ぶ~
Kubernetes入門
SAFeでつくる「DXに強い組織」~企業の課題を解決する13のアプローチ~
AWSで作るマイクロサービス
マイナビニュース スペシャルセミナー 講演レポート/当日講演資料 まとめ
セキュリティアワード特設ページ

一覧はこちら

今注目のIT用語の意味を事典でチェック!

一覧はこちら

会員登録(無料)

ページの先頭に戻る