CloudFront Functions で JWT を検証する使い方を調べたり考察したメモ
一先ず、現状としては
- JWT 検証するなら鍵情報をコードに埋め込む必要がある
- 3rd party ライブラリは使えないので JWT の検証は自前で実装する必要がある
といった点から、自前で管理する共有鍵を使うようなシンプルな JWT を検証するようなシナリオなら良いものの、外部サービス等で公開されてる鍵を使って JWT を検証するようなシナリオに実戦投入するには、それなりの覚悟が必要そうな印象。
費用やパフォーマンスの面では Lambda@Edge よりか CloudFront Functions が勝ってそうなので、そのへんを重要視する場合には頑張ってみても良いかもしれない。
リリース記事
ブログ記事
公開されているサンプル
JWT を検証するサンプル
ドキュメントに掲載されているサンプル
CloudFront Funrtions にはネットワークアクセス機能が無いため、 JWT 検証に必要な JWK がある場合、それをコード内に埋め込んでおく必要がある・・・
If you need some of the capabilities of Lambda@Edge that are not available with CloudFront Functions, such as network access or a longer execution time, you can still use Lambda@Edge before and after content is cached by CloudFront.
公開されているサンプルでも key をコードに直接埋め込んでいる
function handler(event) {
var request = event.request;
//Secret ket used to verify JWT token.
//Update with your own key.
var key = "LzdWGpAToQ1DqYuzHxE6YOqi7G3X2yvNBot9mCXfx5k";
// If no JWT token, then generate HTTP redirect 401 response.
if(!request.querystring.jwt) {
console.log("Error: No JWT in the querystring");
return response401;
}
var jwtToken = request.querystring.jwt.value;
try{
jwt_decode(jwtToken, key);
}
catch(e) {
console.log(e);
return response401;
}
//Remove the JWT from the query string if valid and return.
delete request.querystring.jwt;
console.log("Valid JWT token");
return request;
}
CloudFront Functions は管理 API が提供されているので、もし JWK をキャッシュして使いたいとか、ローテーションに対応したいとか、そういった要件がある場合には、API 経由でコード内容を更新することで対応できそう。(大変そう)
公開されているサンプルコードを読み解く
crypto
var crypto = require('crypto');
この crypto
は CloudFront Functions の Built-in objects の 1つ
The cryptographic module (crypto) provides standard hashing and hash-based message authentication code (HMAC) helpers. You can load the module using require('crypto'). The module provides the following methods that behave exactly as their Node.js counterparts.
Node.js の crypto と同様に動くとのことなので、 Node.js のドキュメントを参考にすると良さそう
response401
JWT が invalid だった場合に返すレスポンス
//Response when JWT is not valid.
var response401 = {
statusCode: 401,
statusDescription: 'Unauthorized'
};
handler
- メインの処理となるハンドラー
- query string として受け取る jwt を有無をチェック
-
jwt_decode
によって正当な JWT であるか検証- invalid であれば先述のレスポンスを使って 401 を返す
- valid であれば query string から JWT を削除して request を返す
- request を返すと、クライアントのリクエストが続行される
function handler(event) {
var request = event.request;
//Secret ket used to verify JWT token.
//Update with your own key.
var key = "LzdWGpAToQ1DqYuzHxE6YOqi7G3X2yvNBot9mCXfx5k";
// If no JWT token, then generate HTTP redirect 401 response.
if(!request.querystring.jwt) {
console.log("Error: No JWT in the querystring");
return response401;
}
var jwtToken = request.querystring.jwt.value;
try{
jwt_decode(jwtToken, key);
}
catch(e) {
console.log(e);
return response401;
}
//Remove the JWT from the query string if valid and return.
delete request.querystring.jwt;
console.log("Valid JWT token");
return request;
}
curl で動作を試す場合、以下のように指定すれば良い
curl -v "http://xxx.cloudfront.net/?jwt=eyJ..."
jwt_decode
- JWT を decode してる
-
.
で split- header, payload, signature の 3つ に split される
- header, payload は base64 でエンコードされた JSON なので base64 で decode する
-
noVerify
が指定されていなければ JWT の verify を行う- 署名の検証
- 署名メソッドは SHA256
- 署名タイプは HMAC
- JSON に encode した header と payload を
.
で join して後述の_verify
に渡して検証
-
nbf クレームのチェック
- not before の意味で、 JWT が有効となる日時を示す
- これ以前で JWT を処理してはならない
- なんで
payload.nbf*1000
してるんだろう・・・
-
exp クレームのチェック
- expiration time の意味で、 JWT の有効期限を示す
- これより後で JWT を処理してはならない
- なんで
payload.exp*1000
してるんだろう・・・
- 署名の検証
-
function handler(event) {
...
var key = "LzdWGpAToQ1DqYuzHxE6YOqi7G3X2yvNBot9mCXfx5k";
...
var jwtToken = request.querystring.jwt.value;
...
try{
jwt_decode(jwtToken, key);
}
...
}
...
function jwt_decode(token, key, noVerify, algorithm) {
// check token
if (!token) {
throw new Error('No token supplied');
}
// check segments
var segments = token.split('.');
if (segments.length !== 3) {
throw new Error('Not enough or too many segments');
}
// All segment should be base64
var headerSeg = segments[0];
var payloadSeg = segments[1];
var signatureSeg = segments[2];
// base64 decode and parse JSON
var header = JSON.parse(_base64urlDecode(headerSeg));
var payload = JSON.parse(_base64urlDecode(payloadSeg));
if (!noVerify) {
var signingMethod = 'sha256';
var signingType = 'hmac';
// Verify signature. `sign` will return base64 string.
var signingInput = [headerSeg, payloadSeg].join('.');
if (!_verify(signingInput, key, signingMethod, signingType, signatureSeg)) {
throw new Error('Signature verification failed');
}
// Support for nbf and exp claims.
// According to the RFC, they should be in seconds.
if (payload.nbf && Date.now() < payload.nbf*1000) {
throw new Error('Token not yet active');
}
if (payload.exp && Date.now() > payload.exp*1000) {
throw new Error('Token expired');
}
}
return payload;
};
_verify
- JWT の署名を検証している
- HMAC のみ対応
-
_sign
- input: JWT の header + payload
- key: JWT 生成に使用した共有鍵
- method: sha256
-
_constantTimeEquals
- signature: JWT の sigunature
-
_sign(input, key, method)
: - これらが同じなら true を返す
if (!_verify(signingInput, key, signingMethod, signingType, signatureSeg)) {
throw new Error('Signature verification failed');
}
...
function _verify(input, key, method, type, signature) {
if(type === "hmac") {
return _constantTimeEquals(signature, _sign(input, key, method));
}
else {
throw new Error('Algorithm type not recognized');
}
}
_sign
- 共有鍵と SHA256 を使って JWT の header + payload をもとに HMAC を作成
- base64url エンコードしたダイジェストを返す
- これは正常に処理ができれば JWT の signature と同じ値になる
function _sign(input, key, method) {
return crypto.createHmac(method, key).update(input).digest('base64url');
}
crypto.createHmac
hmac.update
hmac.digest
_constantTimeEquals
- サイドチャネルのタイミングを防ぐために一定の時間比較を保証する function
- a と b の長さ、各文字をビッド演算で比較して、同じ値であれば true を返す
//Function to ensure a constant time comparison to prevent
//timing side channels.
function _constantTimeEquals(a, b) {
if (a.length != b.length) {
return false;
}
var xor = 0;
for (var i = 0; i < a.length; i++) {
xor |= (a.charCodeAt(i) ^ b.charCodeAt(i));
}
return 0 === xor;
}
|=
: ビット論理和代入
^
: ビット排他論理和
charCodeAt
SPA のような 1つ の HTML ファイルにアクセスできればよい場合には、クエリストリングに JWT 付与すればいいものの、 SSG した Internal なサイトへのアクセス制御に JWT を使いたい場合には Cookie に JWT を入れて都度検証するようなアーキテクチャが無難そう
これは Lambda@Edge の例がある。
公開されているコード