API GatewayのオーソライザーにCognitoを使用してみた
事業本部Delivery部のアベシです。
こちらの記事では、API GatewayにCognitoのオーソライザーによる認証認可機能を導入する方法について紹介します。
構築にはCDKを使用しました。
Cognito ユーザープールをAPI Gatewayのオーソライザーとする場合の認証の仕組み
① クライアントがCognitoユーザープールにユーザー名とパスワードを渡して認証のリクエストする。
② 認証されたらCognitoユーザープールがIDトークンをクライアントに返す。
③ クライアントがAPIを叩く。その際にIDトークンをヘッダーに乗せてAPI Gatewayに渡す。
④ API GatewayのオーソライザーのCognitoがトークンを検証する
⑤ 検証が成功したらAPIの利用を許可する(認可の部分)
⑥ 後続のLambda関数が実行される
実行環境
以下の環境で構築と動作確認しています。
項目名 | バージョン |
---|---|
mac OS | Ventura 13.2 |
npm | 9.6.2 |
AWS CDK | 2.66.1 |
CDKコード
import { Stack, StackProps, aws_apigateway, aws_lambda_nodejs, Duration, aws_cognito, RemovalPolicy, } from 'aws-cdk-lib'; import { Runtime } from 'aws-cdk-lib/aws-lambda'; import { AccountRecovery } from 'aws-cdk-lib/aws-cognito'; import { Construct } from 'constructs'; export class CdkSampleCognitoAuthApiStack extends Stack { constructor(scope: Construct, id: string, props: StackProps) { super(scope, id, props); // Lambda関数の作成 const nameHelloWorldFunc = "Hello_world_function" const helloWorldFunc = new aws_lambda_nodejs.NodejsFunction( this, nameHelloWorldFunc, { runtime: Runtime.NODEJS_18_X, functionName: nameHelloWorldFunc, entry: 'src/lambda/handlers/hello-world-func.ts', }, ); // API Gatewayの作成 const nameRestApi ="Rest API with Lambda auth"; const restApi = new aws_apigateway.RestApi(this, nameRestApi, { restApiName: nameRestApi, deployOptions: { stageName: 'v1', }, }); // Cognitoユーザープールの作成 const userPoolName = 'userPool'; const cognitoUserPool = new aws_cognito.UserPool(this, 'userPoolName', { userPoolName: userPoolName, selfSignUpEnabled: true, accountRecovery: AccountRecovery.EMAIL_ONLY, standardAttributes: { email: { required: true,// サインアップ時にemailアドレスを必須にする mutable: true,//true場合emailアドレスの変更が可能 }, }, signInAliases: { email: true,username: true },//email:trueとするとユーザー名にemailアドレスが使える autoVerify: { email: true },//autoVerifyを記述しない場合、emailアドレスの検証が必要 removalPolicy: RemovalPolicy.DESTROY,//DESTROYの場合はスタックを削除するとuserPoolも削除される。本番環境はRetainを推奨 }); // Cognitoのユーザープールにクライアントを追加 const userPoolClientName = 'userPoolClient'; cognitoUserPool.addClient(userPoolClientName, { userPoolClientName: userPoolClientName, authFlows: { adminUserPassword: true},//adminUserPasswordがfalse場合、ユーザー名とパスワードでトークンの取得ができない }); // CognitoのAuthorizerの作成 const cognitoAuthorizer = new aws_apigateway.CognitoUserPoolsAuthorizer( this, 'cognitoAuthorizer', { cognitoUserPools: [cognitoUserPool], }, ); // API Gatewayのリソースを作成 const restApiTasks = restApi.root.addResource('hello_world'); // API GatewayのリソースにLambdaを紐付ける。Cognito Authorizerを指定する。 restApiTasks.addMethod( 'GET', new aws_apigateway.LambdaIntegration(helloWorldFunc), { authorizer: cognitoAuthorizer, // CognitoUserPoolsAuthorizerをオーソライザーに指定 }, ); } }
コードの解説
Lambdaのビルド方法定義
API Gatewayの後続のLambda関数のビルド方法を定義します。 NodejsFunctionクラスを使用して定義します。 ランタイムはNode.js18を指定しています。 関数はHello Worldを返すだけの内容となってます。
API Gateway
RestApiクラスを使用してAPI Gatewayを作成しています。 Lambda proxy統合を使用して、Lambda関数のレスポンスをそのまま返すようにしています。
Cognitoユーザープールの作成
UserPoolクラスを使用してCognitoユーザープールを作成しています。
サインアップ時にemailアドレスを必須にするため、standardAttributes
にemail:true
を追加しています。
autoVerify
にemailを追加することで、emailアドレスの検証が不要になります。
signInAliases
にemailを追加することで、ユーザー名にemailアドレスを使用できます。プロパティを後から変更できません。変更する場合はユーザープールを削除して再作成する必要があります。
RemovalPolicy
にDESTROY
を指定することで、スタックを削除するとユーザープールも削除されます。本番環境ではRETAIN
を推奨します。
const userPoolName = 'userPool'; const cognitoUserPool = new aws_cognito.UserPool(this, 'userPoolName', { userPoolName: userPoolName, selfSignUpEnabled: true, accountRecovery: AccountRecovery.EMAIL_ONLY, standardAttributes: { email: { required: true, mutable: true, }, }, signInAliases: { email: true,username: true }, autoVerify: { email: true }, removalPolicy: RemovalPolicy.DESTROY, });
ユーザープールにクライアントを追加
addClient
メソッドを使用してユーザープールにクライアントを追加しています。
authFlows
にadminUserPassword:true
を追加することで、管理者権限のユーザーがユーザー名とパスワードでトークンを取得できるようになります。この指定をしない場合、マネジメントコンソール上で確認すると認証フローの項目のALLOW_ADMIN_USER_PASSWORD_AUTH
が有効ならずトークンが取得できません。
cognitoUserPool.addClient(userPoolClientName, { userPoolClientName: userPoolClientName, authFlows: { adminUserPassword: true}, });
Cognito Authorizerの作成
CognitoUserPoolsAuthorizerクラスを使用してCognitoのオーソライザーを作成しています。
cognitoUserPoolsプロパティに先程作成したCognitoユーザープールを指定します。
const cognitoAuthorizer = new aws_apigateway.CognitoUserPoolsAuthorizer( this, 'cognitoAuthorizer', { cognitoUserPools: [cognitoUserPool], }, );
API GatewayのリソースにLambdaを紐付ける
authorizerプロパティに先程作成したCognito Authorizerを指定します。
Lambda統合プロキシの指定にはLambdaIntegration
を使用します。先程定義したLambda関数を指定します。
restApiTasks.addMethod( 'GET', new aws_apigateway.LambdaIntegration(helloWorldFunc), { authorizer: cognitoAuthorizer, }, );
デプロイ
※ これ移行の操作は全てAWS CLIを使用してコマンドラインから操作します
AWS CLIを使用して以下のコマンドでデプロイします。
--require-approval never '*'
オプションを指定することで、スタックの変更内容を確認せずにデプロイできます。
cdk deploy --require-approval never '*'
デプロイが完了したらAPI GatewayのURLが表示されますので控えておきます。
Outputs: CdkSampleCognitoAuthApiStack.RestAPIwithLambdaauthEndpoint******** = https://********.execute-api.ap-northeast-1.amazonaws.com/v1/
ユーザーの登録
Cognitoのユーザープールにユーザーを登録します。
コマンドは以下の通りです。
aws cognito-idp admin-create-user \ --user-pool-id <ユーザープールID>\ --username test-user \ --user-attributes \ Name=email,Value= [email protected] \ Name=email_verified,Value=True \
ユーザープールIDは、マネジメントコンソールのユーザープールの詳細画面に表示されています。
正常に登録できるとメールアドレスに初期パスワード
が届きます。
トークン取得
以下のコマンドでトークンを取得できます。
aws cognito-idp admin-initiate-auth \ --user-pool-id <ユーザープールID> \ --client-id <クライアントID> \ --auth-flow "ADMIN_USER_PASSWORD_AUTH" \ --auth-parameters \ USERNAME=test-user,PASSWORD=<メールアドレスに届いた初期パスワード>
クライアントIDは以下の手順で調べます。
- マネジメントコンソールの
アプリケーション統合
のタブのメニューを開く。
-
最下段の
アプリケーションクライアントのリスト
に作成したクライアントとクライアントIDが表示されています。
初回取得の場合はレスポンスが以下のように、"ChallengeName": "NEW_PASSWORD_REQUIRED"
とSession
が返ってきます。
{ "ChallengeName": "NEW_PASSWORD_REQUIRED", "Session": "AYABeDzc0EFKMAO8uNsFTk-rOk0AHQABAAdTZXJ2aWNlABBDb2duaXRvVXNlclBvb2xzAAEAB2F3cy1rbXMAUGFybjphd3M6a21zOmFwLW5vcnRoZWFzdC0xOjM0NjM3NzU0NDkyNzprZXkvZDNhY2NlYmQtNTdhOC00NWE0LTk1ZmEtYzc2YzY5ZDIwYTRkALgBAgEAePZZnC4WFmlF02bVD7JImpVw_X4vigfhMFizLpHK-pJkAd07W2aSvVwKZOONB1n063MAAAB-MHwGCSqGSIb3DQEHBqBvMG0CAQAwaAYJKoZIhvcNAQcBMB4GCWCGSAFlAwQBLjARBAz8Y-zK7GVW7VIoJ-wCARCAO5dbw48VzY_kD7kI0W8DrPcplKHtSNQlWzsIiZLBsjZ4OU9f6x1R9ZB7A2JVdYT3qiBp78TLV8lYR9HoAgAAAAAMAAAQAAAAAAAAAAAAAAAAACP4NTSBDDCft6ULpIUiAm3_____AAAAAQAAAAAAAAAAAAAAAQAAALiftguZhr2yAe1r-MR9pmvHY5y7ujPAJ8oQXFBX0jNp_4lITaXUyz0GVSX3Y1WYiV_5FAylxTyc-P5fsW6Fv7E7yU5Iz1BS9YXUS3rE8DvhpRABpYZGPNCeQ5HTWXs3obgrMtJA2tWJWWnv4r55_l6sgo7WRTBFLouhgP_PAcxKoPAzY1As40NnPxMB6RIkzXX_8Gx1Wp5b8JTbLUkwz5V9xp20j8k3d5aAtL1hGPhIYsUkTWHah-l1tKvNX79V188zTD5JbndAfg", "ChallengeParameters": { "USER_ID_FOR_SRP": "test-user", "requiredAttributes": "[]", "userAttributes": "{\"email_verified\":\"True\",\"email\":\"[email protected]\"}" } }
初期パスワード変更
以下のコマンドで初期パスワードを変更します。
session
には、先程のadmin-initiate-auth
のレスポンスのSession
を指定します。
デフォルトのパスワードポリシーは、8文字以上且つ1文字以上の英大文字と数字と記号を含む必要があります。
aws cognito-idp admin-respond-to-auth-challenge \ --user-pool-id <ユーザープールID> \ --client-id <クライアントID> \ --challenge-name NEW_PASSWORD_REQUIRED \ --challenge-responses NEW_PASSWORD='Hogefugapiy0!',USERNAME=test-user \ --session "AYABeH_Ov5KzUTRUq6_Ue4q3djYAHQABAAdTZXJ2aWNlABBDb2duaXRvVXNlclBvb2xzAAEAB2F3cy1rbXMAUGFybjphd3M6a21zOmFwLW5vcnRoZWFzdC0xOjM0NjM3NzU0NDkyNzprZXkvZDNhY2NlYmQtNTdhOC00NWE0LTk1ZmEtYzc2YzY5ZDIwYTRkALgBAgEAePZZnC4WFmlF02bVD7JImpVw_X4vigfhMFizLpHK-pJkAcgRqG0veGkrClAcwbXY5RAAAAB-MHwGCSqGSIb3DQEHBqBvMG0CAQAwaAYJKoZIhvcNAQcBMB4GCWCGSAFlAwQBLjARBAyC7qV8MCqOeNGoCLICARCAO5trPDNQ6WQFigwaA1t0EZYViRuyaXmebDIao0578EUUNk-y8UhgZLUqh2pb3-qwxXYnytiKjak53MpjAgAAAAAMAAAQAAAAAAAAAAAAAAAAAM350xrmWG3y2P1ULrpNVt7_____AAAAAQAAAAAAAAAAAAAAAQAAALh4-DHXU6bvNI6QrGluKcZZedTKNHpc7yISe8i5NqjdBaXx064w25wuoMZ5_023QSjRSvIEFMTJgzMj9EvpH4z80Zh2EoHF8__88bNpwZlXHAOGbt_NGCPHqgHIBETVBXEa6YlaAXlUDMzmm68M19fq_fRCz97haivLa5ducNf7bS4xwCTaWzH2QTSA5jyg07zthEXcrqfrLDPuOJ6kQ3OkPe5QiA_3o7DZvtKACxv5mYJR_oHzYDPtV1YkObxe7_6ptiz-bQkuWw"
レスポンスは以下です。 IDトークンが返ってきました。
"ExpiresIn": 3600, "TokenType": "Bearer", "RefreshToken": "***********************************", "IdToken": "***********************************"
APIコール
以下のコマンドでAPIコールを行います。
$ idtoken=<取得したトークン> $ curl -H "Authorization: Bearer ${idtoken}" 'https://********.execute-api.ap-northeast-1.amazonaws.com/v1/hello_world'
レスポンス
Hello World!!
問題なくLambdaからのコールバックがかえってきました。
以上。