AWS CDK でLambdaオーソライザー(トークン)を実装して認証を試してみた
こんにちは!製造ビジネステクノロジー部の小林です。
最近Lambdaオーソライザーを使う機会があったので、その理解を深めるために検証を行いました。
API Gatewayでは、認証・認可を独自にカスタマイズすることができます。今回は、AWS CDK と Lambdaオーソライザーを使ってシンプルな固定トークン認証を実装してみました。
Lambdaオーソライザーとは?
Lambdaオーソライザーは、API Gatewayがバックエンドにリクエストを送る前に、認証・認可のロジックを実行するための機能です。
引用:https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html
Lambda関数が「許可 (Allow)」のポリシーを返せばAPIへのアクセスが許可され、「拒否 (Deny)」を返せばアクセスは拒否されます。
Lambdaオーソライザーのタイプ
Lambdaオーソライザーには、認証情報の取得方法によって2つのタイプがあります。
1. トークンベースのオーソライザー
JSON Web Token (JWT) や OAuthトークンなど、単一のベアラートークンで発信者IDを受け取る場合に用います。認証トークンは通常、Authorization ヘッダーから取得します。今回の実装はこちらのタイプです。
ベアラートークンとは?
ベアラートークンとは、クライアントが認証のために提示する文字列形式の認証情報です。
JWT (JSON Web Token) や OAuthトークンなどが代表的な例です。
発信者ID(Principal ID)とは?
発信者IDとは、リクエストを送信したユーザーやアプリケーションを特定するための識別子です。
- 例えば、JWTのペイロードには、ユーザーIDを示すsub(Subject)クレームが含まれています。
- Lambdaオーソライザーは、このsubの値を抽出し、principalIdとしてAPI Gatewayに返します。
2. リクエストパラメータベースのオーソライザー
複数のヘッダー、クエリ文字列、ステージ変数など、複数のIDソースに基づいて発信者IDを受け取る場合に使用します。
シンプルな固定トークン認証で実装してみる
ここでは、"allow"という固定の文字列が認証トークンとして送られてきた場合のみ、アクセスを許可するオーソライザーを実装します。
概要図
lambda-authorizer/
├── bin/
│ └── lambda-authorizer.ts
├── lib/
│ └── lambda-authorizer-stack.ts
├── lambda/
│ ├── authorizer.ts
│ └── backend.ts
...
1. Lambdaオーソライザー関数の作成
まず、認証ロジックを記述するLambda関数を作成します。
- event.authorizationToken: クライアントから送られてきた認証トークンを取得します。
- 認証ロジック: authorizationTokenが"allow"であるかを確認します。
- レスポンス: 認証結果に基づいて、IAMポリシーを返します。
import { APIGatewayTokenAuthorizerEvent, APIGatewayAuthorizerResult } from 'aws-lambda';
export const handler = async (event: APIGatewayTokenAuthorizerEvent): Promise<APIGatewayAuthorizerResult> => {
// ① 認証トークンを取得
const token = event.authorizationToken;
// ② 認証ロジック:トークンが'allow'なら許可、それ以外は拒否
const effect = token === 'allow' ? 'Allow' : 'Deny';
// ③ IAMポリシーを返す
return {
principalId: 'user', // 固定のユーザーID
policyDocument: {
Version: '2012-10-17',
Statement: [{
Action: 'execute-api:Invoke',
Effect: effect,
Resource: event.methodArn,
}],
},
};
};
このソースでは、event.authorizationTokenが"allow"と一致すればeffectが"Allow"となり、アクセスが許可されます。それ以外の値の場合は"Deny"となり、アクセスが拒否されます。
2. バックエンドLambdaのコード
この関数は、オーソライザーの認証を通過したリクエストのみが実行されます。
import { APIGatewayProxyResult } from 'aws-lambda';
export const handler = async (): Promise<APIGatewayProxyResult> => {
return {
statusCode: 200,
body: JSON.stringify({ message: 'Hello from lambda API' }),
};
};
3. AWS CDKによるインフラの定義
AWS CDKを使って、API Gateway、Lambda関数などをまとめて定義します。
import { Stack, StackProps, aws_apigateway as apigw } from 'aws-cdk-lib';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import { Runtime } from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';
import * as path from 'path';
export class LambdaAuthorizerStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
// バックエンドLambda関数の定義
const backendFunction = new NodejsFunction(this, 'BackendFunction', {
runtime: Runtime.NODEJS_22_X,
entry: path.join(__dirname, '..', 'lambda', 'backend.ts'),
handler: 'handler',
});
// Lambdaオーソライザー関数の定義
const authorizerFunction = new NodejsFunction(this, 'AuthorizerFunction', {
runtime: Runtime.NODEJS_22_X,
entry: path.join(__dirname, '..', 'lambda', 'authorizer.ts'),
handler: 'handler',
});
// API Gatewayの定義
const api = new apigw.RestApi(this, 'MyProtectedApi', {
restApiName: 'MyProtectedApi',
description: 'API protected by a Lambda authorizer',
});
// Lambdaオーソライザーの定義
const authorizer = new apigw.TokenAuthorizer(this, 'MyLambdaAuthorizer', {
handler: authorizerFunction,
identitySource: 'method.request.header.Authorization',
});
// バックエンド統合の定義
const backendIntegration = new apigw.LambdaIntegration(backendFunction);
// リソースとメソッドの定義
const helloResource = api.root.addResource('hello');
helloResource.addMethod('GET', backendIntegration, {
authorizer: authorizer,
});
}
}
上記でリソースの作成は完了です。cdk deployコマンドでデプロイします。
動作確認
デプロイが完了したら、AWSコンソールで作成されたリソースを確認してみます。
Lambdaオーソライザーが作成されていますね!
では、実際にAPIを呼び出して、認証が正しく動作するか確認してみます。curlコマンドを使って動作を確認します。{API_ID}はデプロイされたAPIのエンドポイントIDに置き換えてください。
1. 認証なしでアクセス(失敗パターン)
Authorizationヘッダーがない場合、認証情報が不足しているため401 Unauthorizedが返されます。
curl -i -X GET https://{API_ID}.execute-api.ap-northeast-1.amazonaws.com/prod/hello
結果
HTTP/2 401
~省略
{"message":"Unauthorized"}%
2. 無効なトークンでアクセス(失敗パターン)
Authorizationヘッダーに"invalid-token"のような無効なトークンを渡すと、オーソライザーがDenyポリシーを返すため403が返されます。
curl -i -X GET https://{API_ID}.execute-api.ap-northeast-1.amazonaws.com/prod/hello \
-H "Authorization: invalid-token"
結果
HTTP/2 403
〜省略
{"Message":"User is not authorized to access this resource with an explicit deny"}
3. 有効なトークンでアクセス(成功パターン)
Authorizationヘッダーに"allow"を含めてリクエストを送信すると、オーソライザーがAllowポリシーを返すため、バックエンドのLambda関数が実行されます。
curl -i -X GET https://{API_ID}.execute-api.ap-northeast-1.amazonaws.com/prod/hello \
-H "Authorization: allow"
結果
HTTP/2 200
〜省略
{"message":"Hello from lambda API!"}
余談
AWSコンソールからでも、Lambdaオーソライザーの動作確認ができるようです。便利ですね!
おわりに
今回は、Lambdaオーソライザーを使ってAPI Gatewayにカスタム認証を実装する方法を解説しました。シンプルな固定トークン認証を通して、認証フローを確認することができましたね。次回はJWTトークンを利用した認証に挑戦し、よりセキュアなAPIを構築してみます!この記事が皆様のお役に立てば幸いです。
参考記事