本連載では、以下のイメージのように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 まで。