API GatewayのオーソライザーにCognitoを使用してみた

2023.03.23

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

事業本部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アドレスを必須にするため、standardAttributesemail:trueを追加しています。
autoVerifyにemailを追加することで、emailアドレスの検証が不要になります。
signInAliasesにemailを追加することで、ユーザー名にemailアドレスを使用できます。プロパティを後から変更できません。変更する場合はユーザープールを削除して再作成する必要があります。
RemovalPolicyDESTROYを指定することで、スタックを削除するとユーザープールも削除されます。本番環境では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メソッドを使用してユーザープールにクライアントを追加しています。
authFlowsadminUserPassword: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からのコールバックがかえってきました。

以上。