Carpe Diem

備忘録。https://github.com/jun06t

OpenID Connect の署名検証

概要

OpenID Connectで必須なJWTの検証方法です。
以前書いた「Node.jsでOpenID Connect認証」を前提としています。

検証方法は主に2つあります。

  1. Googleの検証用エンドポイントを使う
  2. 公開鍵を使い自分で検証する

公開鍵の方はさらに2つやり方があるので、順に紹介します。

環境

  • Nodejs 0.10.22

事前準備(ID Tokenを取得)

https://github.com/jaredhanson/passport-openidconnect/blob/master/lib/strategy.js#L104

ここで作者がconsole.logを出しているので、ターミナルで出力を見るとJWT形式のID Tokenが分かります。
私の場合以下の様なID Tokenが取得出来ました。※個人情報のため値をいじってます

{ access_token: 'ya29.BwEd7bBEGsXCsuuSvvjYZCqgmXzLy1ho69P0g_kVjR_a_9e6RMlsS4Wl-o17rvA3SfsEkpvnVnXpg',
  token_type: 'Bearer',
  expires_in: 3600,
  id_token: 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjEZjgyOTViMjM3MGRkODhkZTRjMjJhZWQ4NWNhYmY1MmQyNzU3ZjUifQ.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTAyOTUyOTA4OTYzOTU1NzQwMjc0IiwiYXpwIjoiNjk4MTAwMDA3MjQyLTRqcDQxZzltYzUwdIwa2syMmwxMTNhbnBnYWduMGNpLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiYXRfaGFzaCI6IlB5QUFtYzBWN0lzMlZKamdmQ2lVYXciLCJhdWQiOiI2OTgxMDAwMDcyNDItNGpwNDFnOW1jNTB2cjBrazIybDExM2FucGdhZ24Y2kuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJpYXQiOjE0MjIyODE3MjUsImV4cCI6MTQyMjI4NTYyNX0.sG65mFYE-zuGlAYOh0e1E5qmhKGhR_-wmJsMEYNaBtGqBIi-czbYVJVZRPqraKxUST1-SCdWb3VcQvhs4gIXA2x5w8bP7yOul-_aXx8kcEJp2NnzaTgdBQ7k0oE3vAaKE_8Kg3JxGc0_0apq9aJ9cyGHwrHbazs-bgGqqCKqmQ' }

検証方法①:Googleの検証用エンドポイントを利用する

https://www.googleapis.com/oauth2/v1/tokeninfo?id_token=

のクエリパラメータに先ほどのID Tokenを付けてリクエストを投げると結果を返してくれます。

間違ったID Tokenだと以下の様な結果になります。

{
 "error": "invalid_token",
 "error_description": "Invalid Value"
}

正しいID Tokenだと以下の様なレスポンスが返ります。

{
 "issuer": "accounts.google.com",
 "issued_to": "my_service_client_id",
 "audience": "my_service_client_id",
 "user_id": "user_unique_id",
 "expires_in": 3561,
 "issued_at": 1422281725
}

確認する項目は以下です。

  • issuergoogleであること
  • issued_toaudienceは自分のサービスのclientIdと一致していること

これが正しければ、このID Tokenは自分のサービス用に発行されてたものであると確証できます。

検証方法②:IdP(Google)の提供している公開鍵を使って自分で検証する

以下のライブラリを使って検証します。

$ npm install jsonwebtoken

このライブラリで検証するために必要なのは以下の2つです。

  • IDToken
  • IdPの公開鍵

後者の公開鍵ですが、OpenIDConnectを提供しているIdPは主に以下の2つの方法で公開鍵を提供しています。ちなみにGoogleの場合これら公開鍵は1日に1回変わるらしいです。

  • JWK(JSON Web Key)
  • X.509証明書

今回はそれぞれのやり方を紹介します。

公開鍵①:JWK(JSON Web Key)を使う

JWKの取得

Googleは以下のエンドポイントでJWK形式の公開鍵を公開しています。

https://www.googleapis.com/oauth2/v3/certs

アクセスすると以下の様なレスポンスが返ります。

{
 "keys": [
  {
   "kty": "RSA",
   "alg": "RS256",
   "use": "sig",
   "kid": "16f8295b2370dd88de4c22aed85cabf52d2757f5",
   "n": "tL3TtFEjtVIlFrMsyRncvS6ZLDRr6PejeQv7hx1k-oX0599OTYA4FQE8YYX4z95_NaQXx833DPay7KVzw751kHJz9eiSYyZmYFMM786E-PspFvdJMhU2ZCLgxLUXZ_Gq7ORgxHkJHcBWR8HstjI3zpWAOhfqg8YvSnMeOStQ1Ns=",
   "e": "AQAB"
  },
  {
   "kty": "RSA",
   "alg": "RS256",
   "use": "sig",
   "kid": "8472c6590b1778fe529c1bd3a8f181cc2af4b200",
   "n": "rIVm3h1WGbvKjmvzrpwPFeyAWIeP3W87z-C9k0YarePIF0Y77KgaMB83cVv5Hp85Che-Z_nb_y0kBhrOha4_q_6gFEOhyz8PUZSzdY2zkhX8Dci-vic9HulL5cFWjDGPXwekHLm_EmXkPkKu7-6nbkxmwcVQMGX2lEeawCqqNmk=",
   "e": "AQAB"
  }
 ]
}

この中のneを使います。それぞれは

  • n: modulus
  • e: exponent

として、これらを用いてRSA署名検証に使われる公開鍵を取り出すことができます。

JWKから公開鍵を生成する

こちらもライブラリを作っている方がいらっしゃるので利用させて頂きます。
まず試しにどうやって公開鍵を生成するか使ってみます。

$ npm install rsa-pem-from-mod-exp

publicKey.jsとして以下のファイルを作ります。

//getPem = function(modulus_base);
var getPem = require('rsa-pem-from-mod-exp');

//modulus should be a base64/base64Url string
var modulus =  "tL3TtFEjtVIlFrMsyRncvS6ZLDRr6PejeQv7hx1k-oX0599OTYA4FQE8YYX4z95_NaQXx833DPay7KVzw751kHJz9eiSYyZmYFMM786E-PspFvdJMhU2ZCLgxLUXZ_Gq7ORgxHkJHcBWR8HstjI3zpWAOhfqg8YvSnMeOStQ1Ns=";

//exponent should be base64/base64url
var exponent = "AQAB";

var pem = getPem(modulus, exponent);

console.log(pem);

こんな感じにすると以下のように出力されます。

-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBALS907RRI7VSJRazLMkZ3L0umSw0a+j3o3kL+4cdZPqF9OffTk2AOBUB
PGGF+M/efzWkF8fN9wz2suylc8O+dZByc/XokmMmZmBTDO/OhPj7KRb3STIVNmQi
4MS1F2fxquzkYMR5CR3AVkfB7LYyN86VgDoX6oPGL0pzHjkrUNTbAgMBAAE=
-----END RSA PUBLIC KEY-----

JWKでID Tokenを検証する

公開鍵の作り方が分かったので、このやり方で最初に挙げたJWT検証ライブラリjsonwebtokenで利用します。

test.jsとして以下のファイルを作成します。

var jwt = require('jsonwebtoken');
var getPem = require('rsa-pem-from-mod-exp');

var token = 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjEZjgyOTViMjM3MGRkODhkZTRjMjJhZWQ4NWNhYmY1MmQyNzU3ZjUifQ.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTAyOTUyOTA4OTYzOTU1NzQwMjc0IiwiYXpwIjoiNjk4MTAwMDA3MjQyLTRqcDQxZzltYzUwdIwa2syMmwxMTNhbnBnYWduMGNpLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiYXRfaGFzaCI6IlB5QUFtYzBWN0lzMlZKamdmQ2lVYXciLCJhdWQiOiI2OTgxMDAwMDcyNDItNGpwNDFnOW1jNTB2cjBrazIybDExM2FucGdhZ24Y2kuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJpYXQiOjE0MjIyODE3MjUsImV4cCI6MTQyMjI4NTYyNX0.sG65mFYE-zuGlAYOh0e1E5qmhKGhR_-wmJsMEYNaBtGqBIi-czbYVJVZRPqraKxUST1-SCdWb3VcQvhs4gIXA2x5w8bP7yOul-_aXx8kcEJp2NnzaTgdBQ7k0oE3vAaKE_8Kg3JxGc0_0apq9aJ9cyGHwrHbazs-bgGqqCKqmQ';

var modulus =  "tL3TtFEjtVIlFrMsyRncvS6ZLDRr6PejeQv7hx1k-oX0599OTYA4FQE8YYX4z95_NaQXx833DPay7KVzw751kHJz9eiSYyZmYFMM786E-PspFvdJMhU2ZCLgxLUXZ_Gq7ORgxHkJHcBWR8HstjI3zpWAOhfqg8YvSnMeOStQ1Ns=";

var exponent = "AQAB";

var cert = getPem(modulus, exponent);

jwt.verify(token, cert, function(err, decoded) {
  console.log('error: ', err)
  console.log('result: ', decoded)
});

実行します。

$ node test.js

すると以下の様な結果となります。

error:  null
result: {
    "iss": "accounts.google.com",
    "sub": "user_unique_id",
    "azp": "my_service_client_id",
    "at_hash": "PyAAmc0V7Is2VJjgfCiUaw",
    "aud": "my_service_client_id",
    "iat": 1422281725,
    "exp": 1422285625
}

Googleに検証してもらった時と同じく以下の項目をチェックします。

  • issgoogleであること
  • audは自分のサービスのclientIdと一致していること

大丈夫そうでしたら正しいID Tokenということになります。

公開鍵①:X.509証明書を使う

証明書の取得

GoogleはX.509形式の証明書も公開しています。

https://www.googleapis.com/oauth2/v1/certs

\nの改行記号を普通の改行にして、public.pemとして保存します。

-----BEGIN CERTIFICATE-----
MIICITCCAYqgAwIBAgIIcAilVKRkTx4wDQYJKoZIhvcNAQEFBQAwNjE0MDIGA1UE
AxMrZmVkZXJhdGVkLXNpZ25vbi5zeXN0ZW0uZ3NlcnZpY2VhY2NvdW50LmNvbTAe
Fw0xNTAxMjUyMDU4MzRaFw0xNTAxMjcwOTU4MzRaMDYxNDAyBgNVBAMTK2ZlZGVy
YXRlZC1zaWdub24uc3lzdGVtLmdzZXJ2aWNlYWNjb3VudC5jb20wgZ8wDQYJKoZI
hvcNAQEBBQADgY0AMIGJAoGBALS907RRI7VSJRazLMkZ3L0umSw0a+j3o3kL+4cd
ZPqF9OffTk2AOBUBPGGF+M/efzWkF8fN9wz2suylc8O+dZByc/XokmMmZmBTDO/O
hPj7KRb3STIVNmQi4MS1F2fxquzkYMR5CR3AVkfB7LYyN86VgDoX6oPGL0pzHjkr
UNTbAgMBAAGjODA2MAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBYGA1Ud
JQEB/wQMMAoGCCsGAQUFBwMCMA0GCSqGSIb3DQEBBQUAA4GBAH+l4BlDBOoxAv6/
l8CI81tbEgzlQGZ/aQKDVhUC21tkUiCS3M39ZXOrK54Aav/nDPArAu7KMCvGyNrW
tIQvrf5Mi14C9Y9mk2imFPtxGjOF6haxfCGkubnFY05Two/dkUG7Qm8bzJQy5j23
/0QCIx4dSMiv0ON6GFlMAKbPglM5
-----END CERTIFICATE-----

証明書でID Tokenを検証する

test.jsというファイルを作成します。

var jwt = require('jsonwebtoken');
var fs = require('fs');

var token = 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjEZjgyOTViMjM3MGRkODhkZTRjMjJhZWQ4NWNhYmY1MmQyNzU3ZjUifQ.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTAyOTUyOTA4OTYzOTU1NzQwMjc0IiwiYXpwIjoiNjk4MTAwMDA3MjQyLTRqcDQxZzltYzUwdIwa2syMmwxMTNhbnBnYWduMGNpLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiYXRfaGFzaCI6IlB5QUFtYzBWN0lzMlZKamdmQ2lVYXciLCJhdWQiOiI2OTgxMDAwMDcyNDItNGpwNDFnOW1jNTB2cjBrazIybDExM2FucGdhZ24Y2kuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJpYXQiOjE0MjIyODE3MjUsImV4cCI6MTQyMjI4NTYyNX0.sG65mFYE-zuGlAYOh0e1E5qmhKGhR_-wmJsMEYNaBtGqBIi-czbYVJVZRPqraKxUST1-SCdWb3VcQvhs4gIXA2x5w8bP7yOul-_aXx8kcEJp2NnzaTgdBQ7k0oE3vAaKE_8Kg3JxGc0_0apq9aJ9cyGHwrHbazs-bgGqqCKqmQ';

var cert = fs.readFileSync('public.pem');

jwt.verify(token, cert, function(err, decoded) {
  console.log('error: ', err)
  console.log('result: ', decoded)
});

こうすると以下の様な結果になります。

error:  null
result: {
    "iss": "accounts.google.com",
    "sub": "user_unique_id",
    "azp": "my_service_client_id",
    "at_hash": "PyAAmc0V7Is2VJjgfCiUaw",
    "aud": "my_service_client_id",
    "iat": 1422281725,
    "exp": 1422285625
}

JWKの時と同じ結果になりましたね。
以上です。お疲れ様でした。

ソース