JWTの概要とJWT認証について
最近、認証・認可の学習を進める中で、OAuthやOpenID ConnectにおいてJWTが利用されているという記述を目にしました。JWT自体は認証専用の技術ではないと理解していましたが、そもそもJWTとは何か?またなぜ認証技術として活用されるのか疑問に思い、調べてみることにしました。
主にRFC 7519を参考にしました↓
JWTとは何か?
RFCでは以下のように説明されています。
JWTは、JSONオブジェクトを用いて、二者間でクレーム(主張)を安全に表現するためのコンパクトでURLセーフな手段を提供します。主な目的は、認証や情報交換の際に、エンティティ間でセキュリティを確保しつつ、情報を軽量に伝達することです。
JWTの説明で出てくる「クレーム」というのは、送受信したいデータのことでJWTではこれをJSONオブジェクトとして扱います。つまり、JWTというのは、JSONオブジェクトをコンパクトかつURLセーフな形式にしたものだと言えますね。「コンパクトかつURLセーフな形式」をどのように実現しているかは以下の記事でわかりやすく説明されていました。
簡単に説明すると、
コンパクトにするために
→ データ項目の名称を省略形にしてキーを短くしている(例:iss、subなど)
URLセーフにするために
→ JSONオブジェクトをBase64URLエンコードしている
という方法をとっています。
JWTでどのようにセキュリティを確保しているの?
前節冒頭のRFCの2文目に、
主な目的は、認証や情報交換の際に、エンティティ間でセキュリティを確保しつつ、情報を軽量に伝達することです。
とあります。
つまり、単にJSONオブジェクトをコンパクトでURLセーフな形式で送受信するだけでなく、セキュリティも同時に確保しているということです。
では、どのようにセキュリティを確保しているのでしょうか?
その答えはJWS(JSON Web Signature)とJWE(JSON Web Encryption)にあります。実は、JWTは使用方法によりJWSまたはJWEになります。完全性を担保したい場合にはJWS(JWT)を用い、機密性を担保したい場合にはJWE(JWT)を用います。つまり、JWTはその用途に応じて必ずJWSまたはJWEのいずれかに分類されるということです。
ここでは、JWSとJWEの詳しい説明は省きますが、両者はJSON Serialization形式とCompact Serialization形式の2種類が存在し、JWTとして扱われるのはCompact Serialization形式です。
内包関係は以下のようになります。
上記のことを踏まえて、この記事では署名を使ったJWT(上記画像の「署名したもの」)について見ていきます。
以降でJWTと表記している箇所は署名を使ったJWT(JWS)のことです。
JWTの構成要素
JWTは、以下の3要素から構成されます。
- JOSEヘッダー
- ペイロード
- 署名
この3つの要素が.
(ピリオド)で連結された文字列がJWTの全体を表しています。
eyJhbGciOiJIUzI1NiJ9.eyJkYXRhIjoic2FtcGxlIGRhdGEiLCJleHAiOjE3NDAzMTY4ODV9.bBtgEsvnnSmqVn7spFxVRHVYf9PeEm4liafOW_uZ02E
JOSEヘッダー
JOSEヘッダーには、JWTに適用される署名アルゴリズムや、オプションで追加プロパティが記述されます。以下の例では、エンコード後のオブジェクトがJWTであり、かつそのJWTがHMAC SHA-256アルゴリズムで署名されたJWSであることを示しています。このオブジェクトをBase64URLエンコードすることで、JWTの先頭要素が作成されます。
{
"typ":"JWT",
"alg":"HS256"
}
ペイロード
JWTのペイロード部分は「JWTクレームセット」と呼ばれ、実際に送受信するデータが格納されます。ここでのクレームとは、JSONのキーと値のペアを指し、キーをクレーム名、値をクレーム値と呼びます。
以下はペイロードの例です。
- 発行者(iss)がjoe
- 有効期限(exp)が1300819380
- http://example.com/is_rootに対してtrueという情報
{
"iss": "joe",
"exp": 1300819380,
"http://example.com/is_root": true
}
上記のクレーム名のうち、iss
とexp
は登録済みクレーム名と呼ばれ、IANAの「JSON Web Token Claims」レジストリに登録されています。つまり、発行者を示す場合は、独自にissuer
と定義するのではなく、登録されているiss
を使用する必要があります。これは、前節で説明した通り、JWTをコンパクトに保つための工夫です。
署名
この要素は、JWTのペイロードの完全性を保証するための署名です。署名を付与することで、改竄防止が実現されます。
詳しい署名方法については省略しますが、JOSEヘッダーに指定された署名アルゴリズムを用いてペイロード部分の署名を生成し、それをBase64URLエンコードすることでJWTの末尾要素が作成されます。
JWTの生成、検証をしてみる
rubyを使って実際にJWTの生成と検証を試してみます。
JWTの操作にはruby-jwtというgemを使います↓
JWTの生成
JWT.encodeメソッドに、鍵・ペイロード・署名アルゴリズムを渡すことでJWTを生成することができます。
今回はJWTの有効期限を60sにしました。
require "jwt"
secret_key="secret_key"
# ペイロードの作成(例として、現在時刻から1時間後に有効期限が切れる設定)
payload = {
data: 'sample data',
exp: Time.now.to_i + 60
}
# JWTの生成(ペイロードに対して秘密鍵とHS256アルゴリズムで署名)
token = JWT.encode(payload, secret_key, 'HS256', {'typ': 'JWT'})
puts("生成したJWT: #{token}")
ruby jwt_generate.rb
生成したJWT: eyJhbGciOiJIUzI1NiJ9.eyJkYXRhIjoic2FtcGxlIGRhdGEiLCJleHAiOjE3NDAzMTY4ODV9.bBtgEsvnnSmqVn7spFxVRHVYf9PeEm4liafOW_uZ02E
JWTの検証
先ほど生成したJWTを引数に渡すとJWTの検証が行われます。
require "jwt"
secret_key="secret_key"
if ARGV.empty?
abort("JWTトークンをコマンドライン引数として渡してください。")
end
token = ARGV[0]
# JWTの検証
begin
decoded_token = JWT.decode(token, secret_key, true, { algorithm: 'HS256' })
puts("検証に成功しました!")
puts("デコードしたペイロード: #{decoded_token[0]}")
puts("ヘッダー情報: #{decoded_token[1]}")
rescue JWT::ExpiredSignature
puts("エラー: トークンの有効期限が切れています。")
rescue JWT::DecodeError => e
puts("エラー: トークンが無効です。詳細: #{e.message}")
end
検証に成功するとjwt_generate.rbで定義していた元のペイロードが得られます。
ruby jwt_verify.rb eyJhbGciOiJIUzI1NiJ9.eyJkYXRhIjoic2FtcGxlIGRhdGEiLCJleHAiOjE3NDAzMTY4ODV9.bBtgEsvnnSmqVn7spFxVRHVYf9PeEm4liafOW_uZ02E
検証に成功しました!
デコードしたペイロード: {"data"=>"sample data", "exp"=>1740316885}
ヘッダー情報: {"typ"=>"JWT", "alg"=>"HS256"}
JWT認証の仕組み
JWTは、署名を利用することでデータの完全性(改竄が行われていないこと)を検証できます。JWT認証では、ユーザーの識別が完了した後、ペイロードにユーザー情報(機密情報は含めない)を格納したJWTを生成し、そのトークンをクライアントとサーバ間でやり取りすることで、認証が必要な操作を実現します。
クライアントとサーバー間でJWTを使った認証の流れは以下のようになります:
- ユーザーがログインフォームからユーザー名とパスワードを送信
- サーバーが認証情報を検証し、正しければユーザー情報を含むJWTを生成
- 生成したJWTをクライアントに返却
- クライアントは受け取ったJWTを保存(通常はローカルストレージやCookie)
- 以降の認証が必要なAPIリクエスト時にJWTをAuthorizationヘッダーに含めて送信
- サーバーはリクエストに含まれるJWTを検証し、有効であれば要求されたリソースを返却
この認証方式は、「トークンベース認証」と呼ばれ、サーバ側でユーザのログイン状態を保持しないことが特徴です。
Discussion