AWS CDKでCognitoを使ったREST APIを作ってみる

Cognitoを認証基盤としたREST APIをCDKで作成する機会があったので、
個人の記録として整理しておきたいと思います。
実装する構成は以下です。

CDK準備

CDKのバージョンはv2です。環境としてはcdk bootstrap済みであることを想定しています。

$ mkdir cognito && cd cognito
$ cdk init app --language typescript


AWS CDK (Typescript)

/lib/cognito-stack.ts

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as cognito from 'aws-cdk-lib/aws-cognito';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as logs from 'aws-cdk-lib/aws-logs';

export class CognitoStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Cognitoユーザープールの作成
    const userPool = new cognito.UserPool(this, 'UserPool', {
      selfSignUpEnabled: true, // ユーザーが自己登録できるように設定
      userVerification: {
        emailSubject: 'Verify your email for our app!', // メールの検証メッセージ
        emailBody: 'Thanks for signing up to our app! Your verification code is {####}', // メール本文
        emailStyle: cognito.VerificationEmailStyle.CODE, // 検証コードを含むメール
      },
      signInAliases: {
        email: true, // メールアドレスをサインインに使用
      },
    });

    // Cognitoユーザープールのクライアントを作成
    const userPoolClient = userPool.addClient('UserPoolClient', {
      userPoolClientName: 'sample_client', //アプリケーションクライアント名
      authFlows: {
        adminUserPassword: true,
        userPassword: true,
      },
    });
    
    // カスタムドメインを追加
    userPool.addDomain('UserPoolDomain', {
      cognitoDomain: {
        domainPrefix: 'testsampledomain',
      }
    });

    // DynamoDBテーブルの作成
    const table = new dynamodb.Table(this, 'DynamoDB', {
      tableName: 'sample_table', // テーブル名
      partitionKey: {
        name: 'id',
        type: dynamodb.AttributeType.NUMBER,
      },
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });  

    // APIGW用のロググループ
    const accessLogGroup = new logs.LogGroup(this, 'AccessLog',{
      logGroupName: 'APIGateway-AccessLog',
      retention: logs.RetentionDays.ONE_DAY,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    // API Gatewayの作成
    const api = new apigateway.RestApi(this, 'ApiGateway', {
      restApiName: 'sample_API',
      deployOptions: {
        stageName: 'prod', // ステージ名を設定
        tracingEnabled: true, // X-Ray
        dataTraceEnabled: true,
        loggingLevel: apigateway.MethodLoggingLevel.ERROR,
        accessLogDestination: new apigateway.LogGroupLogDestination(accessLogGroup),
        accessLogFormat: apigateway.AccessLogFormat.clf()
      },
    });

    // Lambda関数の作成
    const lambdaFunction = new lambda.Function(this, 'Lambda', {
      runtime: lambda.Runtime.PYTHON_3_9,
      handler: 'sample.handler', // Lambda関数のエントリーポイント
      code: lambda.Code.fromAsset('lambda'), // Lambda関数のコードを 'lambda' フォルダに配置
      tracing: lambda.Tracing.ACTIVE,
        timeout: cdk.Duration.seconds(30),
        environment: {
          TABLE_NAME: table.tableName,
        },
    });

    // Lambda関数に必要なIAMロールを作成
    const lambdaRole = new iam.Role(this, 'LambdaRole', {
      assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
    });

     // DynamoDBテーブルへのアクセスポリシーをLambda関数に追加
     table.grantReadData(lambdaFunction);


    // Lambda関数にAPI Gatewayの呼び出しを許可
    lambdaFunction.grantInvoke(new iam.ServicePrincipal('apigateway.amazonaws.com'));

    // API GatewayのルートリソースにLambda統合を作成
    const integration = new apigateway.LambdaIntegration(lambdaFunction);

    // Cognitoユーザープール用のAuthorizerを作成
    const authorizer = new apigateway.CognitoUserPoolsAuthorizer(this, 'CognitoAuthorizer', {
      cognitoUserPools: [userPool],
    });

    // API GatewayメソッドにCognito認証を適用
    const method = api.root.addMethod('GET', integration, {
      authorizationType: apigateway.AuthorizationType.COGNITO,
      authorizer,
    });    
  }
}


Lambdaコード(Python)

/lambda/sample.py
Pythonで用意しました。lambdaフォルダを作成し、sample.pyとして格納します。
動作としては、API Gatewayアクセスで発火し、DynamoDBテーブルから「id」と「userName」を取得してきます。

import os
import boto3

def handler(event, context):
    # DynamoDBテーブル名を環境変数から取得
    table_name = os.environ['TABLE_NAME']
    
    # クエリパラメータからidを取得
    id = event['queryStringParameters']['id']
    
    # DynamoDBクライアントの初期化
    dynamodb = boto3.resource('dynamodb')
    
    # DynamoDBテーブルの取得
    table = dynamodb.Table(table_name)
    
    # テーブルから指定されたidのデータを取得
    response = table.get_item(Key={'id': int(id)})
    
    # レスポンスデータの作成
    if 'Item' in response:
        item = response['Item']
        user_name = item['userName']
        response_data = {
            'statusCode': 200,
            'body': f'ID: {id}, User Name: {user_name}'
        }
    else:
        response_data = {
            'statusCode': 404,
            'body': f'ID: {id} not found'
        }
    
    return response_data

CDKデプロイ

$ cdk synth
$ cdk deploy

これでインフラが出来上がりました。

DynamoDBにデータ登録

