事業本部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コード
lib/cdk-sample-cognito-auth-api-stack.ts
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= hogefuga@example.com \
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\":\"abe.daisuke@classmethod.jp\"}"
}
}
初期パスワード変更
以下のコマンドで初期パスワードを変更します。
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からのコールバックがかえってきました。
以上。