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』で定義されている予約済みのプロパティ名およびクレーム名は以下の通りです。

プロパティ名 クレーム名 意味
iss issuer JWTの発行者
sub subject JWTの主体
aud audience JWTの主体の一覧
exp expiration Time JWTの有効期限
nbf not before JWTが有効になる日時
iat issued At JWTの発行時刻
jti JWT ID JWTを識別するためのユニーク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)やってます。フォローしてもらえるとうれしいです!

参考資料