こんにちは!インフラやクラウドのセキュリティの世界へようこそ。
日々、新しいシステムやAPIを作っていると、「誰がこのデータにアクセスしていいのか」「偽物のユーザーがこっそり侵入してこないか」といった問題に頭を悩ませますよね。
今回は、APIを守るための現代の必須アイテム「API GatewayにおけるJWT検証と認可プロセスのオフロード」について、身近な防犯の仕組みに例えながら、一歩ずつ優しく紐解いていきましょう!
—
1. 家の鍵と合鍵(JWT)に例えるセキュリティの基本
皆さんが住んでいるマンションや家を想像してみてください。
毎回、玄関のドアを開けるときに、管理人に「私はこの部屋の住人です」とパスポートを見せるのは面倒ですし、時間もかかりますよね。
そこで、一度ログイン(本人確認)を済ませた人には、「この人は○号室の住人で、今月の家賃も払っていて、ゴミ捨て場には入れるけれど、管理人室には入れない権限があります」という特製のハンコが押された『入場パス(=JWT:JSON Web Token)』を渡します。
APIの世界でも同じです。
ユーザーが最初にログインしたとき、サーバーは「このユーザーのIDは何番で、どんな権限があるか」という情報を書き込んだデジタルな入場パス(JWT)を発行します。ユーザーは、その後のAPIリクエスト(「データをちょうだい!」というお願い)のたびに、この入場パスをポケットから取り出してAPIに見せるわけです。
攻撃者はどうやってこのパスを偽造するのか?
もし、この入場パスがただの紙切れ(暗号化されていない、またはサインがないデータ)だったらどうでしょう?
悪意ある攻撃者は、コンビニのコピー機やパソコンで勝手に「私は管理人です!全室の鍵を開けられます!」と書いた偽の入場パスを作り、APIに突撃してくるかもしれません。これが、不正アクセスのメカニズムです。
だからこそ、「このパスは本物のシステムが発行したものか?」「偽造されていないか?」を厳しくチェックする仕組み(署名検証)が絶対に必要になるのです。
—
2. なぜ「オフロード(丸投げ)」が必要なの?
さて、この入場パスのチェック、誰がやるべきでしょうか?
これまでは、APIを動かしている裏側のプログラム(バックエンドのLambda関数やサーバー)が、届いたパスを一つひとつ受け取って、「うーん、このハンコは本物かな?期限は切れてないかな?」と一生懸命チェックしていました。
でも、考えてみてください。
人気のあるアプリだと、1秒間に何千人もの人が「データをちょうだい!」とやってきます。そのたびに、裏側のプログラムが全員分のパスの真贋判定(しんがんはんてい)をしていたら、プログラムはパンクしてしまいますよね。
そこで登場するのが、「API Gateway(門番)」での認可プロセスのオフロード(丸投げ)です!
門番にチェックを完全に任せよう
マンションに例えるなら、屈強なセキュリティガードマン(Lambda Authorizer)を一番手前のエントランスに立たせるイメージです。
怪しいパス(偽造されたJWT、有効期限切れのJWT)を持っている人は、エントランスの段階で「おっと、そこから先には通せません!」と追い返します。
本物のパスを持っている人だけが、奥にいる家主(バックエンドのAPI)のところに通されます。
これによって、裏側のプログラムは面倒なパスのチェックをしなくて済み、本来の仕事(美味しい料理を振る舞うような、ビジネスロジックの処理)に集中できるようになるというわけです。
—
3. Lambda Authorizerによる実装パターンを覗いてみよう
それでは、実際にAWSなどのクラウド環境で、この「門番(Lambda Authorizer)」をどうやって動かすのか、具体的なコード例を見てみましょう。
今回は、Node.jsを使って「送られてきたJWTのサインが正しいか」「期限は切れていないか」をチェックし、さらに「特定のスコープ(権限)を持っているか」まで判定するシンプルな門番のコードをご紹介します。
const jwt = require('jsonwebtoken');
// サーバーがあらかじめ持っている秘密の合言葉(または公開鍵)
// ※実際の現場では環境変数から安全に呼び出します
const SECRET_KEY = process.env.JWT_SECRET_KEY || 'super-secret-key-for-demo';
exports.handler = async (event) => {
try {
// 1. リクエストのヘッダーから「Authorization」を取り出す
// 例: "Bearer eyJhbGciOiJIUzI1NiIsInR..."
const authHeader = event.authorizationToken;
if (!authHeader) {
throw new Error('認可トークンが見つかりません');
}
// "Bearer " という文字を取り除いて、純粋なJWTだけを取り出す
const token = authHeader.replace('Bearer ', '');
// 2. JWTの署名検証と有効期限(exp)の自動チェック
// ここで偽造されていればエラーが発生して弾かれます
const decoded = jwt.verify(token, SECRET_KEY);
// 3. スコープベースの認可制御(権限のチェック)
// 例: このAPIは "read:reports" という権限を持つユーザーだけに通したい
const userScopes = decoded.scopes || []; // パスに書かれている権限のリスト
const requiredScope = 'read:reports';
if (!userScopes.includes(requiredScope)) {
// 権限が足りない場合はアクセス拒否
console.log(`ユーザー ${decoded.sub} は必要な権限を持っていません。`);
return generatePolicy(decoded.sub, 'Deny', event.methodArn);
}
// 4. チェッククリア!奥のAPIへの通行を許可するポリシーを返す
console.log(`ユーザー ${decoded.sub} のアクセスを許可します。`);
return generatePolicy(decoded.sub, 'Allow', event.methodArn);
} catch (error) {
console.error('認証エラー:', error.message);
// エラー(期限切れ、署名不一致など)があった場合は一律で拒否
// ※セキュリティの鉄則:攻撃者にヒントを与えないため「なぜダメか」は詳細に教えない
return generatePolicy('user', 'Deny', event.methodArn);
}
};
// API Gatewayに「通して良いか・ダメか」を伝えるための権限ポリシーを組み立てる関数
function generatePolicy(principalId, effect, resource) {
const authResponse = {};
authResponse.principalId = principalId;
if (effect && resource) {
const policyDocument = {};
policyDocument.Version = '2012-10-17';
policyDocument.Statement = [];
const statementOne = {};
statementOne.Action = 'execute-api:Invoke';
statementOne.Effect = effect;
statementOne.Resource = resource;
policyDocument.Statement.push(statementOne);
authResponse.policyDocument = policyDocument;
}
return authResponse;
}
コードのポイント
jwt.verify()の偉大さ: この1行だけで、パスの改ざんがないか、有効期限が切れていないかを暗号学的にしっかりと検証してくれます。- セキュリティの鉄則(エラーの隠蔽): 検証に失敗したとき、コード側で「期限切れです」や「署名が違います」と親切に教えすぎると、攻撃者にヒントを与えてしまいます。基本的には「Deny(拒否)」だけを返すようにするのがセキュアな設計のコツです。
—
4. 現場で役立つ!さらに安全性を高めるためのTips
無事に門番を置くことができたら、次は一歩進んだ実務レベルの注意点を確認しておきましょう。
1. 公開鍵(JWKS)のキャッシュを活用する
認証局(Auth0やCognito、Firebase Authなど)から公開鍵を使って検証する場合、毎回ネット経由で鍵を取りに行くとAPIのレスポンスが遅くなります。メモリ上に鍵をキャッシュ(一時保存)する仕組みを必ず組み込みましょう。
2. ログの取り扱いに注意する
デバッグのために、受け取ったJWTそのものをそのままサーバーのログ(CloudWatch Logsなど)に出力してしまうのは絶対にNGです。ログを見た人が誰でもそのトークンを使ってなりすましができてしまいます。ログにはユーザーID程度にとどめ、トークンは出力しないようにしましょう。
—
まとめ
今回は、API GatewayにおけるJWT検証と認可プロセスのオフロードについて、防犯の仕組みに例えて解説しました。
- JWT(入場パス)でユーザーの身元と権限を安全に持ち運ぶ
- Lambda Authorizer(門番)にパスのチェックを丸投げ(オフロード)して、裏側のシステムを守る
- 権限(スコープ)が足りない人や偽物のパスを持っている人は、エントランスの段階でしっかりシャットアウトする
セキュリティ対策というと難しく聞こえがちですが、「誰がどこに入れて、どこから先は入れてはいけないのか」という基本のルールを整理していくと、パズルのようにスッキリと組み立てることができます。
一歩ずつ、安全で強いシステムを作っていきましょう!それではまた次の記事でお会いしましょう。
コメント