こんにちは!
API Gateway RestAPI 開発時に、お客様指定の独自の認証サーバーと連携する要件が存在する際、カスタムオーソライザーを構築する事が多いかと思います。
その際、正しいアクセストークンを渡しているのに API Gateway が User is not authorized to access this resource
エラーでアクセスを拒否することがありませんか ? 筆者は小一時間ハマりました…
エラーの原因と対応方法を解説します!
ずばり、API Gateway の Custom Authorizer のキャッシュ設定に原因があります。以下の画像では認可のキャッシュが有効化されていますが、各 API で同一の Cusomt Authorizer を使用している場合、実装の方法次第では直近で認可した API の認可情報を、そのまま別 API の認可にも適用してしまいます。
以下は、クライアントが事前に取得したアクセストークンを使用して GET /users
という API をリクエストしている図です。1回目のリクエストは問題なく無事に疎通するかと思います。
しかし、ここで同じアクセストークンを使用し別 API へ2回目のリクエストした場合はどうでしょうか。Authorizer は、1回目のアクセストークンに対して、1回目のリクエスト API への認可を提供しています。この認可情報を API Gateway がキャッシュしている場合、TTLの時間を超過してキャッシュがクリアーされるまで、同一アクセストークンは1回目のリクエスト API にしか、アクセスできなくなってしまう、ということです。
より具体的に理解するために、Custom Authorizer のソースコードを見てみましょう。
import middy from 'utils/middy';
import { APIGatewayAuthorizerResult, APIGatewayTokenAuthorizerEvent } from 'aws-lambda';
import logger from 'utils/logger';
export const handler = middy.handler(async (event: APIGatewayTokenAuthorizerEvent): Promise<APIGatewayAuthorizerResult> => {
const methodArn = event.methodArn;
try {
return generatePolicy('user', 'Allow', methodArn);
} catch (error) {
logger.error({
...(error as UnauthorizedError),
});
return generatePolicy('user', 'Deny', methodArn);
}
});
export const generatePolicy = (principalId: string, effect: string, resource: string): APIGatewayAuthorizerResult => {
const authResponse: APIGatewayAuthorizerResult = {
principalId: principalId,
policyDocument: {
Version: '2012-10-17',
Statement: [
{
Action: 'execute-api:Invoke',
Effect: effect,
Resource: resource,
},
],
},
};
return authResponse;
};
上記の event.methodArn
には、クライアントのリクエストに対応するAPI Gateway の ARN が入っています。Authorizer が 返却した Allow (アクセスを許可) または Denny (アクセスを拒否) という結果を、API Gateway がキャッシュしているということです。
本 API Gateway に認可を絞った上で、ワイルドカードを使用し Allow または Denny を返却します。
import middy from 'utils/middy';
import { APIGatewayAuthorizerResult, APIGatewayTokenAuthorizerEvent } from 'aws-lambda';
import logger from 'utils/logger';
export const handler = middy.handler(async (event: APIGatewayTokenAuthorizerEvent): Promise<APIGatewayAuthorizerResult> => {
const region = process.env.REGION as string;
const accountId = process.env.ACCOUNT_ID as string;
const apiId = process.env.API_GATEWAY_ID as string;
const stage = process.env.STAGE as string;
const methodArn = `arn:aws:execute-api:${region}:${accountId}:${apiId}/${stage}/*`;
try {
return generatePolicy('user', 'Allow', methodArn);
} catch (error) {
logger.error({
...(error as UnauthorizedError),
});
return generatePolicy('user', 'Deny', methodArn);
}
});
export const generatePolicy = (principalId: string, effect: string, resource: string): APIGatewayAuthorizerResult => {
const authResponse: APIGatewayAuthorizerResult = {
principalId: principalId,
policyDocument: {
Version: '2012-10-17',
Statement: [
{
Action: 'execute-api:Invoke',
Effect: effect,
Resource: resource,
},
],
},
};
return authResponse;
};
上記に対応するために、CloudFormation で Lambda に環境変数を設定しておきましょう。以下は Serverless Framework の例です。
functions:
authorizer:
handler: 'authorizer.handler'
environment:
REGION: '${self:provider.region}'
ACCOUNT_ID: !Sub ${AWS::AccountId}
API_GATEWAY_ID: !Ref ApiGatewayRestApi
STAGE: '${self:provider.stage}'
APIGateway の認可のキャッシュを無効または TTL を0秒にします。尚、TTL に undefined などの無効な値を設定した場合、デフォルトの300秒が設定されてしまいますのでご注意ください。
わたしとしては、キャッシュを無効化すると Lambda を API リクエストの都度実行することになり、パフォーマンス面の懸念も出てきますので、できる限り前者のロジックで対応したほうが賢明です。スロットリングのリスクも抱えますので、出来る限り前者のロジックで対応しましょう。
本記事の方法でエラーは解消されますが、本質的にセキュリティを重んじるなら何かしらカスタムロジックが更に必要になると感じます。わたしたちは通常、API Gateway の Custom Authorizier が持つ認可処理と、各 API の持つ認可処理それぞれの役割を分けて考えることで、セキュリティの向上を図っています。各 API の持つ認可には、JSON Shemeによるカスタムバリデーションなども検討可能ですので、ぜひこちらもご覧ください。
サーバーレスの開発はお気軽にお問い合わせください。
スモールスタート開発支援、サーバーレス・NoSQLのことなら
ラーゲイトまでご相談ください
低コスト、サーバーレスの
モダナイズ開発をご検討なら
下請け対応可能
Sler企業様からの依頼も歓迎