IDトークンの中身を紐解いてみる

OpenID Connectにおける認証情報の送付

これまでの記事で、OAuth2.0やOpenID Connectでいかに安全にユーザの認可や認証を実現するかを少しずつ見ていきました。
これまではどちらかというと、認証リクエストやcodeのやり取りを途中で傍受された場合のケースを考えていましたが、例えばOpenID ConnectのIDトークンを改竄されて送付されるような、認証情報の改竄にはどう対応しているのかというのが気になってきたので、
その前にまずはIDトークンの構造から整理してみようと思います。
IDトークンはJWT(Json Web Token)はJSON形式で表されているので、このJWTの構成も含めてみていきたいと思います。

JWTの形式

JWTは実際のデータとしては例えば下記のような形となります。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6InRlc3QtMDEiLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6MTUxNjIzOTAyMn0.6pUVriX3UlaNBHs27IfePpeXHz45yWONeLgBxmltIP0

よくわからない文字列に見えますが、じっくり中身を見てみると二つのピリオドで三つのパートに区切られていることがわかります。

  1. eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
  2. eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6InRlc3QtMDEiLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6MTUxNjIzOTAyMn0
  3. 6pUVriX3UlaNBHs27IfePpeXHz45yWONeLgBxmltIP0

それぞれのパートに役割があるので、この文字列を可読なものに変換しながらまとめたいと思います。
先んじてこの役割というのが、順に 1. ヘッダー 2. ペイロード 3. 署名 という構造になっているのですが、 このような構造ででデジタル署名もしくはMAC(Message Authentication Code)化を使用しているデータ構造を「JWS(JSON Web Signature)」と呼びます。
(※署名/MAC化でなく暗号化を使用している場合はJWT=JSON Web Encryption)となります)

1.ヘッダー

JWTはBase64URLエンコードされているので、まずはデコードをします。 上記例に挙げたヘッダー部をデコードするとこんな感じです。

{
    "alg": "HS256",
    "typ": "JWT"
}

ヘッダー部ではJWTの暗号化方式と、追加プロパティについて記述しています。 具体的に使用可能なパラメータはこんな感じ。 RFC7515 に定義があります。

パラメータ名 内容
alg JWSに使われる暗号化アルゴリズムを表示(必須パラメータ)。定義済みのアルゴリズムこちらにあります。
jku jku = JWK(JSON Web Key) Set URLのことで、JWTのデジタル署名に利用される秘密鍵と対応している公開鍵のURLが記載されています。
jwk jkuと同様にJWTのデジタル署名に利用される秘密鍵と対応する公開鍵の情報を表しますが、こちらは鍵本体を記載します。
kid JWSで使用されている鍵のIDを表します。
typ JWSの種類を表します。

先ほどのデコードしたヘッダー部分は、「暗号化アルゴリズムはHS256=HMAC SHA 256で、種類はJWT」ということを示しています。

2.ペイロード

ペイロード部には渡したい情報本体(クレーム)が入っています。
ヘッダーと同様にBase64URLエンコードされているので、デコードすると中身はこんな感じ。

{
    "sub": "1234567890",
    "name": "test-01",
    "iat": 1516239022,
    "exp": 1516239022
}

色々とクレーム名が設定されていますが、実際には仕様で定義済みのものと、発行者が自分で設定できるものがあります。
定義済みのものはこんな感じ(RFC7519)

名前 内容
iss クレームの発行者名
sub 発行されたクレーム保持者の識別子
iat JWTが発行された日時
exp JWTの有効期限
nbf JWTの有効期間開始日時

同様に例で挙げた内容は「クレーム保持者の識別子は"1234567890"で、発行日時は"2018/01/18 10:30:22"、有効期限は"2018/01/18 10:30:22"、nameというクレームが"test-01"」という風になります。(※時刻はUNIX時間で記載)

3.署名

最後のパートが署名です。こちらもBase64URLエンコードされていますが、中身のデータはバイナリとなっています。 ヘッダーのalgパラメータで定義があるアルゴリズムで署名されているので、今回はHMAC SHA256となります。
署名というのは、1・2のヘッダーとペイロード部の内容を対象としているので、今回のようなHS256の場合は秘密鍵を使用してヘッダーとペイロード(base64URLエンコード済のもの)をSHA256でハッシュ化した値が署名された値と一致していれば、クレーム発行者が発行した正しい内容だということがわかります。

署名の形式

上記例ではHS256でしたが、下記のように

{
  "alg": "RS256",
  "kid": "e8732db06287515556213b80acbcfd08cfb302a9",
  "typ": "JWT"
}

algパラメータがRS256、つまり「RSASSA-PKCS-v1_5 using SHA-256」ですが要するに公開鍵暗号を利用しているパターンです。
左記ほどは記載のなかったkidパラメータ=JSWで使用されている鍵のIDが定義されています。
こちらはGoogleが発行したJWSなのですが、Googleが公開している発行情報( https://accounts.google.com/.well-known/openid-configuration )へアクセスすると、

 "jwks_uri": "https://www.googleapis.com/oauth2/v3/certs",

という形で公開鍵の情報が記載されているURLが記載されており、さらにアクセスすると、

{
  "keys": [
    {
      "kid": "e8732db06287515556213b80acbcfd08cfb302a9",
      "alg": "RS256",
      "e": "AQAB",
      "n": "4RIrO30287Wsq3gqXCMkUYMVAeI3H8LVE6IXR1krdFeGnZLiGUPwcbkeVpXf3lmJdsStOg-jijces2DZCfPyIBiQuLYfxxmAZE6ErJ0QJFg1stwli2Pz9ncYhFoqi8pXr7kEzEJBTzX4thuw56ydbGsshSEznPXoerCJOc7UI2-n0wFCWQ4YLHbh_PrWt4vdadyUUUW_QpQHXQLdD8q_Qwqdj0O9zlJE7R6Elw2E9EqnHyIGu1hmLxhqrTru1M18SUhONYbVskV_BCEdVKs__X96849HorWQDCAgVMWfGsdMVq55FAdJ680N5UmQDRynIZ4-PeNGN4S9iw2mbMNEBQ",
      "kty": "RSA",
      "use": "sig"
    },
    {
      "e": "AQAB",
      "n": "xT_ngLZNmT5GBdkLtJZjNeTB-8B5yWgrq_e5eMZ1hrZhcmLK-dSnIkpOPV8_OekV67EnQ7I4II2rcNJnHGrGKZziXO3XN2gtUHE-mBJC99oULSbX_QwBKz7gC_IBPq9EuxTt6Oq6fPkVQ9DbRIgWJSEGBF_KRaNl3kyAlIZfpY7XgHyJTTv8E7yAcYKPR-36gzdl-ps0sDLKzUuAtZNq8llK0u80z6AtAUIYwWdkEhM9upy6keKITasIxcsO7M6kZPINUSbh6t5VAm8FuqRmxpgg-9c9_GQSGd89InVypoVzWLQ-wOGg5G4H6JqIgtj0TRFt4gK0eoFi2U0d3l8bcw",
      "alg": "RS256",
      "use": "sig",
      "kty": "RSA",
      "kid": "8462a71da4f6d611fc0fecf0fc4ba9c37d65e6cd"
    }
  ]
}

と、鍵の一覧が取得できます。ヘッダーに記載されているkid「e8732db06287515556213b80acbcfd08cfb302a9」が前者に記載されているので、こちらから公開鍵を取得して署名を検証することとなります。

この辺りの署名の仕組みがまだ疑問が残っている部分も多いので次回はもう少しこの辺りを深堀したいなと思っています。なかなか奥深い分野です。。。