Closed9

CloudFront Functions で JWT を検証する使い方を調べたり考察したメモ

ピン留めされたアイテム
noid11noid11

一先ず、現状としては

  • JWT 検証するなら鍵情報をコードに埋め込む必要がある
  • 3rd party ライブラリは使えないので JWT の検証は自前で実装する必要がある

といった点から、自前で管理する共有鍵を使うようなシンプルな JWT を検証するようなシナリオなら良いものの、外部サービス等で公開されてる鍵を使って JWT を検証するようなシナリオに実戦投入するには、それなりの覚悟が必要そうな印象。
費用やパフォーマンスの面では Lambda@Edge よりか CloudFront Functions が勝ってそうなので、そのへんを重要視する場合には頑張ってみても良いかもしれない。

noid11noid11

CloudFront Funrtions にはネットワークアクセス機能が無いため、 JWT 検証に必要な JWK がある場合、それをコード内に埋め込んでおく必要がある・・・

https://aws.amazon.com/jp/blogs/aws/introducing-cloudfront-functions-run-your-code-at-the-edge-with-low-latency-at-any-scale/

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 をコードに直接埋め込んでいる

https://github.com/aws-samples/amazon-cloudfront-functions/blob/8af1b1b64d27054b6727cf36ca38b34f97dc6ccf/verify-jwt/index.js#L89-L91

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;
}

noid11noid11

公開されているサンプルコードを読み解く

https://github.com/aws-samples/amazon-cloudfront-functions/blob/8af1b1b64d27054b6727cf36ca38b34f97dc6ccf/verify-jwt/index.js

crypto

var crypto = require('crypto');

この crypto は CloudFront Functions の Built-in objects の 1つ

https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/functions-javascript-runtime-features.html#writing-functions-javascript-features-builtin-modules-crypto

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 のドキュメントを参考にすると良さそう

https://nodejs.org/api/crypto.html

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

https://nodejs.org/api/crypto.html#crypto_crypto_createhmac_algorithm_key_options

hmac.update

https://nodejs.org/api/crypto.html#crypto_hmac_update_data_inputencoding

hmac.digest

https://nodejs.org/api/crypto.html#crypto_hmac_digest_encoding

_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;
}

|=: ビット論理和代入

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Bitwise_OR_assignment

^: ビット排他論理和

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Bitwise_XOR

charCodeAt

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/charCodeAt

noid11noid11

SPA のような 1つ の HTML ファイルにアクセスできればよい場合には、クエリストリングに JWT 付与すればいいものの、 SSG した Internal なサイトへのアクセス制御に JWT を使いたい場合には Cookie に JWT を入れて都度検証するようなアーキテクチャが無難そう

これは Lambda@Edge の例がある。
https://aws.amazon.com/jp/blogs/networking-and-content-delivery/authorizationedge-using-cookies-protect-your-amazon-cloudfront-content-from-being-downloaded-by-unauthenticated-users/

公開されているコード
https://github.com/aws-samples/cloudfront-authorization-at-edge

このスクラップは2021/05/05にクローズされました