JWTの概要と構成要素(ヘッダ/ペイロード/署名)を理解する

JWTについて

JWTはJSON Web Tokenの略で、署名されたJSON形式のクレーム(Claim、情報、内容、属性情報)をURLセーフな形で表現する際のトークンの仕様です。JWTの仕様はRFC 7519で定義されています。JWTの読み方は「ジョット」です。

JWTにはヘッダ.ペイロード.署名というフォーマットのJWS形式(RFC 7515)と、ヘッダ.キー.初期ベクター.暗号文.認証タグというフォーマットのJWE形式(RFC 7516)の2つがあります。

1 JWSとはJSON Web Signatureの略で、JSONの署名に関する仕様です。JWSはJSON Web Encryptionの略で、JSONの暗号化に関する仕様です。

ほとんどの場合、JWTにはJWS形式のヘッダ.ペイロード.署名というフォーマットが採用されています。ですので、本記事で紹介するJWTもJWS前提での説明となります。

JWT(JWS)の構成要素について

JWTはBase64URLエンコードした「ヘッダ」「ペイロード」「署名」をピリオド(.)で連結させた文字列です。Base64URLエンコードによりJWTはURLセーフな形で表現されます。JWTの具体例は以下の通りです。

### JWT
eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJleGFtcGxlX2FwcCIsInN1YiI6IjEiLCJleHAiOjE2NDcwNDk0OTF9.J9CmVMZrjO2rXaL-vu9hB_VFvXseD5L6k-qxVqbLrjl-K9hO4X4rXfg-e8K-CufGu2TFhr2srFDHnvUJzEzWlKuk5jUdnnegcSppHRYifYihPezthan4tiH2CxPW9y-HSVDeiY3BgSuPs5uAZv36bfqzOj878h1FDqleUpnxmE4EY9g5yr-u0lbMJepS05F8FH5HwPRG8Z3wMDkbs4_HRo1HwUGhJax75YOuDqXXLGjK57iMH-aCBPBDzLLcGR7T1ftDvC7fqoKq_MR-yI2Ymco99AHKamRsPxQTZz1ydHeSY7_bjNCgMzCA0LfKHqUHuGYTturKlI99WfWmRw

上記のJWTの「ヘッダ」「ペイロード」「署名」はそれぞれ以下の部分です。

### ヘッダ
eyJhbGciOiJSUzI1NiJ9

### ペイロード
eyJpc3MiOiJleGFtcGxlX2FwcCIsInN1YiI6IjEiLCJleHAiOjE2NDcwNDk0OTF9

### 署名
J9CmVMZrjO2rXaL-vu9hB_VFvXseD5L6k-qxVqbLrjl-K9hO4X4rXfg-e8K-CufGu2TFhr2srFDHnvUJzEzWlKuk5jUdnnegcSppHRYifYihPezthan4tiH2CxPW9y-HSVDeiY3BgSuPs5uAZv36bfqzOj878h1FDqleUpnxmE4EY9g5yr-u0lbMJepS05F8FH5HwPRG8Z3wMDkbs4_HRo1HwUGhJax75YOuDqXXLGjK57iMH-aCBPBDzLLcGR7T1ftDvC7fqoKq_MR-yI2Ymco99AHKamRsPxQTZz1ydHeSY7_bjNCgMzCA0LfKHqUHuGYTturKlI99WfWmRw

以下ではJWTを構成する「ヘッダ」「ペイロード」「署名」の要素について説明します。

ヘッダについて

ヘッダはJWTの署名の検証に必要な情報を格納する要素です。元データはJSONで定義されており、Base64URLエンコードによりJWTの文字列の一部となります。

ヘッダのJSONで指定できるプロパティ名は仕様で決まっており、たとえばalgというプロパティは署名のアルゴリズムを意味します。

以下のように、RS256(SHA-256アルゴリズムを利用したRSA)で署名したJWTのヘッダをBase64URLデコードするとalgRS256がセットされていることがわかります。サンプルコードはRubyで実装しています。

なお、RubyでRSA暗号の鍵を作成するためのOpenSSL::PKey::RSAクラスの詳細解説は【Ruby】OpenSSL::PKey::RSAを利用した秘密鍵・公開鍵の生成方法で紹介しています。

$ irb

> require 'openssl'
> require 'jwt'

### /dev/randomを利用して乱数を初期化する
> OpenSSL::Random.seed(File.read("/dev/random", 16))

### RSAオブジェクト(RSA秘密鍵)の生成
> rsa_private = OpenSSL::PKey::RSA.generate(2048)

### RSA暗号の秘密鍵を使ってJWTにエンコード
> token = JWT.encode({ iss: "example_app", sub: "1"}, rsa_private, 'RS256')

### ヘッダを取得
> header = token.split('.')[0]

### ヘッダをデコードすると署名のアルゴリズムで利用した『RS256』が記載されている
> JSON.parse(Base64.urlsafe_decode64(header))
=> {"alg"=>"RS256"}

ペイロードについて

ペイロードとはデータ本体のことを指します。元データはJSONで定義されており、各プロパティがクレーム(Clai)mと呼ばれるデータの属性情報を表します。

RFC 7519『4.1. Registered Claim Names』で定義されている予約済みのプロパティ名およびクレーム名は以下の通りです。

