[前編] AWS CDKで API Gateway + Lambda 構成のREST APIを構築して Auth0 + Lambda Authorizerの認可機能を導入してみた

2023.03.08

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

CX事業本部Delivery部のアベシです。

こちらの記事では、API Gateway + Lambda のREST APIに Auth0 + Lambda Authorizerの認可を導入する方法について紹介します。
前編、更編に分けて紹介します。

今回の前編ではLambda Authorizer と Auth0を使ったAPI Gatewayの保護の仕組みと、土台となるAPIのCDKコードについて紹介しようと思います。

Lambda Authorizer と Auth0を使った認可の仕組み

以下のフローで認可が行われます。

① クライアントがAuth0に認可をリクエストする。
② 認可されたらAuth0がアクセストークンを返す。
③ クライアントがAPIコールする。その際にアクセストークンをヘッダーとしてAPI Gatewayに渡す。
④ API GatewayがLambda Authorizerにアクセストークンを渡して関数を実行する。
⑤ Lambda Authorizerはアクセストークンを用いてAuth0へ公開鍵の取得をリクエストする。
⑥ Auth0が公開鍵をLambda Authorizerに返し、アクセストークンの RS256 署名の検証を実行する。
⑦ 検証が成功したらAPIを実行するために必要なポリシーを作成しAPI Gatewayに渡す。
⑧ API GatewayがLambda関数を実行する。


上で解説した内容はAuth0の以下の公式記事にも詳しく書かれてますので、参照していただければと思います。

実行環境

以下の環境で構築と動作確認しています。

項目名 バージョン
mac OS Ventura 13.2
npm 9.6.0
AWS CDK 2.66.1

API Gateway + Lambda の REST API

まずは土台となるREST APIのコードが以下となります。 Lambda統合プロキシを用いたのREST APIとなります。

lib/lambda-authorizer-with-auth0-stack.ts

import {
  Stack,
  StackProps,
  aws_apigateway,
  aws_lambda_nodejs,
  Duration
} from 'aws-cdk-lib';
import { Runtime } from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';


export class LambdaAuthorizerWithAuth0Stack extends Stack {
  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, props);

    //HelloWorldするLambdaの作成
    const nameHelloWorldFunc = "Hello_world_func" // 名前にspaceが使えないので注意
    const registerTaskFunc = new aws_lambda_nodejs.NodejsFunction(
      this,
      nameHelloWorldFunc,
      {
        runtime: Runtime.NODEJS_18_X,
        functionName: nameHelloWorldFunc,
        entry: 'src/lambda/handlers/hello-world-func.ts',
        timeout: Duration.seconds(25),
        logRetention: 30,
      },
    );

    // API Gateway RestAPIの作成
    const nameRestApi ="Rest API with Lambda auth";
    const restApi = new aws_apigateway.RestApi(this, nameRestApi, {
      restApiName: `Rest_API_with_Lambda_auth`,
      deployOptions: {
        stageName: 'v1',
      },
    });

    //API Gatewayにリクエスト先のリソースを追加
    const restApiHelloWorld = restApi.root.addResource('hello_world');

    //リソースにGETメソッド、Lambda統合プロキシを指定
    restApiHelloWorld.addMethod(
      'GET',
      new aws_apigateway.LambdaIntegration(registerTaskFunc)
    );
  }
}

コードの解説

  • Lambdaのビルド方法定義

API Gatewayの後続のLambda関数のビルド方法の定義します。
aws_lambda_nodejsのNodejsFunctionクラスを使用してビルドします。
ランタイムはNode.js18です(CDKのバージョンが古いと指定できません)。Node.jsはバージョン16が2023年9月11日でサポート終了なのできれば18を使いましょう。
あと、functionNameプロパティですが、名前にspaceが使えないです。私はよく忘れてひっかかってしまってます。

Bringing forward the End-of-Life Date for Node.js 16

  • Rest APIの作成

aws_apigatewayのRestApiクラスを使って定義します。

LambdaとのリクエストメッセージとレスポンスのやりとりにはLambda統合プロキシを使用しています。
メソッドの定義のところでLambdaIntegrationクラスを使用して定義しています。

    restApiHelloWorld.addMethod(
      'GET',
      new aws_apigateway.LambdaIntegration(registerTaskFunc)
    );

Lambdaからのレスポンスは決められたJSON形式で返す必要があります。

プロキシ統合のための Lambda 関数の出力形式

レスポンスに使えるプロパティは以下となります。

{
    "isBase64Encoded": true|false,
    "statusCode": httpStatusCode,
    "headers": { "headerName": "headerValue", ... },
    "multiValueHeaders": { "headerName": ["headerValue", "headerValue2", ...], ... },
    "body": "..."
}

この内必須項目はstatusCodeのみとなります。

Lambda統合プロキシを使用しない場合はLambdaへリクエストメッセージのどの部分を渡すのか、Lambdaからのレスポンスにどんな項目を返すのかを指定するために、モデルとマッピングテンプレートの定義が必要です。
実際にどちらも試すとLambda統合プロキシのほうが如何に楽にREST APIが作れるか解ると思います。

デプロイ

$ cdk deploy --require-approval never '*'

Bundling asset LambdaAuthorizerWithAuth0Stack/Hello_world_func/Code/Stage...

...

Outputs:
LambdaAuthorizerWithAuth0Stack.RestAPIEndpointB14C3C54 = https://**********.execute-api.ap-northeast-1.amazonaws.com/v1/

AWS CLIでデプロイするとAPI GatewayのURLが表示されますので控えておきます。

