AWS のページから引用
- ユーザープールの JSON Web トークン (JWT) セットをダウンロードして保存します。それら
をhttps://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json で検索で
きます。 - JWT 形式からトークン文字列をデコードします。
- iss クレームを確認します。これは、ユーザープールと一致する必要があります。たとえ
ば、us-east-1 リージョンで作成されたユーザープールの iss 値が https://cognito-idp.useast-1.amazonaws.com/{userPoolId} であるとします。 - token_use クレームを確認します。
- JWT トークンヘッダーから kid を取得すると、ステップ 1 で保存された対応する JSON Web キーが
取得されます。 - デコードされた JWT トークンの署名を確認します。
- exp クレームを確認し、トークンが期限切れでないことを確認します。
トークン内でクレームを信頼し、要件に満たすものとしてそれを使用できます。
個人的に5と6がわからなかったので、メモ。
まず1でこんなjsonファイルをダウンロードする
jwks.json
{
"keys": [
{
"alg": "RS256",
"e": "AAAA",
"kid": "5XeKL5ebJ5VD5RmIaKqYQHZ9+iEtJXzovVSqvYl0kEk=",
"kty": "RSA",
"n": "kbOKCIHPzbnTY9BxmedkQroHsp74X-y-I2ye_nRxzT8jBv6z81WTC1iu_JJypVaw1hw6-zqp4QCjrtqvoWACPkNJfiDoTfw0HEoA3nYYKGyiHtQOpD994bNLSS4jHX3YZyYFsFv26Mj0edlWfjExr0dSX0uuSIEBcuuG0wPm3pflZaQKDuyPQ7RDzyeYz8Y25h_Zpdy0DJKIhdmU4V5BrhMk7oi_wD4VdyqdNWLuhSXUXbRf0H38jYVJoigW5JaDJpqwgkWgiseRRoAxdlInoqxXhecjk1Y03OD-4EKCxWE2oyw7YMWLe73UdHAJZI5JwBtCL1IIjiiHQIoH_Ji_Yw",
"use": "sig"
},
{
"alg": "RS256",
"e": "AAAA",
"kid": "e32pkKHzpmt9V89+cQUTDBRA2+8QEXMF2zkz4WkMxgM=",
"kty": "RSA",
"n": "-EOIwnvIrAdHHmJT-YYPLeBvveFh4oYQtl2vhTcOuAAplzCXaYKliH82vGEu5HpaUmj0PJUCEj6pJOMhTvjR509IQPStcyiODEby911gKEf2a-XzawPDGfathM-k_m0FUgvCypfHeBNs-hUgDTVPzDk4jgo8cHu2cIiMqxUgeAn5_l71Hh3WtPg5t2A8Kucz0TH6D3B_-MTZ7r4Vx8X2cg_B7MxmNlnRV55_thMcpxCtkShrRqFTnccMiDC3EYvh7-u0g4i1bvydXEx-_lrQW29CfVwK5yU2NauEBh0F1BGULWLxWAytwzDyuRHReupg4A9dfSqSvB5ckLV8qPmvjQ",
"use": "sig"
}
]
}
※値は適当です。
検証の流れ
- User pool のログインでもらったアクセストークンをdecodeして、
kid
を取得する。 - 上でダウンロードしたファイルの
kid
のn
(modulus)とe
(exponent)を取得する。 - 冪剰余(nとk)からpem文字列を作成する。
- アクセストークンをpemを使って検証する。
ソースコード的にはこんな感じ。
※accessToken
のgetAccessToken().getJwtToken()
で取れた値。
※RSA公開鍵を作るライブラリが見つからなかったので、他のサイトで同じことやってるものをつかってる。
var jwt = require('jsonwebtoken');
var jwktjson = require('./jwkt.json');
var accessToken = 'eyJraWQiOiIyMzJwa0tIenBtdDlWODkrY1FVVERCUkEyKzhRRVhNRjJ6a3o0V2tNeGdNPSIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiJjNGM3YWQ1MS01Y2I1LTQ5ZTctODJjZi0wMGQ1YTExMzI4YmYiLCJ0b2tlbl91c2UiOiJhY2Nlc3MiLCJzY29wZSI6ImF3cy5jb2duaXRvLnNpZ25pbi51c2VyLmFkbWluIiwiaXNzIjoiaHR0cHM6XC9cL2NvZ25pdG8taWRwLmFwLW5vcnRoZWFzdC0xLmFtYXpvbmF3cy5jb21cL2FwLW5vcnRoZWFzdC0xXzN4d1RGaVZPNSIsImV4cCI6MTQ5MDk0MjY3NCwiaWF0IjoxNDkwOTM5MDc0LCJqdGkiOiJlYzVhMGExYi00MDg2LTRkNmYtODdhMS00NDUzODdkZjZmOGMiLCJjbGllbnRfaWQiOiI1cW5hOGhxZzVhaml1bjljZzNtMXRpNWdrayIsInVzZXJuYW1lIjoiaGF5YXRvLnNoaW1va2F3YUBuZXh0cmVtZXIuY29tIn0.bgRbZdbvMrmpSE-iGz6v9bSbcTafDaPlOq1TBgxW2W1-F70JQv_Nm_-6QOTtIMP9BtF_wK0UR8L8cWEIUbqou3b5f63yNDhn0J23C7kG1xASdJptnJZ1BG1nIjbAIuuySItef3I_0I089r5bYQYXRLOUdXUhXhpjd8v3hhkArHEAbE0kVX-jWpPGtFm-Ar8peS1m_8TSWg0cUEgFSORyZ789uOv82PovrWhMq9KBEmQtKyWwZq3co5X57TdvMgCq_XXVQa8Ox3rGP1-RzMzDzUEtVzdkBjSkqtvy9yYGh9lfMgvMlK7ljirQUjBp4bWEu7m5Afdr_qWuY7_fG8t6EA';
var decoded = jwt.decode(accessToken, { complete: true });
if (!decoded){
console.error('accessToken is not JSON Web Token.');
process.exit(1);
}
var kid = decoded.header.kid;
var n; // modulus
var e; // exponent
for (var i = 0; i < jwktjson.keys.length; i++) {
var row = jwktjson.keys[i];
if (kid == row['kid']) {
n = row['n'];
e = row['e'];
break;
}
}
var pem = rsaPublicKeyPem(n, e);
try {
var verified = jwt.verify(accessToken, pem, { algorithms: ['RS256'] });
console.log(verified);
} catch (err) {
console.error(err);
}
// Create public key PEM from Base64 modulus and exponent
// http://stackoverflow.com/questions/18835132/xml-to-pem-in-node-js
function rsaPublicKeyPem(modulus_b64, exponent_b64) {
function prepadSigned(hexStr) {
var msb = hexStr[0]
if (
(msb >= '8' && msb <= '9') ||
(msb >= 'a' && msb <= 'f') ||
(msb >= 'A' && msb <= 'F')) {
return '00' + hexStr;
} else {
return hexStr;
}
}
function toHex(number) {
var nstr = number.toString(16)
if (nstr.length % 2 == 0) return nstr
return '0' + nstr
}
// encode ASN.1 DER length field
// if <=127, short form
// if >=128, long form
function encodeLengthHex(n) {
if (n <= 127) return toHex(n)
else {
n_hex = toHex(n)
length_of_length_byte = 128 + n_hex.length / 2 // 0x80+numbytes
return toHex(length_of_length_byte) + n_hex
}
}
var modulus = new Buffer(modulus_b64, 'base64');
var exponent = new Buffer(exponent_b64, 'base64');
var modulus_hex = modulus.toString('hex')
var exponent_hex = exponent.toString('hex')
modulus_hex = prepadSigned(modulus_hex)
exponent_hex = prepadSigned(exponent_hex)
var modlen = modulus_hex.length / 2
var explen = exponent_hex.length / 2
var encoded_modlen = encodeLengthHex(modlen)
var encoded_explen = encodeLengthHex(explen)
var encoded_pubkey = '30' +
encodeLengthHex(
modlen +
explen +
encoded_modlen.length / 2 +
encoded_explen.length / 2 + 2
) +
'02' + encoded_modlen + modulus_hex +
'02' + encoded_explen + exponent_hex;
var seq2 =
'30 0d ' +
'06 09 2a 86 48 86 f7 0d 01 01 01' +
'05 00 ' +
'03' + encodeLengthHex(encoded_pubkey.length / 2 + 1) +
'00' + encoded_pubkey;
seq2 = seq2.replace(/ /g, '');
var der_hex = '30' + encodeLengthHex(seq2.length / 2) + seq2;
der_hex = der_hex.replace(/ /g, '');
var der = new Buffer(der_hex, 'hex');
var der_b64 = der.toString('base64');
var pem = '-----BEGIN PUBLIC KEY-----\n'
+ der_b64.match(/.{1,64}/g).join('\n')
+ '\n-----END PUBLIC KEY-----\n';
return pem
}
accessToken
は1時間で切れるので注意。
以下のように無視もできるけども。
jwt.verify(accessToken, pem, { algorithms: ['RS256'], ignoreExpiration: true });
おしまい