BLOG

ブログ

【Cognito】Docker環境内にCognitoユーザープールを構築する

こちらは、Mavs Advent Calendar2023の13日目の記事です!🎅🎄

こんにちは、ショーです!

認証基盤としてAWSのCognitoを使う機会があり、さらにローカル開発でDockerを使っていたので
「気軽にDockerコンテナ内にCognitoのユーザープールを構築することは出来ないかな」と思い、
試してみたら出来ましたので、その方法をご紹介します!

モチベーション

  • 自分専用のCognitoユーザープールをDockerで作成したい
  • ユーザープールの作り直しを容易に行いたい
  • ローカルCognitoを使用するにあたり、毎回実施する手順で出来るだけ手間がないようにしたい

完成イメージ

イメージ図

今回のブログで以下を実現出来るようにします。

ディレクトリ構成

最終的なディレクトリ構成は以下のようになります。

.
├── mavs-api-server
│   ├── .aws                                                           // awsプロファイル設定
│   │   ├── config
│   │   └── credentials
│   ├── build.gradle
│   ├── gradle
│   ├── gradlew
│   ├── gradlew.bat
│   ├── settings.gradle
│   └── src
│       └── main
│           ├── java
│           │   └── com
│           │       ├── Application.java
│           │       ├── auth
│           │       │   └── controller
│           │       │       ├── AuthController.java                    // 認証処理コントローラー
│           │       │       └── form
│           │       │           ├── request
│           │       │           │   ├── AuthSignInRequestData.java     // APIリクエストデータ定義クラス
│           │       │           └── response
│           │       │               └── AuthSignInResponseData.java    // APIレスポンスデータ定義クラス
│           │       └── domain
│           │           ├── dto
│           │           │   └── AmazonCognitoListUserDto.java          // CognitoユーザーデータDTO
│           │           ├── repository
│           │           │   └── AmazonCognitoRepository.java           // Cognito操作用リポジトリ
│           │           └── service
│           │               └── AuthService.java                       // 認証処理サービス
│           └── resources
│               └── application.yml
├── docker-compose.yml
├── start_services.sh                                                  // 各サービス起動シェルスクリプト
└── mock-user-pool
    ├── .aws                                                           // awsプロファイル設定
    │   ├── config
    │   └── credentials
    ├── bash
    │   └── create-user-pool.sh                                        // Cognitoユーザープール作成シェルスクリプト
    └── output
        └── mock-cognito.properties                                    // ユーザープールID、アプリクライアントID設定ファイル

動作環境

  • Docker(DockerCompose)
  • moto/server:latest
  • Java:Amazon Corretto 17 (※)
  • Spring Boot:3.1.1 (※)

※ 今回の記事ではJava(Spring)を使用しますが、他のバックエンドの言語でも問題ございません。

構築手順

目次

  1. docker-compose.ymlファイルを作成
  2. モックAWS環境を構築
  3. AWS-CLI実行環境を構築
  4. 動作確認
  5. アプリケーション環境を構築
  6. 3つのコンテナを起動
  7. APIの動作確認

① docker-compose.ymlファイルを作成

初めにdocker-compose.ymlを作成します。

version: "3"
services:
  mavs-api-server:
    image: public.ecr.aws/docker/library/amazoncorretto:17
    container_name: "mavs-api-server"
    tty: true
    environment:
      END_POINT: http://mock-aws-local:4000
    env_file:
        - ./mock-user-pool/output/mock-cognito.properties
    volumes:
      - ./mavs-api-server:/usr/src/app
      - ./mavs-api-server/.aws:/root/.aws
      - ./mock-user-pool/output:/aws/configuration
    working_dir: /usr/src/app
    ports:
      - "8080:8080"
    depends_on:
      - mavs-user-pool
    networks:
      - external
      - default
  mock-user-pool: 
    image: public.ecr.aws/aws-cli/aws-cli
    container_name: "mock-user-pool"
    tty: true
    environment:
      ENDPOINT_URL: http://mock-aws-local:4000
      COGNITO_USER_EMAIL: sample@example.com
      COGNITO_USER_PASSWORD: [適宜、パスワード設定]
    entrypoint: sh -c "chmod +x /mock-user-pool/bash/create-user-pool.sh && /mock-user-pool/bash/create-user-pool.sh && tail -f /dev/null"
    ports:
      - "4010:4010"
    depends_on:
      - mock-aws-local
    volumes:
      - ./mock-user-pool/bash:/mock-user-pool/bash
      - ./mock-user-pool/.aws:/root/.aws
      - ./mock-user-pool/output:/output
    networks:
      - external
      - default
  mock-aws-local:
    image: motoserver/moto:latest
    container_name: "mock-aws-local"
    environment:
      MOTO_PORT: 4000
    ports:
      - "4000:4000"
    networks:
      - external
      - default

