echo("備忘録");

IT技術やプログラミング関連など、技術系の事を備忘録的にまとめています。

【AWS CDK】【AWS Lambda】Lambdaオーソライザを実装する

はじめに

これは「AWS Lambda と Serverless Advent Calendar 2023」 16日目の記事です。

qiita.com

今回のお題

  • Lambdaオーソライザを使用し、API GatewayからLambdaを実行する前段で認可処理を実施する
  • LambdaオーソライザはRequestベースのものを使用する
  • Lambdaオーソライザ含め、API GatewayからLambdaのリソースを全てAWS CDKで作成する

Lambdaオーソライザって何?

API Gatewayにリクエストを送信された時に「特定のリクエストにのみ対象のLambdaを実行させたい」という認可処理を行いたい時があります。

この「特定のリクエスト」のチェックを行う機構が「オーソライザ」になります。
そして、このオーソライザをLambdaのソースコードで実装したものが「Lambdaオーソライザ」です。

API Gatewayでは、オーソライザとしてこの「Lambdaオーソライザ」を使用することが可能です。

そこで、今回はこのLambdaオーソライザを実装する処理について記載します。

具体的なユースケースは?

具体的なユースケースの例としては、以下の通りです。

  • ログインしているユーザーにのみ該当のLambdaを実行させる
  • アプリ上で何らかの権限を有しているユーザーにのみ該当のLambdaを実行させる

Lambdaオーソライザの種類

Lambdaオーソライザには、以下の2種類があります

種類 説明 送信する値 備考
TOKENオーソライザ JSON ウェブトークン (JWT) やOAuth, SAMLなどのベアラートークンでの認可を行う トークンの値
Requestオーソライザ リクエスト情報として渡された各種情報を元に認可を行う 各種リクエスト情報 例えば、Authorizationヘッダの値を元にログインチェックを行う、など

なお、今回はRequestオーソライザのみを扱います。(Tokenオーソライザは扱いません)

Tokenオーソライザについては、下記のブログが参考になりますので、こちらをご参照ください。

参考サイト

Lambdaオーソライザの実装

Lambdaオーソライザの実装ですが、いきなりサンプルコードを記載します。
※基本的には「参考サイト」に記載したAWS公式サイトのコードとほぼ同じです。

import { Context, APIGatewayRequestAuthorizerEvent, APIGatewayAuthorizerResult } from "aws-lambda";
  
const generateAuthorizerResult = (effect: string, resource: string): APIGatewayAuthorizerResult => {
  const result = {
    principalId: 'Authorizer',
    policyDocument: {
      Version: "2012-10-17",
      Statement: [{
        Action: 'execute-api:Invoke',
        Effect: effect,
        Resource: resource,
      }]
    }
  } as APIGatewayAuthorizerResult;
  
  return result;
}
  
export const handler = async (event: APIGatewayRequestAuthorizerEvent, context: Context ): Promise<APIGatewayAuthorizerResult> => {
  
  const param = event?.headers?.Authorization ?? '';
  console.log(`param is ${param}`);
  
  if (param === 'allow') {
    return generateAuthorizerResult('Allow', event.methodArn);
  } else if (param === 'deny') {
    return generateAuthorizerResult('Deny', event.methodArn);
  } else {
    throw new Error('Unauthorized');
  }
};

重要なのは以下の点。(前者は問題ないと思うので、以後は後者のみ記載)

  • 未認証(ステータス:401) を返したいときは、Unauthorized エラーをスローする
  • それ以外は APIGatewayAuthorizerResult 型のオブジェクトをreturnする

後者の APIGatewayAuthorizerResult 型オブジェクトで最も重要なのは policyDocument.Statementの「Effect」 で、これを「Allow」にすれば許可、「Deny」にすれば不許可です。

またActionやResourceも設定できるので、「最小権限の原則」に従い、Allowする場合はここも設定しておくとよいと思います。(実際には上ソースのように「Resource」にはevent.methodArnの値をそのまま指定すればOKです)

なおDenyの場合、ActionやResourceはどちらも"*"でもいいと思います。(もちろん指定してもいい)

ちなみに APIGatewayAuthorizerResult 型オブジェクトについては、下記AWS公式サイトに詳しく載っているので、そちらも参照してください。

Amazon API Gateway Lambda オーソライザーからの出力(AWS公式サイト)