curl コマンドでAPIを叩いてみましょう。

curl -X GET \
https://**********.execute-api.ap-northeast-1.amazonaws.com/v1/hello_world

出力

Hello World!!

CDKのコードにLambda Authorizerを追加

先程のコードにLambda Authorizerの導入に必要な定義を追加します。
追加後のコードが以下となります。

lib/lambda-authorizer-with-auth0-stack.ts

import {
  Stack,
  StackProps,
  aws_apigateway,
  aws_lambda_nodejs,
  aws_iam,
  Duration,
} from 'aws-cdk-lib';
import { Runtime } from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';

export class LambdaAuthorizerWithAuth0Stack extends Stack {
  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, props);

    const nameHelloWorldFunc = "Hello_world_func"
    const registerTaskFunc = new aws_lambda_nodejs.NodejsFunction(
      this,
      nameHelloWorldFunc,
      {
        runtime: Runtime.NODEJS_18_X,
        functionName: nameHelloWorldFunc,
        entry: 'src/lambda/handlers/hello-world-func.ts',
        timeout: Duration.seconds(25),
        logRetention: 30,
      },
    );

    // Lambda Authorizer用のLambda関数
    const nameLambdaAuthorizerFunc = "Lambda_Authorizer_Function"
    const lambdaAuthorizerFunc = new aws_lambda_nodejs.NodejsFunction(
      this,
      nameLambdaAuthorizerFunc,
      {
        functionName: nameLambdaAuthorizerFunc,
        entry: 'src/lambda/handlers/lambda-authorizer.ts',
        runtime: Runtime.NODEJS_18_X,
        timeout: Duration.seconds(25),
        logRetention: 30,
        environment: { //トークンの検証に必要なデータを環境変数に格納
          AUDIENCE: "https://**********.execute-api.ap-northeast-1.amazonaws.com",
          JWKS_URI: "https://your_tenant.auth0.com/.well-known/jwks.json",
          TOKEN_ISSUER: "https://your_tenant.auth0.com/"
        },
      },
    );

    const restApi = new aws_apigateway.RestApi(this, 'RestAPI', {
      restApiName: `restApi`,
      deployOptions: {
        stageName: 'v1',
      },
    });


    // Lambda Authorizerの定義
    const lambdaAuth = new aws_apigateway.TokenAuthorizer(
      this,
      'lambdaAuthorizer',
      {
        authorizerName: 'lambdaAuthorizer',
        handler: lambdaAuthorizerFunc,//ここでLambda Authorizer用のLambda関数を割り当てる
        identitySource: aws_apigateway.IdentitySource.header('Authorization'),//アクセストークンを渡すためのヘッダーを指定
      },
    );

    const restApiTasks = restApi.root.addResource('hello_world');

    restApiTasks.addMethod(
      'GET',
      new aws_apigateway.LambdaIntegration(registerTaskFunc),
      {
        authorizer: lambdaAuth, // 定義したLambdaAuthorizerを指定
      },
    );
  }
}

コードの解説

追加したコードについて説明します。

  • Lambda Authorizer用のLambda関数のビルド定義

こちらもNodejsFunctionクラスつかったビルドを定義しています。
アクセストークンのRS256署名を検証するの際に使う値を環境変数に指定してます。
この変数については後編で説明します。

const lambdaAuthorizerFunc = new aws_lambda_nodejs.NodejsFunction(
  this,
  nameLambdaAuthorizerFunc,
  {
    functionName: nameLambdaAuthorizerFunc,
    entry: 'src/lambda/handlers/lambda-authorizer.ts',
    runtime: Runtime.NODEJS_18_X,
    timeout: Duration.seconds(25),
    logRetention: 30,
    environment: { //トークンの検証に必要なデータを環境変数に格納
      AUDIENCE: "https://**********.execute-api.ap-northeast-1.amazonaws.com",
      JWKS_URI: "https://your_tenant.auth0.com/.well-known/jwks.json",
      TOKEN_ISSUER: "https://your_tenant.auth0.com/"
    },
  },
);
  • Lambda Authorizerの定義
    TokenAuthorizerクラスを使用してAPI Gatewayの認可にLambda Authorizerを使用するように定義しています。
    handlerプロパティに前段で作成したLambda Authorizer用のLambda関数を割り当てます。
    identitySourceプロパティにはどのヘッダーにアクセストークンを持たせるか指定します。
    ここではAuthorizationヘッダーを指定しています。
const nameLambdaAuth = 'lambdaAuthorizer'
const lambdaAuth = new aws_apigateway.TokenAuthorizer(
  this,
  nameLambdaAuth,
  {
    authorizerName: nameLambdaAuth,
    handler: lambdaAuthorizerFunc,// ここでLambda Authorizer用のLambda関数を割り当てる
    identitySource: aws_apigateway.IdentitySource.header('Authorization'),
  },
);
  • メソッドに認可方法を指定

前段でTokenAuthorizerクラスを使って定義したLambda Authorizerauthorizerプロパティに指定しています。

restApiTasks.addMethod(
  'GET',
  new aws_apigateway.LambdaIntegration(registerTaskFunc),
  {
    authorizer: lambdaAuth, // 定義したLambda Authorizerを指定
  },
);

CDKのコードについての解説は以上です。

後編で紹介する内容

次回の後編では以下を紹介いたします。

  • Auth0側の設定
  • Lambda AuthorizerのLambda関数の紹介と解説
  • 認可の動作確認


前編は以上、後編に続く。。。

後編

以下のブログが後編となります。