今回は3つのサービスを用意します。
各サービスの概要は以下の通りで、それぞれの詳細は後述します。

サービス名Dockerコンテナの概要
mock-aws-localモックAWS環境
mock-user-poolAWS-CLI実行環境
mavs-api-serverアプリケーション環境

② モックAWS環境を構築

「motoserver/moto」というDockerイメージを基に、擬似的なAWS環境を構築します。
と言いつつ、モックAWS環境だけであれば上記のdocker-compose.ymlのみで完結します。

「motoserver/moto」とは

疑似的にAWS環境を構築できるPython製のライブラリです。
今回はCognito操作(cognito-idp)で使用しますが、それ以外のAWSのサービスも対応できるそうです。
注意点として、すべての操作をサポートしているわけではございません。
対応している操作はリファレンスをご参照ください。
https://docs.getmoto.org/en/latest/docs/services/cognito-idp.html

③ AWS-CLI実行環境を構築

次にAWS-CLIを実行するための環境を用意します。
cognito-idpコマンドを実行することが目的となります。

初めに、ローカル用のAWS CLIプロファイル設定を行います。
Docker起動の度に aws configure コマンドでプロファイルの設定を行うのは手間ですし、
ローカル環境ということで、プロファイル設定ファイルを用意しちゃいます。

[default]
region = ap-northeast-1
[default]
aws_access_key_id = dummy
aws_secret_access_key = dummy

上記2ファイルを「mock-user-pool/.aws」配下に配置してください。
配置さえしてしまえば、後はdocker-compose.ymlのvolumesでDockerコンテナ側と共有するように設定しているため、他は特に手順はありません。

    volumes:
      - ./mock-user-pool/.aws:/root/.aws

ここからがいよいよタイトルにもあるCognitoユーザープールの作成となります。
ユーザープールを作成するにあたり、以下が必要です。

  1. ユーザープールの作成
  2. アプリクライアントの作成
  3. 管理者ユーザーの作成
  4. 管理者ユーザーのパスワード設定
  5. 管理者ユーザー設定の確認

これらの手順を1コマンドずつ手動で叩くのは、これまた手間ですので、上記全てを実行するシェルスクリプトを用意し、docker compose up時に実行するようにしていきます。

まずは以下のシェルスクリプトを「/mock-user-pool/bash」配下に作成してください。
docker-compose.ymlで定義している環境変数をシェルスクリプトで参照しているため、再掲します。

    environment:
      ENDPOINT_URL: http://mock-aws-local:4000
      COGNITO_USER_EMAIL: sample@ma-vericks.com
      COGNITO_USER_PASSWORD: [適宜、パスワード設定]
#!/bin/bash

# プロパティファイルを削除
if [ -f "/output/mock-cognito.properties" ]; then
    rm -f /output/mock-cognito.properties
fi

# ユーザープールの作成
echo execute create-user-pool
USER_POOL_ID=$(
  aws cognito-idp create-user-pool \
    --pool-name MyUserPool \
    --alias-attributes "email" \
    --username-attributes "email" \
    --query UserPool.Id \
    --output text \
    --endpoint-url ${ENDPOINT_URL} \
    --schema \
        Name=email,Required=true
)
echo USER_POOL_ID: $USER_POOL_ID

# アプリクライアントの作成
echo execute create-user-pool-client
set $(
  aws cognito-idp create-user-pool-client \
    --client-name MyUserPoolClient \
    --user-pool-id ${USER_POOL_ID} \
    --generate-secret \
    --query "[UserPoolClient.ClientId, UserPoolClient.ExplicitAuthFlows]" \
    --output text \
    --endpoint-url ${ENDPOINT_URL} \
    --explicit-auth-flows "ADMIN_USER_PASSWORD_AUTH"
)
CLIENT_ID=${1}
EXPLICIT_AUTH_FLOWS=${2}
echo CLIENT_ID: $CLIENT_ID
echo EXPLICIT_AUTH_FLOWS: $EXPLICIT_AUTH_FLOWS

# 管理者ユーザーの作成
echo execute admin-create-user
ADMIN_USER=$(aws cognito-idp admin-create-user \
  --user-pool-id ${USER_POOL_ID} \
  --username ${COGNITO_USER_EMAIL} \
  --message-action SUPPRESS \
  --desired-delivery-mediums EMAIL \
  --endpoint-url ${ENDPOINT_URL} \
  --user-attributes \
    Name=email,Value=${COGNITO_USER_EMAIL} \
    Name=email_verified,Value=true)