DynamoDBにデータを登録します。
ここからは、マネージメントコンソールでポチポチ作業していきます。
テーブルにはidとuserNameを登録します。以下がイメージです。

1.AWS コンソールにログインし、DynamoDBを選択します。
2.左メニューのテーブル → 「項目を探索」を選択します。
3.テーブル一覧から「sample_table」を選択します。
4.「項目を作成」をクリックします。
5.設定値を入力します。
 「id」:数字
 「userName」:文字列
6.「項目を作成」をクリックします。

7.項番の4~6を何回か繰り返し、適宜項目を追加します。
以上でDynamoDBへのデータ登録は完了です。


Cognitoユーザプールにユーザ作成

出来上がったCognitoユーザプール内にユーザを作成します。
トークンを取得するためのユーザとなります。
1.AWS コンソールにログインし、Cognitoを選択します。
2.左メニューのユーザープールをクリックします。
3.ユーザープール一覧からCDKで作成したユーザープールをクリックします。
4.「アプリケーションの統合」タブをクリックします。

5.最下段までスクロールし、アプリケーションクライアント名をクリックします。
6.「ホストされたUIを表示」をクリックします。

7.Sign in画面が別タブで表示されますので、下部の「Sign up」をクリックします。

8.Sign up画面に遷移します。メールアドレスとパスワードを入力しユーザ登録します。メールアドレスは受信可能なアドレスにしてください。後続手順に必要な認証コードがメールで届きます。

以下のようなメールが届きますので認証コードをコピーします。

9.認証コードの登録画面に遷移しますので、受信したメールに記載されているコードをペーストするか入力します。
「Confirm account」をクリックすれば登録完了です。

10.ユーザが無事登録できたかをCognito画面で確認します。
Cognitoの画面に戻り、「ユーザー」タブをクリックします。
11.ユーザー一覧画面にユーザー名が登録されているか確認します。
確認ステータス欄が「確認済み」となっていれば完了です。

APIへアクセス

ここからはAPIへアクセスするための手順となります。
API Gatewayにアクセスするためには、CognitoのIdトークンが必要です。
そのためまずは、CognitoにアクセスしIdトークンを取得します。
AWS CLIコマンドを使用しますので、AWS CLIが実行できる環境が必要になります。

Idトークンの取得
トークンを取得するには、以下4つの情報が必要です。
・user-pool-id (ユーザープールID)
・client-id (クライアントID)
・USERNAME (ユーザー名)
・PASSWORD (パスワード)

・user-pool-id (ユーザープールID)
 ユーザープール画面で確認ができます。
・client-id (クライアントID)
 アプリケーションの統合タブ → 最下段 → クライアントIDが確認できます
・USERNAME (ユーザー名)
 ユーザータブ → ユーザー一覧画面から確認ができます。
・PASSWORD (パスワード)
 ユーザー登録時に設定したパスワードです。
4つの情報が全てそろったら、以下のAWS CLIコマンドを実行します。

aws cognito-idp admin-initiate-auth \
  --user-pool-id XXXXXXXXXXXX \
  --client-id XXXXXXXXXXXXXXXXXXXXXX \
  --auth-flow "ADMIN_USER_PASSWORD_AUTH" \
  --auth-parameters \
    USERNAME=XXXXXXXXXXXXXXXXXXXXXX,PASSWORD=XXXXXXXXX

コマンドが成功すると以下のように応答が返ってきます。
返ってくるトークンは3つ(AccessToken、RefreshToken、IdToken)ですが、使うのはIdTokenなので、IdTokenをコピーします。

{
    "ChallengeParameters": {},
    "AuthenticationResult": {
        "AccessToken": "XXXXXXXXXXXXXXXXXXXXXXXXX",
        "ExpiresIn": 3600,
        "TokenType": "Bearer",
        "RefreshToken": "XXXXXXXXXXXXXXXXXXXXXXXXX",
        "IdToken": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
    }
}

これでAPI Gatewayにアクセスするためのトークンが取得できました。

curlを実行する場合
curlコマンドを利用してAPI Gatewayにアクセスします。
以下のcurlコマンドでid:1のデータを取得します。<Idトークン>の箇所に、取得したIDトークンをペーストします。

$ curl "https://XXXX.execute-api.ap-northeast-1.amazonaws.com/prod/?id=1" \
--header 'Authorization: <Idトークン>'

以下が実行結果です。
無事取得できましたね。

ID: 1, User Name: sato

ちなみにトークンを入れずにアクセスを行うと・・・
未認証ということで怒られてしまいます。当然ですが。

 $ curl "https://XXXX.execute-api.ap-northeast-1.amazonaws.com/prod/?id=1"

{"message":"Unauthorized"}


ブラウザで実行する場合
ブラウザを使ってAPIGatewayにアクセスするには、ModHeaderを使うのが楽です。
取得したトークンをブラウザ上で埋め込み、API Gatewayにアクセスしてみます。
※筆者はchromeの拡張機能として利用しています。
Name:Authorization
Value:取得したIdトークン

API Gatewayにアクセス
ModHeaderにトークンをセットできたら、API GatewayのURL(GETメソッド)宛にアクセスを行います。以下はid:1のデータを取得します。

 https://XXXX.execute-api.ap-northeast-1.amazonaws.com/prod/?id=1

無事取得できましたね。
末尾のid番号を変えると、別ID情報を取得することができます。

手順としては以上となります。

まとめ

ここまで読んでいただきありがとうございます。
今回はCDKを使ってCognitoを認証基盤としたREST APIを実装してみました。少しでも誰かの役に立てれば幸いです。


いいなと思ったら応援しよう!