JSON Web Tokenを完全に理解する

はじめに

JSON Web Tokenを完全に理解する - Qiita この記事を書いた本人です。 はてなブログにもコピーを掲載します。

JSON Web Tokenとは

ざっくり言うと「2者間で安全にクレームを表現するための方式」です。RFC7519で技術仕様の標準化が行われています。JSON Web Token(以下JWT)は大きく分けて3つの属性に分割できます。ヘッダー、ペイロード、署名の3つです。ペイロードには実際に2者間(クライアント・サーバー間と考えると分かりやすいかもしれません)で受け渡しがしたいJSONが入り、ヘッダー、署名を使って改ざんの検証に用いられます。

まだぼんやりしていて分かりにくいですが、各属性がどのようなものなのかもう少し具体的に見ていくことにします。その後、実際にどのように使っていくかを見ると理解はしやすくなります。

ヘッダー属性

ヘッダーにはどのようなアルゴリズムを用いて署名を行っているかやトークンのタイプの2つで構成されています。

例:

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

この文字列をbase64urlエンコードしたものがJWTのヘッダーとなります。プログラムっぽく書くならばこんな感じです。

header = base64url_encode('{"alg":"RS256","typ":"JWT"}')

ヘッダーに関しては以上です。とても単純な仕様ですね。

ペイロード

実際に2者間で受け渡ししたい実体の入る属性です。要領はヘッダーと同じです。 例:

{
  "exp": "1550905975",
  "name": "k_k_hogetaro",
  "is_engineer": true
}

ヘッダーと同じくペイロードもbase64urlエンコードしたものとなります。 ヘッダーと違う点を挙げるとするといくつか予約語が定義されています。上の例であるとexpは仕様で定められた予約のkeyとなります。その他の予約語仕様書を参照してみてください。

署名

ペイロード属性はbase64urlエンコードされているだけですので、このままだとデコードすれば中身を確認でき、改変できてしまいます。それだと困る場合がいろいろあります。例えばCookieの値とか。そこで署名を行い、改変の有無を検証可能なものにします。 ハッシュアルゴリズムHS256のケースを例とします。他のアルゴリズムのケースも後述します。 HS256は共通の秘密鍵で署名、検証を行います。

プログラムのように書くと以下のようになります。

value = header + "." + payload + "." # base64urlエンコード済みのヘッダーとペイロードを「.」で繋いだ形のものを用います。
signature = hash256(value, secret_key);

これが署名のすべてです。

JWTの完成形

これでJWTのすべてが揃いました。ヘッダー、ペイロード、そして署名です。これらを「.」で繋いだものがJWTとなります。

JWT = "#{header}.#{payload}.#{signature}"

JWTの使われ方

↑まででJWTの具体的な形を見てきました。しかし、まだどのように使われるかが全くわかりません。なので、実際にどのような使われ方があるのか見ていきます。

アルゴリズムHS256の場合

署名のところでもHS256は扱いましたが、共通の鍵を用いて署名と検証を行います。なので、以下のようなケースが想定されます。あくまで一例です。

1, まず、どのサービスにでもあるようなログインを行います。利用可能ユーザーであればJWTを返却します hoge.png

2, ユーザーはJWTを用いてサービスのAPIにアクセスします。 hoge.png

となります。2の検証ですが、署名部は秘密鍵保有しているコンピューターにしか生成できません。そしてヘッダー、ペイロードが改変されていると、その署名と食い違いが発生するので、改変がわかる仕組みになっています。例えば次のJWTが発行されたとします。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDJ9.UqRu8fGnUAmn-Z_wwsgGVNTXANkIiDdEbj-BdZRafks
$ echo eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 | base64 -d
{"alg":"HS256","typ":"JWT"}
$ echo eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDJ9 | base64 -d
{"sub":"1234567890","name":"John Doe","iat":151623902}

もしペイロード

{"sub":"1234567890","name":"cat dog","iat":151623902}

に変えた場合、payloadが

c3ViOjEyMzQ1Njc4OTAgbmFtZTpjYXQgZG9nIGlhdDoxNTE2MjM5MDIK

に変わります。ですが署名はクライアント側では改変不可です。(秘密鍵が漏洩している場合はその限りではない) なので、サーバー側で、もう一度、ヘッダーとペイロードから署名を作成し、送られてきた署名と一致するかどうかで検証が可能となります。(実際にはそのような処理を書くのはやめて、ライブラリに処理を任せましょう。)

以上がHS256の場合の一例です。認証サーバーを独立させている場合はこの方法だといろいろ辛くなってきます。共通鍵を認証サーバーとその他Webサーバーで共有することは漏洩の危険性が高まります。よって認証サーバーを立てている場合、公開鍵暗号方式を用いるアルゴリズムを使ったJWTを生成するほうが賢明に思えます。

RS256の場合

RS256はHS256と異なり、公開鍵、秘密鍵のペアを使用します。

こちらも具体例を想定してみます。 hoge.png 1, 認証サーバーにログインに必要な情報を送る 2, 秘密鍵を用いてJWT生成 3, クライアントにJWTを返却 4, 3のJWTを用いてサービスのAPIを叩く 5, JWTの検証 6, サービスの処理 7, クライアントにレスポンス

と言った感じになります。

公開鍵暗号方式を用いたため、署名を生成する認証サーバー以外は公開鍵を保持するだけでよくなりました。 これでサーバーやサービスが増えても公開鍵を保有するだけでJWTの検証が可能になりました。

まとめ

今更感はありますが、JWTについてまとめました。もしかしたら、現在使っているサービスのCookieにJWTが付与されているケースもあるかもしれません。そういう時はbase64 -d でどんなものか確認してみると面白いかもしれません。