# 結果を出力
echo $ADMIN_USER

# 管理者ユーザーのパスワード設定
echo execute admin-set-user-password
ADMIN_SET_USER_PASSWORD=$(aws cognito-idp admin-set-user-password \
  --user-pool-id ${USER_POOL_ID} \
  --username ${COGNITO_USER_EMAIL} \
  --password ${COGNITO_USER_PASSWORD} \
  --permanent \
  --endpoint-url ${ENDPOINT_URL})

# 結果を出力
echo $ADMIN_SET_USER_PASSWORD

# ユーザーリスト、および、各ユーザーステータスの確認
# 出力結果のユーザーステータスに"CONFIRMED"があればOK
echo execute list-users
USER_LIST=$(aws cognito-idp list-users \
  --user-pool-id ${USER_POOL_ID} \
  --endpoint-url ${ENDPOINT_URL})

# 結果を出力
echo $USER_LIST

# プロパティファイルにユーザープールIDとクライアントIDを設定
cat <<EOF > /output/mock-cognito.properties
USER_POOL_ID=$USER_POOL_ID
CLIENT_ID=$CLIENT_ID
EOF

上記シェルスクリプトでのポイントとしては、作成したユーザープールとアプリクライアントのIDを、ファイル(mock-cognito.properties)に書き込んでいるところです。理由は後述します。

上記のシェルスクリプトファイルを実行するコマンドを、docker-compose.ymlのentrypointに指定することで、シェルスクリプトを自前で実行する必要がなくなります。

    entrypoint: sh -c "chmod +x /mock-user-pool/bash/create-user-pool.sh && /mock-user-pool/bash/create-user-pool.sh && tail -f /dev/null"

④ 動作確認

ここで一旦動作確認しようと思います。管理者ユーザーの認証処理をおこないます。
お使いのターミナルを起動し、docker-compose.ymlを配置しているところまで移動します。
その後、以下のコマンドを上から順に実行して認証情報が返ってくることを確認します。

# mock-aws-localサービスの起動
docker-compose up -d mock-aws-local

# mock-user-poolサービスの起動
docker-compose up -d mock-user-pool

# mock-user-poolサービス起動後、コンテナ内に入る
docker-compose exec -it mock-user-pool sh

*** コンテナ内作業 ***
# 以下コマンドで認証処理を行う
aws cognito-idp admin-initiate-auth \
  --user-pool-id ${USER_POOL_ID} \
  --client-id ${CLIENT_ID} \
  --auth-flow ADMIN_USER_PASSWORD_AUTH \
  --auth-parameters "USERNAME=${COGNITO_USER_EMAIL},PASSWORD=${COGNITO_USER_PASSWORD}" \
  --endpoint-url ${ENDPOINT_URL}

※ ${USER_POOL_ID},${CLIENT_ID} は、「./mock-user-pool/output/mock-cognito.properties」に記載されている

認証コマンド実行結果が以下のように返ってきたらOKです!

