はじめに
これは「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で作成する方法です。
const authorizerLambda = new NodejsFunction(this, 'AuthorizerFunction', {
entry: path.resolve(__dirname, '../../lambda', 'authorizer.ts'),
handler: 'handler',
functionName: 'AuthorizerFunction',
});
const authorizer = new RequestAuthorizer(this, 'RequestAuthorizer', {
handler: authorizerLambda,
identitySources: [IdentitySource.header('Authorization')],
authorizerName: 'RequestAuthorizer',
resultsCacheTtl: cdk.Duration.seconds(0),
});
const res = restApi.root.addResource('hoge');
res.addMethod('GET', new LambdaIntegration(hogeLambda), {
authorizer,
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') {
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で個別に認可処理を記載する必要がなくなる(かも)ので、機会があれば一度導入を検討してみてもよいと思います。
それでは、今回はこの辺で。