LoginSignup
57

More than 5 years have passed since last update.

WebCrypto APIでJSON Web Tokenの検証を試してみる

Last updated at Posted at 2015-05-26

Web Cryptography (WebCrypto) APIを使うと、JavaScriptで暗号化や復号、署名やその検証等の処理を実行することができるようになります。特に、CryptoJSでも扱われていないRSA等も扱えるのが便利かもしれません。

Chrome 37以降、Firefox 34以降で利用できるようですので、まず試してみました。なお、Safari 8でもwebkitプレフィクス付きで使えるようですが(window.crypto.webkitSubtle)、仕様が異なるのかこちらでは動作確認が取れなかったため、今回はChromeとFirefoxのみに限定することにします。

今回は、GoogleのOpenID Connect認証で得られるJWTを例にして試してみます。

JSON Web Token (JWT)

例えば、GoogleアカウントでOpenID Connectを使った認証を自分のWebサイトに実装すると、ログイン成功時にリダイレクトURIに遷移する際、JWTを格納したパラメータid_tokenがURLクエリパラメータの一つとして付与されます。

具体的には、次のような内容になります:

[ヘッダ].[クレーム].[シグネチャ]

[ヘッダ]は、署名に使われた鍵の識別子をJSONで記述したものをBase64 URLエンコードしたものとなります。Base64 URLデコードして得られるJSON文字列の内容は、例えば次のようになります:

{"alg":"RS256","kid":"7158d85c857f368bf3a448b22720a3a55b073447"}

[クレーム]は、トークンで検証するIDプロバイダ名やユーザ名等の情報をJSONで記述したものをBase64 URLエンコードしたものになります。

ここで、[ヘッダ]kidで示されるIDプロバイダ(ここではGoogle)の秘密鍵を用いて、ペイロード[ヘッダ].[クレーム]に署名して得られるシグネチャをBase64 URLエンコードしたものが、上記の[シグネチャ]となります。

Google OpenID Connectの場合、署名を検証するために必要となる公開鍵(JWK; JSON Web Key)がWebサイト上で公開されています。一定時間ごとに更新されるため、署名の検証時にはその都度JWKの内容をWebサイトから取得したほうがよいでしょう。

(参考): http://christina04.hatenablog.com/entry/2015/01/27/131259

まず、鍵を取得

それでは、順を追ってJWTの検証を試してみます。

簡単のため、今回はJWTの内容を予め変数に入れておくことにします。
まず、GoogleのWebサイトよりJWKを取得し、JWTのヘッダで指定されているkidの鍵を選択します。

sample.js
var token = '[JWTの内容]';

function base64UrlDecode(t) {
  return atob(t.replace(/\-/g, '+').replace(/_/g, '/'));
}

function getJSON(e) { return e.json(); }

function getJWK(e) {
  var key;
  var k = JSON.parse(base64UrlDecode(token.split('.')[0]));
  for(var i of e.keys) {
    if(i.kid == k.kid) {
      key = i;
      // Base64のpaddingが残っていると、Chromeではcrypto.subtle.importKeyでエラー
      key.n = key.n.replace(/=+$/, '');
    }
  }
  if(key)
    getCryptoKey(key);
}

fetch('https://www.googleapis.com/oauth2/v2/certs').then(getJSON).then(getJWK);

次に、得られたJWKをインポートして、WebCryptoで利用するCryptoKeyオブジェクトを取得します。

sample.js
function getCryptoKey(key) {
  crypto.subtle.importKey('jwk', key, {
    name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-256' }
  }, true, ['verify']).then(verifyJWT);
}

上記の例では、関数verifyJWT()の引数として、CryptoKeyオブジェクトとして格納されたGoogleのRSA公開鍵が渡されることになります。

crypto.subtle.importKey()の引数ですが、第1引数はインポート元の鍵のデータフォーマットを指定します。JWKであれば、'jwk'を指定し、第2引数でJWKのJSONオブジェクトをそのまま渡せばよいことになります。

第3引数で、暗号化や署名の方式を指定しますが、JWTのヘッダでRS256が指定されているため、WebCryptoの場合は、署名の方式として'RSASSA-PKCS1-v1_5'(長いですが)、ハッシュの方式として'SHA-256'を指定します。

第4引数は、鍵をエクスポートできるようにしてよいかどうかを指定します。

第5引数では、鍵の用途を一つないし複数、配列で指定します。暗号化なら'encrypt'、復号なら'decrypt'、署名なら'sign'、署名の検証なら'verify'、などが指定可能です。

得られた鍵で署名を検証

これまでの手順で得られた公開鍵を用いて、JWTの内容が真正なものかどうかを検証します。

sample.js
function toBufferSource(t) {
  var r = new Uint8Array(t.length);
  for(var i = 0 ; i < s.length ; i++)
    r[i] = s.charCodeAt(i);
  return r;
}

function verifyJWT(cryptoKey) {
  var t = token.split('.');
  var signature = toBufferSource(base64UrlDecode(t[2]));
  var payload = toBufferSource(t[0] + '.' + t[1]);
  crypto.subtle.verify({
    name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-256' }
  }, cryptoKey, signature, payload).then(function(result) {
    console.log(result ? '検証成功' : '検証失敗');
  });
}

crypto.subtle.verify()の引数ですが、第1引数はcrypto.subtle.importKey()の第3引数と同じです。第2引数には検証に用いる公開鍵、すなわちcrypto.subtle.importKey()で取得したCryptoKeyオブジェクトを渡します。

さらに、第3引数には署名をBase64 URLデコードしてUint8Arrayに格納したもの、第4引数には検証したいJWTデータ([ヘッダ].[クレーム])をUint8Arrayに格納したものを渡すと、Promiseで検証結果をbooleanで取得できます。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
57