プロパティ名クレーム名意味
ississuerJWTの発行者
subsubjectJWTの主体
audaudienceJWTの主体の一覧
expexpiration TimeJWTの有効期限
nbfnot beforeJWTが有効になる日時
iatissued AtJWTの発行時刻
jtiJWT IDJWTを識別するためのユニークID

上記以外のプロパティ名もペイロードに追加できます。

ペイロードを作成する際は、予約済みクレームから適切なものを選択したり、必要であればクレームを独自定義したりすることになります。

署名について

署名はJWTの署名情報が記述されている箇所です。

JWTのヘッダ要素とペイロード要素に署名アルゴリズム(ヘッダのalgで指定されたアルゴリズム)を適用し、Base64URLエンコードすることでJWTの文字列の一部となります。

参考: JWTの各要素をBase64URLデコードして「ヘッダ」「ペイロード」「署名」の元データを確認してみる

JWTをピリオド区切りで要素ごとに分解し、それぞれの要素をBase64URLデコードした結果について紹介します。検証環境のフレームワークはRuby on Rails、署名アルゴリズムはRS256です。

$ rails c

### /dev/randomを利用して乱数を初期化する
> OpenSSL::Random.seed(File.read("/dev/random", 16))

### RSAオブジェクト(RSA秘密鍵)の生成
> rsa_private = OpenSSL::PKey::RSA.generate(2048)

### 公開鍵の作成(JWTをデコードする際に利用する)
> rsa_public = rsa_private.public_key

### ペイロードの作成
> payload = { iss: "example_app", sub: "1", exp: (DateTime.current + 14.days).to_i }

### 秘密鍵を使ってペイロードをJWTにエンコード
> token = JWT.encode(payload, rsa_private, 'RS256')
=> "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJleGFtcGxlX2FwcCIsInN1YiI6IjEiLCJleHAiOjE2NDcxMjg3MjB9.OgZlmGQntlInwCztsZZPzG0nh3u4qSY9RCeDRq9z_rMrLvVFl2ctrf5Zv918TiU55IJyRCnv0rXJDTqeSZ-RkeQxlmafINTE4UlOcsUfXz0LnDBLUtsumm0FRC-7B_bsUBUni3DFzA-9j2aQyNdxGkKlpsPurLQmmwn6lU4A-1LFeX0eIKi731T8AXGUVPO4hcSaSX79mmH8Vqkdm7zXU8Wpqt97LI2SlF0w-pxRdITKenSgtFJkihgLCDn7Wt24uJRq7zbN420oY7JnfTS3lJ5Yy1GyfWSD7Jd4-0mZtcKhcjWKv0_VKTLaBsNojCZzX1zVSutlQPVQbPzt7E8JdA"

### ピリオドを区切り文字としてJWTを3つのパーツに分割。
> jwt_header, jwt_payload, jwt_signature = token.split('.')

### ヘッダをBase64URLデコード
> JSON.parse(Base64.urlsafe_decode64(jwt_header))
=> {"alg"=>"RS256"}

### ペイロードをBase64URLデコード
> JSON.parse(Base64.urlsafe_decode64(jwt_payload))
=> {"iss"=>"example_app", "sub"=>"1", "exp"=>1647128720}

### 署名をBase64URLデコード(署名はバイナリデータで保存されている)
> Base64.urlsafe_decode64(jwt_signature)
=> "'\xD0\xA6T\xC6k\x8C\xED\xAB]\xA2\xFE\xBE\xEFa\a\xF5E\xBD{\x1E\x0F\x92\xFA\x93\xEA\xB1V\xA6\xCB\xAE9~+\xD8N\xE1~+]\xF8>{\xC2\xBE\n\xE7\xC6\xBBd\xC5\x86\xBD\xAC\xACP\xC7\x9E\xF5\t\xCCL\xD6\x94\xAB\xA4\xE65\x1D\x9Ew\xA0q*i\x1D\x16\"}\x88\xA1=\xEC\xED\x85\xA9\xF8\xB6!\xF6\v\x13\xD6\xF7/\x87IP\xDE\x89\x8D\xC1\x81+\x8F\xB3\x9B\x80f\xFD\xFAm\xFA\xB3:?;\xF2\x1DE\x0E\xA9^R\x99\xF1\x98N\x04c\xD89\xCA\xBF\xAE\xD2V\xCC%\xEAR\xD3\x91|\x14~G\xC0\xF4F\xF1\x9D\xF009\e\xB3\x8F\xC7F\x8DG\xC1A\xA1%\xAC{\xE5\x83\xAE\x0E\xA5\xD7,h\xCA\xE7\xB8\x8C\x1F\xE6\x82\x04\xF0C\xCC\xB2\xDC\x19\x1E\xD3\xD5\xFBC\xBC.\xDF\xAA\x82\xAA\xFC\xC4~\xC8\x8D\x98\x99\xCA=\xF4\x01\xCAjdl?\x14\x13g=rtw\x92c\xBF\xDB\x8C\xD0\xA030\x80\xD0\xB7\xCA\x1E\xA5\a\xB8f\x13\xB6\xEA\xCA\x94\x8F}Y\xF5\xA6G"

### 参考: JWTのデコード結果(デコードは公開鍵でも秘密鍵でも可)
> JWT.decode(token, rsa_public, true, { algorithm: 'RS256' })
=> [{"iss"=>"example_app", "sub"=>"1", "exp"=>1647128720}, {"alg"=>"RS256"}]

さいごに

Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!

参考資料

タグ: Rails , 認証認可