{
    "AuthenticationResult": {
        "AccessToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImR1bW15IiwidHlwIjoiSldUIn0.eyJpc3MiOiJodHRwczovL2NvZ25pdG8taWRwLmFwLW5vcnRoZWFzdC0xLmFtYXpvbmF3cy5jb20vYXAtbm9ydGhlYXN0LTFfMGNiMThkOGVkNTFmNDAyZWEzNTUzYTBhZjllMmE5ZTciLCJzdWIiOiI4MWQ0NzdjYy1mMDBmLTRlZWYtOGYwYi1jZWRiMmMxNjcyMTYiLCJjbGllbnRfaWQiOiJ5eHJxcHN4cDUwa3RmcjRqbTR6ZmgyYTh6diIsInRva2VuX3VzZSI6ImFjY2VzcyIsImF1dGhfdGltZSI6MTcwMjEzOTQ5MSwiZXhwIjoxNzAyMTQzMDkxLCJ1c2VybmFtZSI6ImNvcnBvcmF0ZTAxQG1hLXZlcmlja3MuY29tIn0.PSauE5DZbXV4DpEgQ2F0K6PdL6bsFmE7GazOEFRh_uNxZMWbXDDT7Uwoa2EfEuNkuk1IUVf7AZg6PkjsGkHVqMDtie1OO4sFhWB1JMgZiVrFNMYhDtQB41qPSfIgakTsjgYWaHi9fG2M9MEEBtTWfkGoNct0yhAnUmsYFF0QiHX9m6ZuX8uiOClKEWVRPFmX9WK910t8ICnpGlStYdQAJ0lWKIhYrx41fiWoZI7l60Cm2RVyti0GtVZT2YHoSS37f9ZocDmdpyUM-yYUDbguKrCvjUqQV64Iv9tRGPFfD6VepiUTijzOy3iSNV62Mg8440RbupDMqxfj2bvOIjfN_Q",
        "ExpiresIn": 3600,
        "TokenType": "Bearer",
        "RefreshToken": "9087d092-9b6b-4b9e-9070-1d57199a3b1f",
        "IdToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImR1bW15IiwidHlwIjoiSldUIn0.eyJpc3MiOiJodHRwczovL2NvZ25pdG8taWRwLmFwLW5vcnRoZWFzdC0xLmFtYXpvbmF3cy5jb20vYXAtbm9ydGhlYXN0LTFfMGNiMThkOGVkNTFmNDAyZWEzNTUzYTBhZjllMmE5ZTciLCJzdWIiOiI4MWQ0NzdjYy1mMDBmLTRlZWYtOGYwYi1jZWRiMmMxNjcyMTYiLCJhdWQiOiJ5eHJxcHN4cDUwa3RmcjRqbTR6ZmgyYTh6diIsInRva2VuX3VzZSI6ImlkIiwiYXV0aF90aW1lIjoxNzAyMTM5NDkxLCJleHAiOjE3MDIxNDMwOTEsImNvZ25pdG86dXNlcm5hbWUiOiJjb3Jwb3JhdGUwMUBtYS12ZXJpY2tzLmNvbSIsImN1c3RvbTpjb3Jwb3JhdGVfaWQiOiJjb3Jwb3JhdGUwMSxjb3Jwb3JhdGUwMiIsImN1c3RvbTppc19sb2dnZWRfaW4iOiJmYWxzZSJ9.FqmOgocAjlI4KSb784XDi6YdWaYfvbRjUzSmPg9dwP0sG9rKN1EThp4aXXKR80lR5RKc9QeSdSPJeagAtUAVi42CBs3ulpOUV6zAGkIcfk6RaFQXVkS5Vxe4wxg_hHd4tscG3od8tS_MVn2rCARXjuH70CnzAjU65e_agN2DhT5v7IacElUy776qhEmp2KXa3anhs39jRPpWKu2i0YyShBkN9EjNjfPII4LKhNZoHdUPxrzKFQYpdg7TDynGroOfV9GuC0RRNN_CfegI54l4VENBO1uoUu8JWs4ExlNm43rP2loUCyOZsk4tmbP67uyec2jo7nZ5hAvMClMXhkSSKw"
    }
}

⑤ アプリケーション環境を構築

管理者ユーザーのサインイン処理を、アプリケーション環境(Java)から実行出来るようにしていきます。テストAPIを作成します。
SpringBootのアプリケーション環境構築は以下で紹介していますので、ご参照ください。
VScode × Docker × SpringBootでホットデプロイ環境構築

build.gradleの設定

Cognito操作(cognito-idp)を行うため、「aws-java-sdk-cognitoidp」というSDKと、
クラスファイルのセッターとゲッターを自動生成するために「lombok」を追加します。

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.1.1'
	id 'io.spring.dependency-management' version '1.1.0'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
	sourceCompatibility = '17'
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-actuator'
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.springframework.boot:spring-boot-starter-validation'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'

	// Cognito
	implementation 'com.amazonaws:aws-java-sdk-cognitoidp:1.12.521'
	// lombok
	implementation 'org.projectlombok:lombok:1.18.24'
	// lombokアノテーションプロセッサの設定
	annotationProcessor 'org.projectlombok:lombok:1.18.24'
}

DTOクラスの実装

Cognito操作(cognito-idp)を実行したレスポンスデータを扱いやすいデータにするために、
DTOクラスを作成します。
「lombok」の「@Data」(Dataアノテーション)をクラスに付与することで、クラスメンバ変数のセッターとゲッターを自動生成してくれるので便利です。

package com.domain.dto;

import lombok.Data;

/**
 * CognitoのListUser用DTO
 */
@Data
public class AmazonCognitoListUserDto {
    /** メールアドレス */
    private String mailAddress;

    /** ユーザーID */
    private String userId;

    /** メールアドレス認証状態 */
    private boolean emailVerified;
}

RequestDataクラスの実装

