Node.jsでAWS Cognito User Pools のアクセストークンを検証する

AWS のページから引用

  1. ユーザープールの JSON Web トークン (JWT) セットをダウンロードして保存します。それら
    https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json で検索で きます。
  2. JWT 形式からトークン文字列をデコードします。
  3. iss クレームを確認します。これは、ユーザープールと一致する必要があります。たとえ
    ば、us-east-1 リージョンで作成されたユーザープールの iss 値が https://cognito-idp.useast-1.amazonaws.com/{userPoolId} であるとします。
  4. token_use クレームを確認します。
  5. JWT トークンヘッダーから kid を取得すると、ステップ 1 で保存された対応する JSON Web キーが
    取得されます。
  6. デコードされた JWT トークンの署名を確認します。
  7. exp クレームを確認し、トークンが期限切れでないことを確認します。
    トークン内でクレームを信頼し、要件に満たすものとしてそれを使用できます。

http://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-with-identity-providers.html#amazon-cognito-identity-user-pools-using-id-and-access-tokens-in-web-api

個人的に5と6がわからなかったので、メモ。

まず1でこんな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"
        }
    ]
}

※値は適当です。

検証の流れ

  1. User pool のログインでもらったアクセストークンをdecodeして、kidを取得する。
  2. 上でダウンロードしたファイルのkidn(modulus)とe(exponent)を取得する。
  3. 冪剰余(nとk)からpem文字列を作成する。
  4. アクセストークンをpemを使って検証する。

ソースコード的にはこんな感じ。
accessTokengetAccessToken().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 PEM from RSA public key
// 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 });  

おしまい

devalon

Read more posts by this author.