はじめに
これは「AWS Lambda と Serverless Advent Calendar 2023」 16日目の記事です。
今回のお題
- 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, });
上記ソースの通り、
- オーソライザ用Lambda関数を作成する
- Lambdaオーソライザを定義し、そこで使用するオーソライザ用Lambda関数を指定する
- 最後に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を判定している」ケースでもこの現象が発生しうるので、キャッシュを扱う際には考慮が必要です。
もちろん、うまく使えば処理時間も短縮できて便利なので、そこは扱い方次第だと思います。
まとめ
以上、AWS CDK&Lambdaオーソライザを実装する方法でした。
AWSでのサーバーレスにおいて、API Gateway - Lambda というのは「黄金パターン」と言うほどの定番なので、そこに認可処理を挟めるLambdaオーソライザはなかなか便利な機能です。
これを用意しておくと、各Lambdaで個別に認可処理を記載する必要がなくなる(かも)ので、機会があれば一度導入を検討してみてもよいと思います。
それでは、今回はこの辺で。