APIリクエストパラメータの定義クラスを作成します。
フォームバリデーションのアノテーション(「@NotBlank()」や「@Email()」等)で、バリデーションチェックをするようにしています。

package com.auth.controller.form.request;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;

/**
 * Rest APIのリクエスト定義クラス
 */
@Data
public class AuthSignInRequestData {
    /** メールアドレス */
    @NotBlank()
    @Email()
    private String mailAddress;

    /** パスワード */
    @NotBlank()
    private String password;
}

ResponseDataクラスの実装

APIレスポンスデータの定義クラスを作成します。

package com.auth.controller.form.response;

import org.springframework.stereotype.Component;
import lombok.Data;

/**
 * Rest APIのレスポンス定義クラス
 */
@Component
@Data
public class AuthSignInResponseData {
    /** ユーザーID */
    private String userId;

    /** メールアドレス */
    private String mailAddress;

    /** アクセストークン */
    private String accessToken;

    /** リフレッシュトークン */
    private String refreshToken;

    /** IDトークン */
    private String idToken;
}

Repositoryクラスの実装

Cognito操作(cognito-idp)をするリポジトリクラスを作成します。
「@Value」で環境変数を参照できるようにしており、「USER_POOL_ID」「CLIENT_ID」は「mock-cognito.properties」の内容を参照しています。

docker-compose.ymlのenvironment項目で「USER_POOL_ID」「CLIENT_ID」を設定していると、IDを発行する度にenvironment項目に該当のID値を設定して、docker compose upし直さないといけないのが非常に手間だったので、以下のようにしています。
先述したユーザープール作成シェルスクリプトで「mock-cognito.properties」を作成している理由は、上記の手間を削減するためでした!

    environment:
      END_POINT: http://mock-aws-local:4000
    env_file:
        - ./mock-user-pool/output/mock-cognito.properties
package com.domain.repository;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Repository;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.auth.profile.ProfileCredentialsProvider;
import com.amazonaws.client.builder.AwsClientBuilder.EndpointConfiguration;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.cognitoidp.AWSCognitoIdentityProvider;
import com.amazonaws.services.cognitoidp.AWSCognitoIdentityProviderClientBuilder;
import com.amazonaws.services.cognitoidp.model.AdminInitiateAuthRequest;
import com.amazonaws.services.cognitoidp.model.AdminInitiateAuthResult;
import com.amazonaws.services.cognitoidp.model.AttributeType;
import com.amazonaws.services.cognitoidp.model.AuthFlowType;
import com.amazonaws.services.cognitoidp.model.ListUsersRequest;
import com.amazonaws.services.cognitoidp.model.ListUsersResult;
import com.amazonaws.services.cognitoidp.model.UserType;
import com.domain.dto.AmazonCognitoListUserDto;

/**
 * Cognitoリポジトリ
 */
@Repository
public class AmazonCognitoRepository {
    /** CognitoIdentityProviderClient */
    private AWSCognitoIdentityProvider client;

    /** エンドポイント(環境変数) */
    @Value("${END_POINT}")
    private String endPoint;

    /** ユーザープールID(環境変数) */
    @Value("${USER_POOL_ID}")
    private String userPoolId;

    /** クライアントID(環境変数) */
    @Value("${CLIENT_ID}")
    private String clientId;

    /**
     * CognitoIdentityProviderClientを作成
     */
    public void createCognitoIdentityProviderClient() {
        // CredentialsProviderを取得
        AWSCredentialsProvider credentialsProvider = getCredentialsProvider();

        // CognitoIdentityProviderClientを取得
        client = AWSCognitoIdentityProviderClientBuilder.standard()
                .withCredentials(credentialsProvider)
                .withEndpointConfiguration(
                        new EndpointConfiguration(endPoint, Regions.AP_NORTHEAST_1.getName()))
                .build();
    }

    /**
     * ユーザー認証情報を取得
     * 
     * @param userId ユーザーID(UUID)
     * @param password パスワード
     * @return ユーザー認証情報
     */
    public AdminInitiateAuthResult adminInitiateAuth(String userId, String password) {
        Map<String, String> authParam = new HashMap<>();
        authParam.put("USERNAME", userId);
        authParam.put("PASSWORD", password);

        AdminInitiateAuthRequest request = new AdminInitiateAuthRequest();
        request.withAuthFlow(AuthFlowType.ADMIN_USER_PASSWORD_AUTH).withUserPoolId(userPoolId)
                .withClientId(clientId).withAuthParameters(authParam);

        // ユーザー認証情報を取得
        AdminInitiateAuthResult authResult = client.adminInitiateAuth(request);
        return authResult;
    }