AWS CDKでLambdaオーソライザを定義する

次はLambdaオーソライザをAWS CDKで作成する方法です。

// Lambdaオーソライザ用Lambda関数  
const authorizerLambda = new NodejsFunction(this, 'AuthorizerFunction', {
  entry: path.resolve(__dirname, '../../lambda', 'authorizer.ts'),
  handler: 'handler',
  functionName: 'AuthorizerFunction',
});
  
// Lambdaオーソライザ
const authorizer = new RequestAuthorizer(this, 'RequestAuthorizer', {
   handler: authorizerLambda,
   // オーソライザで認証に使う値
   identitySources: [IdentitySource.header('Authorization')],
   authorizerName: 'RequestAuthorizer',
   // キャッシュは思わぬ副作用があるので注意(後述)
   resultsCacheTtl: cdk.Duration.seconds(0),
});
  
// API Gateway(restApi)やLambdaオーソライザ認可後に実施する
// Lambda(hogeLambda)の定義は省略
const res = restApi.root.addResource('hoge');
res.addMethod('GET', new LambdaIntegration(hogeLambda), {
  authorizer,
  // Lambdaオーソライザの場合、ここはCUSTOMを指定
  authorizationType: AuthorizationType.CUSTOM,
});

上記ソースの通り、

  1. オーソライザ用Lambda関数を作成する
  2. Lambdaオーソライザを定義し、そこで使用するオーソライザ用Lambda関数を指定する
  3. 最後にAPI Gatewayのメソッド定義で、オーソライザと認証タイプ(AuthorizationType.CUSTOM)を指定する

という感じで定義すればOKで、AWS CDKの定義自体は意外とシンプルです。

キャッシュを扱う場合の注意

Lambdaオーソライザではキャッシュを扱うことができます。
うまく使えば処理時間になりますが、ちょっと注意が必要なケースもあります。

前提として、キャッシュは下記の挙動をします。

  • キャッシュの結果は「認証ソース」の値ごとに保持される
    • AWS CDKにおける「identitySources」で設定した項目の値
  • キャッシュはTTLで指定した秒数の間保持される(デフォルトは5分)
    • AWS CDKの「resultsCacheTtl」の値
    • キャッシュを使用しない場合は0を指定する

そして「キャッシュがあると予期しない挙動をする」ケースとして、以下に一例を示します。

キャッシュが予期しない挙動をする具体例

仮に「Lambdaオーソライザの実装」で示したソースについて、間違えて`Authorizationヘッダが「allow」の時もDenyする処理を書いてしまったとします。

if (param === 'allow') {
  // Allowと間違えてDenyにしてしまってる
  return generateAuthorizerResult('Deny', event.methodArn);
} else if (param === 'deny') {
  return generateAuthorizerResult('Deny', event.methodArn);
} else {
  throw new Error('Unauthorized');
}

この時Authorizationヘッダに'allow'を指定しても、当然そのリクエストはDenyされます。

そしてその後ミスに気付いて、コードを修正後に再度Authorizationヘッダが'allow'のリクエストを送信すると、そのリクエストは当然Allowされる

...と思いきや、しばらくはDenyされ続けてしまいます。

これが「キャッシュによる予期しない挙動」で、「Authorizationヘッダ'allow'はDenyされた」という結果をTTLの時間が経過するまでキャッシュとして保持します。

その結果、「(Authorizationヘッダが'allow'の場合)本来Allowされるべきリクエストもキャッシュが有効な期間はDenyされる」という現象が発生してしまいます。

他にも、クラスメソッドさんの下記ブログで紹介されている「Authorizationヘッダ以外の値でAllowかDenyを判定している」ケースでもこの現象が発生しうるので、キャッシュを扱う際には考慮が必要です。

dev.classmethod.jp

もちろん、うまく使えば処理時間も短縮できて便利なので、そこは扱い方次第だと思います。

まとめ

以上、AWS CDK&Lambdaオーソライザを実装する方法でした。

AWSでのサーバーレスにおいて、API Gateway - Lambda というのは「黄金パターン」と言うほどの定番なので、そこに認可処理を挟めるLambdaオーソライザはなかなか便利な機能です。

これを用意しておくと、各Lambdaで個別に認可処理を記載する必要がなくなる(かも)ので、機会があれば一度導入を検討してみてもよいと思います。

それでは、今回はこの辺で。