Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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ファイルをダウンロードする

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"
        }
    ]
}

※値は適当です。

検証の流れ

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

おしまい

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away