    /**
     * メールアドレスをキーにユーザー情報を取得
     * 
     * @param email メールアドレス
     * @return ユーザー情報
     */
    public AmazonCognitoListUserDto getListUsersFilter(String email) {
        // メールアドレスでフィルタリング
        String filter = "email = \"" + email + "\"";
        ListUsersRequest listUsersRequest = new ListUsersRequest();
        listUsersRequest.setUserPoolId(userPoolId);
        listUsersRequest.setFilter(filter);

        // ユーザー情報を取得
        ListUsersResult listUsersResult = client.listUsers(listUsersRequest);
        // 取得した情報をDTO整形
        List<AmazonCognitoListUserDto> amazonCognitoListUserDto =
                userDataFormatting(listUsersResult.getUsers());

        if (amazonCognitoListUserDto.size() == 0) {
            return null;
        }
        return amazonCognitoListUserDto.get(0);
    }

    /**
     * 認証情報を取得
     * 
     * @return 認証情報
     */
    private AWSCredentialsProvider getCredentialsProvider() {
        // デフォルトProfileから認証情報を取得
        AWSCredentialsProvider credentialsProvider = new ProfileCredentialsProvider("default");
        System.out.println("Your AWS Access Key ID is "
                + credentialsProvider.getCredentials().getAWSAccessKeyId());
        System.out.println("Your Secret Access Key is "
                + credentialsProvider.getCredentials().getAWSSecretKey());
        return credentialsProvider;
    }

    /**
     * ユーザーリストをDTO整形
     * 
     * @param users ユーザーリスト
     * @return 整形後ユーザーリスト
     */
    private List<AmazonCognitoListUserDto> userDataFormatting(List<UserType> users) {
        List<AmazonCognitoListUserDto> usersEntity = new ArrayList<AmazonCognitoListUserDto>();

        for (UserType user : users) {
            AmazonCognitoListUserDto userEntity = new AmazonCognitoListUserDto();

            for (AttributeType item : user.getAttributes()) {
                switch (item.getName()) {
                    // ユーザーID
                    case "sub" -> userEntity.setUserId(item.getValue());
                    // メールアドレス
                    case "email" -> userEntity.setMailAddress(item.getValue());
                    // メールアドレス認証状態
                    // StringをBooleanに変換
                    case "email_verified" -> userEntity
                            .setEmailVerified(Boolean.valueOf(item.getValue().toLowerCase()));
                    default -> System.out.println("想定外の値です");
                }
                System.out.println(item.getName() + ": " + item.getValue());
            }
            usersEntity.add(userEntity);
        }
        return usersEntity;
    }
}

AWSプロファイル設定

上記の「③ AWS-CLI実行環境を構築」と同様にCognito操作を行う上でプロファイルが必要となります。
下記2ファイルを「/mavs-api-server/.aws」に配置してください。

[default]
region = ap-northeast-1
[default]
aws_access_key_id = dummy
aws_secret_access_key = dummy

上記を配置していただければ、docker-compose.ymlのvolumesでDockerコンテナ側と共有するように設定しているため、他は特に手順はありません。

    volumes:
      - ./mavs-api-server/.aws:/root/.aws

Serviceクラスの実装

サービスクラスを作成します。
AWSプロファイル取得 → ユーザーID(UUID)を取得 → 認証情報取得 → データ整形 を実施します。

package com.domain.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.amazonaws.services.cognitoidp.model.AdminInitiateAuthResult;
import com.auth.controller.form.request.AuthSignInRequestData;
import com.auth.controller.form.response.AuthSignInResponseData;
import com.domain.dto.AmazonCognitoListUserDto;
import com.domain.repository.AmazonCognitoRepository;

/**
 * 認証ビジネスロジック用サービス
 */
@Service
public class AuthService {
    /** Cognitoリポジトリ */
    @Autowired
    AmazonCognitoRepository repository;

    /**
     * サインインを行う
     * 
     * @param requestBody リクエストパラメータ
     * @return 整形後レスポンスデータ
     */
    public AuthSignInResponseData signIn(AuthSignInRequestData requestBody) {
        AuthSignInResponseData response = new AuthSignInResponseData();

        try {
            // 入力メールアドレスに紐づくユーザー情報をCognitoから取得
            repository.createCognitoIdentityProviderClient();
            AmazonCognitoListUserDto signInUser =
                    repository.getListUsersFilter(requestBody.getMailAddress());

            // 認証情報取得
            AdminInitiateAuthResult authResult =
                    repository.adminInitiateAuth(signInUser.getUserId(), requestBody.getPassword());

            // レスポンスDTOにデータ整形
            response.setUserId(signInUser.getUserId());
            response.setMailAddress(requestBody.getMailAddress());
            response.setAccessToken(authResult.getAuthenticationResult().getAccessToken());
            response.setRefreshToken(authResult.getAuthenticationResult().getRefreshToken());
            response.setIdToken(authResult.getAuthenticationResult().getIdToken());
        } catch (Exception e) {
            e.printStackTrace();
        }
        return response;
    }
}

