Lambdaオーソライザー の認証/認可が失敗した場合に特定のレスポンスヘッダーを返したい
はじめに
皆様こんにちは、あかいけです。
今回はLambdaオーソライザーの認証/認可が失敗した場合に、
特定のレスポンスヘッダーを返す方法を調べてみました。
やりたいこと
- Lambdaオーソライザーによる認証/認可の実装
- 認証失敗時(401)や認可失敗時(403)に特定のレスポンスヘッダー(本記事ではHSTS)を返す
HSTSヘッダー is 何?
今回設定するHSTSヘッダー(Strict-Transport-Security)は、ブラウザに対してHTTPSのみを使用するよう指示するヘッダーです。
通常のリダイレクト(301)との違いは、2回目以降のアクセスでのHTTP通信の有無です。
HSTSは2回目以降のアクセスではHTTPSのみになりますが、リダイレクト(301)はアクセスの度にHTTPからHTTPSへの転送が発生します。
詳細については以下ドキュメントがわかりやすいので、よければこちらでご確認ください。
実装方法
今回はAPI Gatewayのゲートウェイレスポンスを利用します。
ゲートウェイレスポンスを設定することにより、バックエンドのLambda等でエラーを返す処理を記述しなくてもAPI Gateway側でエラーを返すようになります。
またLambdaオーソライザーで認証/認可が失敗した場合、そもそもバックエンドへの処理が発生しないため、ゲートウェイレスポンスを利用することで簡単にレスポンスヘッダーを設定できます。
ゲートウェイレスポンスにはいくつか種類があり、
Lambdaオーソライザーに関連するものは以下の通りです。
レスポンスタイプ | デフォルトステータスコード | 該当するパターン |
---|---|---|
UNAUTHORIZED | 401 | Lambdaオーソライザーが発信者の認証に失敗した場合など |
ACCESS_DENIED | 403 | Lambdaオーソライザーでアクセスが拒否された場合など |
AUTHORIZER_FAILURE | 500 | Lambdaオーソライザーが発信者の認証に失敗した場合など(サーバー起因) |
AUTHORIZER_CONFIGURATION_ERROR | 500 | API GatewayからLambdaオーソライザーへ接続できなかった場合など |
その中でも認証/認可のエラーに関連するのはUNAUTHORIZEDとACCESS_DENIEDです。
※ゲートウェイレスポンスの全量は以下をご参照ください
Cloud Formation コード
今回は以下のコード用意しました。
AWSTemplateFormatVersion: "2010-09-09"
Description: >
カスタムヘッダー(x-custom-auth)をチェックし、
Lambda オーソライザーでの認証/認可が失敗した場合にゲートウェイレスポンスでHSTSヘッダーを返すREST APIです。
UNAUTHORIZED パターンと ACCESS_DENIED パターンの両方に対応しています。
Resources:
# API Gateway
ApiGatewayRestApi:
Type: AWS::ApiGateway::RestApi
Properties:
Name: Api
# Lambda用 実行ロール
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action: sts:AssumeRole
Path: /
Policies:
- PolicyName: LambdaPolicy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: arn:aws:logs:*:*:*
# バックエンド用 Lambda関数
LambdaFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: BackendFunction
Handler: index.handler
Role: !GetAtt LambdaExecutionRole.Arn
Runtime: python3.9
Code:
ZipFile: |
def handler(event, context):
return {
"statusCode": 200,
"headers": {
"Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload"
},
"body": "Hello, World!"
}
# Lambdaオーソライザー用 Lambda関数
LambdaAuthorizerFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: LambdaAuthorizerFunction
Handler: index.handler
Role: !GetAtt LambdaExecutionRole.Arn
Runtime: python3.9
Code:
ZipFile: |
def handler(event, context):
# "x-custom-auth" ヘッダーが存在するかチェック
headers = event.get("headers") or {}
if "x-custom-auth" not in headers:
# ヘッダーがない場合は認証エラー(UNAUTHORIZED)
print("UNAUTHORIZED")
return {
"principalId": "user",
"policyDocument": {
"Version": "2012-10-17",
"Statement": [{
"Action": "execute-api:Invoke",
"Effect": "Deny",
"Resource": event["methodArn"]
}]
}
}
auth_value = headers["x-custom-auth"]
if auth_value == "allow":
# 正しい値の場合は許可
print("Allow")
return {
"principalId": "user",
"policyDocument": {
"Version": "2012-10-17",
"Statement": [{
"Action": "execute-api:Invoke",
"Effect": "Allow",
"Resource": event["methodArn"]
}]
}
}
else:
# 間違った値の場合は拒否(ACCESS_DENIED)
print("ACCESS_DENIED")
return {
"principalId": "user",
"policyDocument": {
"Version": "2012-10-17",
"Statement": [{
"Action": "execute-api:Invoke",
"Effect": "Deny",
"Resource": event["methodArn"]
}]
}
}
# API Gateway バックエンドLambda呼び出し用パーミッション
ApiGatewayLambdaPermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref LambdaFunction
Action: lambda:InvokeFunction
Principal: apigateway.amazonaws.com
SourceArn: !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ApiGatewayRestApi}/*/*/*
# API Gateway Lambdaオーソライザー呼び出し用パーミッション
ApiGatewayAuthorizerLambdaPermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref LambdaAuthorizerFunction
Action: lambda:InvokeFunction
Principal: apigateway.amazonaws.com
SourceArn: !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ApiGatewayRestApi}/authorizers/*
# API Gateway リソース
TestResource:
Type: AWS::ApiGateway::Resource
Properties:
ParentId: !GetAtt ApiGatewayRestApi.RootResourceId
PathPart: "test"
RestApiId: !Ref ApiGatewayRestApi
# Lambdaオーソライザー
LambdaAuthorizer:
Type: AWS::ApiGateway::Authorizer
Properties:
Name: LambdaAuthorizer
RestApiId: !Ref ApiGatewayRestApi
Type: REQUEST
AuthorizerUri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LambdaAuthorizerFunction.Arn}/invocations
IdentitySource: method.request.header.x-custom-auth
AuthorizerResultTtlInSeconds: 300
# GET メソッドの定義(Lambda オーソライザーによる認証)
GetMethod:
Type: AWS::ApiGateway::Method
Properties:
HttpMethod: GET
ResourceId: !Ref TestResource
RestApiId: !Ref ApiGatewayRestApi
AuthorizationType: CUSTOM
AuthorizerId: !Ref LambdaAuthorizer
Integration:
IntegrationHttpMethod: POST
Type: AWS_PROXY
Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LambdaFunction.Arn}/invocations
# UNAUTHORIZED (401) レスポンス カスタマイズ
UnauthorizedGatewayResponse:
Type: AWS::ApiGateway::GatewayResponse
Properties:
ResponseType: UNAUTHORIZED
RestApiId: !Ref ApiGatewayRestApi
StatusCode: "401"
ResponseParameters:
gatewayresponse.header.Strict-Transport-Security: "'max-age=31536000; includeSubDomains; preload'"
ResponseTemplates:
application/json: '{"message":"認証エラー: UNAUTHORIZED"}'
# ACCESS_DENIED (403) レスポンス カスタマイズ
AccessDeniedGatewayResponse:
Type: AWS::ApiGateway::GatewayResponse
Properties:
ResponseType: ACCESS_DENIED
RestApiId: !Ref ApiGatewayRestApi
StatusCode: "403"
ResponseParameters:
gatewayresponse.header.Strict-Transport-Security: "'max-age=31536000; includeSubDomains; preload'"
ResponseTemplates:
application/json: '{"message":"認可エラー: ACCESS_DENIED"}'
# API Gateway ステージ デプロイ
ApiDeployment:
Type: AWS::ApiGateway::Deployment
Properties:
RestApiId: !Ref ApiGatewayRestApi
StageName: prod
DependsOn:
- GetMethod
- UnauthorizedGatewayResponse
- AccessDeniedGatewayResponse
Outputs:
ApiUrl:
Description: "API Gateway endpoint URL for the prod stage"
Value: !Sub "https://${ApiGatewayRestApi}.execute-api.${AWS::Region}.amazonaws.com/prod/"
ApiTestEndpoint:
Description: "Test endpoint URL"
Value: !Sub "https://${ApiGatewayRestApi}.execute-api.${AWS::Region}.amazonaws.com/prod/test"
以下のコマンドでデプロイしてください。
aws cloudformation create-stack --stack-name api-gateway-test --template-body file://gateway-response.yaml --capabilities CAPABILITY_NAMED_IAM
実装内容
本検証でのポイントとなる箇所を説明します。
①.Lambdaオーソライザー
Lambdaオーソライザーの認証方法はAPIキーは利用せず、シンプルに特定のリクエストヘッダーが含まれているかを確認しています。
また処理の都合上、認証エラー/認可エラーで分けていますが、
実際のエラー内容はAPI Gateway側で判断されるので、returnの内容は同じとなっています。
# Lambdaオーソライザー用 Lambda関数
LambdaAuthorizerFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: LambdaAuthorizerFunction
Handler: index.handler
Role: !GetAtt LambdaExecutionRole.Arn
Runtime: python3.9
Code:
ZipFile: |
def handler(event, context):
# "x-custom-auth" ヘッダーが存在するかチェック
headers = event.get("headers") or {}
if "x-custom-auth" not in headers:
# ヘッダーがない場合は認証エラー(UNAUTHORIZED)
print("UNAUTHORIZED")
return {
"principalId": "user",
"policyDocument": {
"Version": "2012-10-17",
"Statement": [{
"Action": "execute-api:Invoke",
"Effect": "Deny",
"Resource": event["methodArn"]
}]
}
}
auth_value = headers["x-custom-auth"]
if auth_value == "allow":
# 正しい値の場合は許可
print("Allow")
return {
"principalId": "user",
"policyDocument": {
"Version": "2012-10-17",
"Statement": [{
"Action": "execute-api:Invoke",
"Effect": "Allow",
"Resource": event["methodArn"]
}]
}
}
else:
# 間違った値の場合は拒否(ACCESS_DENIED)
print("ACCESS_DENIED")
return {
"principalId": "user",
"policyDocument": {
"Version": "2012-10-17",
"Statement": [{
"Action": "execute-api:Invoke",
"Effect": "Deny",
"Resource": event["methodArn"]
}]
}
}
②.ゲートウェイレスポンス
ゲートウェイレスポンスに関連した設定は以下の箇所です。
こちらでゲートウェイレスポンスの種類ごとにレスポンスヘッダーを設定できます。
# UNAUTHORIZED (401) レスポンス カスタマイズ
UnauthorizedGatewayResponse:
Type: AWS::ApiGateway::GatewayResponse
Properties:
ResponseType: UNAUTHORIZED
RestApiId: !Ref ApiGatewayRestApi
StatusCode: "401"
ResponseParameters:
gatewayresponse.header.Strict-Transport-Security: "'max-age=31536000; includeSubDomains; preload'"
ResponseTemplates:
application/json: '{"message":"認証エラー: UNAUTHORIZED"}'
# ACCESS_DENIED (403) レスポンス カスタマイズ
AccessDeniedGatewayResponse:
Type: AWS::ApiGateway::GatewayResponse
Properties:
ResponseType: ACCESS_DENIED
RestApiId: !Ref ApiGatewayRestApi
StatusCode: "403"
ResponseParameters:
gatewayresponse.header.Strict-Transport-Security: "'max-age=31536000; includeSubDomains; preload'"
ResponseTemplates:
application/json: '{"message":"認可エラー: ACCESS_DENIED"}'
レスポンス確認
デプロイが完了したら以下コマンドでAPI GatewayのURLを確認できます。
% aws cloudformation describe-stacks --stack-name api-gateway-test --query "Stacks[0].Outputs" --output text
API Gateway endpoint URL for the prod stage ApiUrl https://<api-gateway-id>.execute-api.<region>.amazonaws.com/<stage>/
Test endpoint URL ApiTestEndpoint https://<api-gateway-id>.execute-api.<region>.amazonaws.com/<stage>/<resource>
最後に以下の方法で特定のレスポンスヘッダーが返されるか確認します。
- 認証エラー (UNAUTHORIZED - 401)
Lambdaオーソライザーで判定するリクエストヘッダーが付与されていない場合です。
strict-transport-security(HSTSヘッダー)がレスポンスに含まれていることが確認できます。
% curl -i https://<api-gateway-id>.execute-api.<region>.amazonaws.com/<stage>/<resource>
HTTP/2 401
content-type: application/json
content-length: 43
date: Fri, 14 Mar 2025 04:19:58 GMT
x-amz-apigw-id: HZgxQFC2tjMEOlg=
x-amzn-requestid: 88a55639-fe1a-4d22-a504-f8c290e71638
strict-transport-security: max-age=31536000; includeSubDomains; preload
x-amzn-errortype: UnauthorizedException
x-cache: Error from cloudfront
via: 1.1 29f44a2f60272cb6e4a119f49c4a4390.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT20-P1
x-amz-cf-id: VU9-8s8ZZLTXMVAhILN8FcbhUI9rUENEMfct230iHhEMPR_BFr6rQg==
{"message":"認証エラー: UNAUTHORIZED"}
- 認可エラー (ACCESS_DENIED - 403)
Lambdaオーソライザーで判定するリクエストヘッダーが付与されているが値が誤っている場合です。
strict-transport-security(HSTSヘッダー)がレスポンスに含まれていることが確認できます。
% curl -i -H "x-custom-auth: deny" https://<api-gateway-id>.execute-api.<region>.amazonaws.com/<stage>/<resource>
HTTP/2 403
content-type: application/json
content-length: 44
date: Fri, 14 Mar 2025 04:20:12 GMT
x-amz-apigw-id: HZgzbGGgNjMEqZQ=
x-amzn-requestid: f052867b-b01c-4cf0-845d-b9bb390a30cf
strict-transport-security: max-age=31536000; includeSubDomains; preload
x-amzn-errortype: AccessDeniedException
x-cache: Error from cloudfront
via: 1.1 f61e62675297499135b65035072cd836.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT20-P1
x-amz-cf-id: vPw2bdN9PiKkE4B6WOIbJUaBIo4Ap9DnK00k78yTJ8bfqz-JtP7bQg==
{"message":"認可エラー: ACCESS_DENIED"}
- 成功した場合
最後にリクエストが成功した場合です。
バックエンドのLambdaでHSTSヘッダーを返す設定をしているので、こちらもstrict-transport-security(HSTSヘッダー)がレスポンスに含まれていることが確認できます。
% curl -i -H "x-custom-auth: allow" https://<api-gateway-id>.execute-api.<region>.amazonaws.com/<stage>/<resource>
HTTP/2 200
content-type: application/json
content-length: 13
date: Fri, 14 Mar 2025 04:20:30 GMT
x-amzn-trace-id: Root=1-67d3ae8e-4d442b35130b653042d73c65;Parent=1712c281cb9163a5;Sampled=0;Lineage=2:ea2c6d28:0
x-amzn-requestid: 57ba25fb-2bdf-47d6-9526-100db79b785f
strict-transport-security: max-age=31536000; includeSubDomains; preload
x-amz-apigw-id: HZg2UGJ3tjMEI4A=
x-cache: Miss from cloudfront
via: 1.1 f76b4c0eb6c4658feb5d2183e218bcee.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT20-P1
x-amz-cf-id: BPo-muH3Xi-N3mzTbNJ44yXucFcYK2pYqr0oWfE17dpwefYAUHc0BA==
Hello, World!
その他確認方法
- ブラウザの開発者モード
ブラウザの開発者モードで確認することもできます。
以下はChromeで確認した例です。
まず初回のアクセスでレスポンスヘッダーにstrict-transport-security(HSTSヘッダー)が含まれていることが確認でき、
二回目以降アクセスでHTTPを指定すると、最初に「307 Internal Redirect」 が発生します。
これはブラウザ内で発生するリダイレクトのため、実際の通信はHTTPSで行われます。
また「Non-Authoritative-Reason: HSTS」がレスポンスヘッダーに含まれていますが、これはHSTSによってリダイレクトが発生したことを表しています。
- HSTS確認サイト
あとは以下のような確認サイトでも状態を確認できます。
さいごに
以上、Lambdaオーソライザーの認証/認可が失敗した場合に特定のレスポンスヘッダーを返す方法でした。
思いのほか簡単にレスポンスヘッダーをカスタマイズでき、改めてAWSのマネージドサービスのパワフルさを感じることができました。
今後もAWSのサービスを深掘りしながら、様々な情報を発信していきますので、よろしくお願いします。