目次
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デコードするとalg
にRS256
がセットされていることがわかります。サンプルコードは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)やってます。フォローしてもらえるとうれしいです!