Controllerクラスの実装

最後にコントローラークラスを作成します。サービスクラスのメソッドを呼びます。

package com.auth.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.auth.controller.form.request.AuthSignInRequestData;
import com.auth.controller.form.response.AuthSignInResponseData;
import com.domain.service.AuthService;

/**
 * Rest API定義クラス
 */
@RestController
@RequestMapping("/api/auth")
public class AuthController {
    @Autowired
    private AuthService authService;

    /**
     * サインイン処理
     * 
     * @param AuthSignInRequestData サインイン情報
     * @return ユーザー認証情報
     */
    @PostMapping("/sign-in")
    public ResponseEntity<AuthSignInResponseData> signIn(
            @RequestBody @Validated AuthSignInRequestData requestBody) throws Exception {
        // サインイン処理を実行
        AuthSignInResponseData response = authService.signIn(requestBody);

        // レスポンス返却
        HttpHeaders headers = new HttpHeaders();
        return ResponseEntity.ok().headers(headers).body(response);
    }
}

⑥ 3つのコンテナを起動

①「mock-aws-local」、②「mock-user-pool」、③「mavs-api-server」をそれぞれ起動する際に、①から順番に起動しないといけないのが注意です。

  • 「mock-aws-local」より「mock-user-pool」を先に起動した場合
    → エンドポイント「http://mock-aws-local:4000」が存在しないというエラーになる
  • 「mock-user-pool」より「mavs-api-server」を先に起動した場合
    → 「/output/mock-cognito.properties」が作成される前に、環境変数の「USER_POOL_ID」「CLIENT_ID」を読みにいこうとして存在しなくてエラー

それを意識して毎回手動で実行するのは手間ですので、以下のようなシェルスクリプトを作成し実行することで手間を削減できます。
シェルスクリプトは、docker-compose.yml が配置されている場所と同階層に作成してください。

#!/bin/bash

# mock-aws-localサービスの起動
docker-compose up -d mock-aws-local

# mock-aws-localサービスが起動するまで待機
while ! docker-compose exec mock-aws-local curl -s http://localhost:4000 > /dev/null; do
    echo "Waiting for mock-aws-local to start..."
    sleep 1
done

# バックグラウンドで mock-aws-local のログ表示を開始
docker-compose logs -f mock-aws-local &

# mock-user-poolサービスの起動
docker-compose up -d mock-user-pool

# mock-user-poolサービスが起動する(プロパティファイルが配置される)まで待機
while [ ! -f "./mock-user-pool/output/mock-cognito.properties" ]; do
    echo "Waiting for mock-user-pool to create mock-cognito.properties..."
    sleep 1
done

# バックグラウンドで mock-user-pool のログ表示を開始
docker-compose logs -f mock-user-pool &

# mavs-api-serverサービスの起動
docker-compose up -d mavs-api-server

# バックグラウンドで mavs-api-server のログ表示を開始
docker-compose logs -f mavs-api-server

その後、以下のコマンドを実行すると①から順に起動されます。

# コンテナ群を起動するシェルスクリプトを実行する
sh start_services.sh

⑦ APIの動作確認

作成したAPIを実行してみます。以下を設定し実行してみましょう!

APIパス: http://localhost:8080/api/auth/sign-in
APIメソッド: POST
リクエストボディ:
{
  "mailAddress": "sample@example.com",
  "password": "[適宜、パスワード設定]"
}

サインイン処理を実行して返ってきた認証情報がローカルCognitoから返ってきてました!

