「Forbidden」じゃ分からない!API Gatewayのエラーレスポンスをカスタマイズして分かりやすくしてみた
こんにちは!製造ビジネステクノロジー部の小林です。
サーバーレスアーキテクチャでは、API Gatewayはよく活用されるサービスの一つです。API Gatewayのデフォルトのエラーレスポンスはシンプルに設計されているため、クライアント側での原因特定や対応が難しい場合があります(セキュリティのために詳細を公開したくない場合は除きます)。
今回は、API Gatewayのエラーレスポンスをカスタマイズして、クライアントにとってより分かりやすいメッセージを表示できるようにしてみました。
レスポンスをカスタマイズすることで、エラーメッセージを日本語で表示したり、ユーザーに必要な情報を的確に伝えることができます。この取り組みをAWS CDKを使って実装してみたので、その方法をご紹介します!
やりたいこと
以下の2種類の403エラーレスポンスを、カスタマイズ前後で比較します。
- 無効なAPIキーを使用した場合の403エラー
- AWS WAFによってブロックされたリクエストの403エラー
カスタマイズでは、以下の改善を行います。
- returnCodeの追加(同じ403エラーでも区別できるようにする)
- 403-40301 無効なAPIキーを使用した場合
- 403-40302 AWS WAFによってブロックされた場合
- 日本語によるわかりやすいエラーメッセージの実装
実装にはAWS CDKを使用します。また、実装にあたって気になった用語についても調査しました。
API Gatewayカスタムエラーレスポンスとは?
API Gatewayは、リクエストが統合に到達しなかった場合にゲートウェイレスポンスを返します。そしてこのレスポンスはカスタマイズできます。
デフォルトでは、API Gatewayは標準的なHTTPステータスコードとシンプルなエラーメッセージを返しますが、これらは必ずしもエンドユーザーにとって分かりやすいものではありません。
API Gatewayでは「ゲートウェイレスポンス」という機能を通じて、様々なエラーシナリオに対するレスポンスをカスタマイズできます。例えば、認証エラー(403)、リソースが見つからない場合(404)、サーバーエラー(500)など、それぞれのケースで異なるカスタムレスポンスを設定できます。
APIキーとは
APIキーは、API Gatewayで提供されるAPIへのアクセスを制御するための認証メカニズムの一つです。主に以下の目的で使用されます。
- 承認されたクライアントのみがAPIを使用できるようにする
- 誰がどのくらいAPIを使用しているかを監視する
- 特定のクライアントの使用量を制限する
- API利用に基づく課金モデルの実装
APIキーは通常、HTTPリクエストヘッダー(x-api-key)に含めて送信されます。
使用量プランとは
使用量プラン(Usage Plan)は、API Gatewayで提供するAPIの使用量を管理・制限するための機能です。APIキーと組み合わせて使用され、以下のような制御が可能になります。
- 1秒あたりのリクエスト数に上限を設定し、APIの過負荷を防ぐ
- 日、週、月単位でのリクエスト総数に上限を設定できる
使用量プランを設定することで、無料ユーザーには1日100リクエストまで、有料ユーザーには1日10,000リクエストまで、といった制限を設けることが可能です。
やってみた
AWS CDK(TypeScript)を使用して、以下の2種類のカスタムエラーレスポンスを実装します。
- 403 - 40301: 無効なAPIキーを使用した場合
{
"message": "このリクエストはブロックされています",
"returnCode": "40301"
}
- 403 - 40302: リクエストがAWS WAFによってブロックされた場合
{
"message": "このリクエストはブロックされています",
"returnCode": "40302"
}
これらのカスタムエラーレスポンスにより、クライアントは単なるHTTPステータスコードだけでなく、より具体的な問題の内容を把握できるようにします。
API Gatewayの作成
REST APIを作成します。
// REST API の作成
const api = new apigateway.RestApi(this, 'CustomErrorsApi', {
restApiName: 'Custom Errors API',
deployOptions: {
stageName: 'prod',
},
});
APIキーの作成と使用量プランを設定します。
// APIキーの作成
const apiKey = new apigateway.ApiKey(
this, 'ApiKey', {
apiKeyName: 'TestApiKey',
}
);
// 使用量プランの作成
const plan = new apigateway.UsagePlan(
this, 'UsagePlan', {
name: 'Standard',
apiStages: [
{
api,
stage: api.deploymentStage,
},
],
});
// 使用量プランにAPIキーを関連付け
plan.addApiKey(apiKey);
Lambdaの作成とAPI Gatewayとの統合
シンプルなLambdaを作ってAPI Gatewayと統合します。今回は簡単なデモ用に、インラインコードでLambda関数を定義しています。
// シンプルな Lambda 関数を作成
const helloFunction = new lambda.Function(this, 'HelloHandler', {
runtime: lambda.Runtime.NODEJS_18_X,
handler: 'index.handler',
code: lambda.Code.fromInline(`
exports.handler = async function() {
return {
statusCode: 200,
body: JSON.stringify({ message: 'Hello Lambda' })
};
}
`)
});
/**
* API Gateway のルートに GET メソッドを追加し、Lambda と統合
* APIキー要求を有効化
*/
api.root.addMethod('GET', new apigateway.LambdaIntegration(helloFunction), {
apiKeyRequired: true // APIキーを必須に設定
});
この部分では、単純に「Hello from Lambda」というメッセージを返すだけのLambda関数を作成し、API Gatewayのルートパス(/)にGETメソッドとして統合しています。apiKeyRequired: trueを設定することで、このエンドポイントへのアクセスには有効なAPIキーが必要になります。
API GatewayにWAFを設定
リクエストをWAFにブロックさせるため、API GatewayにWAFを設定します。これにより、特定のパターンのリクエストを自動的にブロックし、APIを保護できます。
// WAFテスト用のエンドポイントを追加
const wafTestResource = api.root.addResource('waf-test');
wafTestResource.addMethod('GET', new apigateway.LambdaIntegration(helloFunction))
// AWS WAFの設定 - 特定のパス(waf-test)だけをブロック
const webAcl = new wafv2.CfnWebACL(this, 'ApiWaf', {
name: 'ApiGatewayWAF',
scope: 'REGIONAL',
defaultAction: { allow: {} },
visibilityConfig: {
cloudWatchMetricsEnabled: true,
metricName: 'ApiGatewayWAF',
sampledRequestsEnabled: true
},
rules: [
// 特定のパス(waf-test)をブロックするルール
{
name: 'BlockWafTestPath',
priority: 0,
action: { block: {} },
statement: {
byteMatchStatement: {
fieldToMatch: {
uriPath: {}
},
positionalConstraint: 'CONTAINS',
searchString: 'waf-test', // 'waf-test'を含むパスをブロック
textTransformations: [
{
priority: 0,
type: 'NONE'
}
]
}
},
visibilityConfig: {
cloudWatchMetricsEnabled: true,
metricName: 'BlockWafTestPath',
sampledRequestsEnabled: true
}
}
]
})
/**
* WAFとAPI Gatewayを関連付ける
*
* この関連付けにより、指定したAPI Gatewayステージへのリクエストに対してWAFのルールが適用され、
* 不正なトラフィックからAPIを保護する。
*/
new wafv2.CfnWebACLAssociation(this, 'WafApiAssociation', {
resourceArn: `arn:aws:apigateway:${this.region}::/restapis/${api.restApiId}/stages/${api.deploymentStage.stageName}`,
webAclArn: webAcl.attrArn
});
このコードでは、waf-testというパスを含むリクエストを自動的にブロックするWAFルールを作成しています。
では上記をデプロイします。デプロイ後は動作確認をしてみます。デプロイが完了すると、API GatewayのエンドポイントURLやAPIキーなどの情報がコンソールに表示されます。これらの情報を使って、実際にAPIが期待通りに動作するか確認していきます。
動作確認
カスタムエラーレスポンスを設定しない状態で動作確認をしてみます。デフォルトの状態で、API Gatewayはどのようなレスポンスを返すのでしょうか?curlコマンドでAPI Gatewayにリクエストを投げてみます。
正常系のリクエスト
curl -X GET https://0a0z30pane.execute-api.ap-northeast-1.amazonaws.com/v1/ \
-H "x-api-key: APIキーを入力"
結果
{"message":"Hello Lambda"}%
APIキーを利用して正常にGETリクエストが実行できました。
では、APIキーを間違えてリクエストしてみます。(APIキーを適当な文字に変更)すると...
{"message":"Forbidden"}%
上記のようなレスポンスがきました。
WAFによるブロックの確認
続いて、パターン2のWAFによるブロックを確認してみます。
curl -X GET https://0a0z30pane.execute-api.ap-northeast-1.amazonaws.com/v1/waf-test \
-H "x-api-key: APIキー"
結果
{"message":"Forbidden"}
先ほど同様の結果ですね。
無効なAPIキーを使用した場合とWAFによってブロックされた場合、まったく同一のエラーメッセージ「Forbidden」が返されています。このような一律的なエラー応答では、開発者やユーザーがどのような理由でリクエストが拒否されたのか判断することが困難です。
そこで次のステップでは、各エラーシナリオに対応したカスタムエラーレスポンスを実装します。これにより、同じHTTPステータスコード(403 Forbidden)でも、エラーの原因に応じて異なる情報を提供できるようになります!
エラーレスポンスをカスタマイズする
ではCDK でAPI Gatewayのエラーレスポンスをカスタマイズしてみます。API GatewayのGatewayResponseを使用することで、特定のエラーシナリオに対してカスタマイズされたレスポンスを返すことができます。
無効なAPIキーに対するカスタムレスポンス
// 403 - 40301: 無効なAPIキーを使用した場合
new apigateway.GatewayResponse(this, 'InvalidApiKeyResponse', {
restApi: api,
type: apigateway.ResponseType.INVALID_API_KEY,
statusCode: '403',
templates: {
'application/json': JSON.stringify({
message: '無効なAPIキーです',
returnCode: '40301'
})
}
});
WAFによるブロックに対するカスタムレスポンス
// 403 - 40302: リクエストがAWS WAFによってブロックされた場合
new apigateway.GatewayResponse(this, 'WafFilteredResponse', {
restApi: api,
type: apigateway.ResponseType.WAF_FILTERED,
statusCode: '403',
templates: {
'application/json': JSON.stringify({
message: 'このリクエストはブロックされています',
returnCode: '40302'
})
}
});
このように、同じHTTPステータスコード(403)でも、エラーの原因に応じて異なるメッセージと識別コードを返すことができます。これにより、クライアント側でエラーの原因を正確に把握し、適切な対応を取ることが可能になります。
動作確認
カスタムエラーレスポンスを実装した後、各シナリオでの動作を確認します。
- 正常なリクエスト
まず、正しいAPIキーを使用してリクエストを送信します。
curl -X GET https://0a0z30pane.execute-api.ap-northeast-1.amazonaws.com/v1/ \
-H "x-api-key: 正しいAPIキー"
結果
{"message":"Hello from Lambda"}
正常にレスポンスが返ってきました。Lambdaからの応答がそのまま返されています。
- 無効なAPIキーの場合
次に、APIキーを間違えてリクエストしてみます。
curl -X GET https://0a0z30pane.execute-api.ap-northeast-1.amazonaws.com/v1/ \
-H "x-api-key: 間違ったAPIキー"
結果
{"message":"無効なAPIキーです","returnCode":"40301"}
カスタムエラーレスポンスが正しく返されました!デフォルトでは単に{"message":"Forbidden"}と表示されていたものが、より具体的なエラー内容とカスタム識別コードを含むメッセージになりました。これにより、クライアント側でAPIキーの問題であることがすぐに分かります。
- WAFによるブロック
続いて、WAFによるブロックを確認するために、WAFでブロックするように設定したパスにアクセスします。
curl -X GET https://0a0z30pane.execute-api.ap-northeast-1.amazonaws.com/v1/waf-test \
-H "x-api-key: 正しいAPIキー"
結果
{"message":"このリクエストはブロックされています","returnCode":"40302"}
WAFによってブロックされ、専用のカスタムエラーレスポンスが返されました!これにより、ユーザーはリクエストがブロックされたことが理解できます。
カスタムエラーレスポンスの実装により、同じHTTPステータスコード(403)でも異なるエラー原因を明確に区別できるようになりました。
まとめ
API Gatewayのカスタムエラーレスポンスを適切に設計することで、クライアントに理解しやすいメッセージを提供できるようになります。また、AWS CDKを活用すれば、これらの設定をコードとして管理でき、バージョン管理も簡単に行えます。皆さんもぜひ、自身のプロジェクトに合わせたカスタマイズを試してみてください。この記事がAPI Gatewayのカスタムエラーレスポンスを検討されている方の参考になれば幸いです。