{
  "userId": "f2b25285-5205-4c84-9eb0-5afe21678726",
  "mailAddress": "sample@example.com",
  "accessToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImR1bW15IiwidHlwIjoiSldUIn0.eyJpc3MiOiJodHRwczovL2NvZ25pdG8taWRwLmFwLW5vcnRoZWFzdC0xLmFtYXpvbmF3cy5jb20vYXAtbm9ydGhlYXN0LTFfMWM5OThlNWE0NTBjNGMxMjllMTUyYTY2MjFhOTJiZTkiLCJzdWIiOiJmMmIyNTI4NS01MjA1LTRjODQtOWViMC01YWZlMjE2Nzg3MjYiLCJjbGllbnRfaWQiOiJ0bnVyM251a3F2ZmI1NXRycGNjbW1jamJvMyIsInRva2VuX3VzZSI6ImFjY2VzcyIsImF1dGhfdGltZSI6MTcwMjE3Nzc0NSwiZXhwIjoxNzAyMTgxMzQ1LCJ1c2VybmFtZSI6ImYyYjI1Mjg1LTUyMDUtNGM4NC05ZWIwLTVhZmUyMTY3ODcyNiJ9.J-BIYHtBlpTniFQ1veOtl2t5sDAGT19joBdrE7NN-BNaCNwclribJ2WWlPj5wi-jhvSBIOVfy0gLoXlKZyXxqZ7ifNj2pqII1rR2uLKP8suqzB0B5hZ7HVnhF8GS73CsRrKxr2Hnywf9Ws8pyZ7auqY0f9Dnvv7wdyMDQmI92cD8UY18Wug9awYZLVmROiHkDDv1LmMBrN7_48NGKqJy2n3b1pk8vN_2VhG_-LKKFhm-49tEZPAK_7SiYgAWQ3b4BO_MZ8gNYphSGN43LIaNsOYwWIHFcYkVgwlCI3BNuz-wHxjnpXkreS5-zEcnFpkAVwDbs9gmgHKzFNHYkBwBIg",
  "refreshToken": "2be5e337-f91f-4ac4-a577-0c705cf9b9ac",
  "idToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImR1bW15IiwidHlwIjoiSldUIn0.eyJpc3MiOiJodHRwczovL2NvZ25pdG8taWRwLmFwLW5vcnRoZWFzdC0xLmFtYXpvbmF3cy5jb20vYXAtbm9ydGhlYXN0LTFfMWM5OThlNWE0NTBjNGMxMjllMTUyYTY2MjFhOTJiZTkiLCJzdWIiOiJmMmIyNTI4NS01MjA1LTRjODQtOWViMC01YWZlMjE2Nzg3MjYiLCJhdWQiOiJ0bnVyM251a3F2ZmI1NXRycGNjbW1jamJvMyIsInRva2VuX3VzZSI6ImlkIiwiYXV0aF90aW1lIjoxNzAyMTc3NzQ1LCJleHAiOjE3MDIxODEzNDUsImNvZ25pdG86dXNlcm5hbWUiOiJmMmIyNTI4NS01MjA1LTRjODQtOWViMC01YWZlMjE2Nzg3MjYiLCJjdXN0b206Y29ycG9yYXRlX2lkIjoiY29ycG9yYXRlMDEsY29ycG9yYXRlMDIiLCJjdXN0b206aXNfbG9nZ2VkX2luIjoiZmFsc2UifQ.PV-w-YNfwpaK90VWZdLxBMrCeq2X72v6DsXFw7kGQAzS-sqNRXkDgQglqzYlhZcDm9U6X6UkkCpN7HRy4X6jLNrO-70unyHUc9poEWixWwNpBQO_oxCfaqQqXKnIlOOtFnpcKlDQfj0Kw7Co2HVBoHVqH5QF8Iqbkd34-55ycBBrbPqDhyYOTdlZN7_yo0Yi6npgR9BDiR4_sgzEbX2lFe0iqZU_SvC0aIXzgveJT4TWWKIRcwolV8F6KwgInCc6NWvGqYFt_Lm-R8KQaw6URGsV_mxZyI7pY20RBxUytVaGyePrO0Ia9KP0AXVZSa8LXaoM4gnukISSW8Ex1ZKE9w"
}

以上です、お疲れ様でした!

まとめ

Docker環境内にCognitoユーザープールを構築する方法の紹介でした。

この方法で自分専用のCognitoユーザープールを作成出来ますし、ユーザープールの作り直しもdocker compose upし直せばいいので、簡単にローカル環境でCognitoを取り扱うことが出来ます。

もし、Cognitoを使用する機会がありDockerをお使いでしたら、上記手順でローカル環境Cognitoを構築することを視野に入れてみていただけたら幸いです!

  • この記事を書いた人
  • 最新の記事

Nakahori Shota

2020年8月にマブスに転職。経験的にはバックエンドが長いが、フロントエンドも魅力を感じて勉強中。 入社初日に激辛麻辣カリー湯麺の辛さ3倍を食べ、マブスの激辛王の称号を得る。(食べきりはしたが、翌日お腹がヤバかった)