JWTについてまっっっっっっっっっっっっっっっっっったくわからず調べたので備忘録としてアウトプットします。
※随時更新予定
誤った内容があればマサカリお願いします。血まみれになりたいsです。
JWTとは
Json Web Tokenを略してJWT
JWTでは、トークン内に任意の情報(クレーム)を保持することが可能であり、例えばサーバはクライアントに対して「管理者としてログイン済」という情報を含んだトークンを生成することができる。クライアントはそのトークンを、自身が管理者としてログイン済であることの証明に使用することができる。トークンは当事者の一方(通常はサーバ)または両方(もう一方は公開鍵を提供する)の秘密鍵により署名されており、発行されたトークンが正規のものか確認することができる。
トークン
:JSONオブジェクトをエンコードした文字列
eyJhbGciOiJIUzI1NiJ9. # ヘッダー
eyJleHAiOjE2MDA1OTY0MzEsInN1YiI6MSwibmFtZSI6InVzZXIwIn0. # ペイロード
lXcwASyLX5GEsMvPYDVhe0ovJj631fUiC0q2ojK-yK0 # 署名
JWT(トークン)の構造
JWT(トークン)は3つの構造からなる
1. ヘッダー
トークンのタイプと使用する署名アルゴリズム
eyJhbGciOiJIUzI1NiJ9. # ヘッダー
=> { "alg": "HS256", "typ": "JWT" }
2. ペイロード
実際のデータを含むクレーム(任意の情報)
eyJleHAiOjE2MDA1OTY0MzEsInN1YiI6MSwibmFtZSI6InVzZXIwIn0. # ペイロード
=> {"exp"=>1600596431, "sub"=>1, "name"=>"user0"}
exp
やsub
などのそれぞれの値をクレームと呼ぶ
デフォルトで指定されている値を**「予約済クレーム」、使用者が任意に指定した値を「公開(パブリック)クレーム」、当事者間で合意された非公開のカスタムクレームを「プライベートクレーム」**と呼ぶ
-
exp
(Expiration Time)- トークンの有効期限(失効時間)
- UNIXタイムスタンプ形式
- 例:
"exp": 1622505600
(2021年6月1日)
-
sub
(Subject)- トークンの主題(通常はユーザーID)
- 文字列形式
- 例:
"sub": "1234567890"
または"sub": "user@example.com"
-
iss
(Issuer)- トークンの発行者
- 文字列形式
- 例:
"iss": "https://api.myapp.com"
3. 署名
トークンの完全性を検証するのに使用(トークンが変更されていないか確認するために使用)
// エンコード => デコード
lXcwASyLX5GEsMvPYDVhe0ovJj631fUiC0q2ojK-yK0
=> HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your-256-bit-secret
)
主な用途
認証
ユーザーがログインしたあと、以降のリクエストにJWTが含まれ、そのトークンにより保護されたルートやリソースへのアクセスが許可される
情報交換
署名されているため、送信者が本人であることを確認でき、情報が改ざんされていないことを確認できる
RubyにおけるJWT
Gemとして提供されているjwt
はJSON Web Tokenの実装を提供している。このgemを使用することで、JWTの生成と検証が簡単に行える。
require "jwt"
# トークンのエンコード(生成)
payload = { data: "test" }
token = JWT.encode payload, "my_secret", "HS256"
# トークンのデコード
decoded_token = JWT.decode token, "my_secret", true, { algorithm: "HS256" }
# トークン生成
def generate_token(user_id)
pay_load = { user_id: user_id, exp: 24.hours.from_now.to_i }
JWT.encode payload, Rails.application.secrets.secret_ley_base, "HS256"
end
# トークン検証
def authenticate_user
token = request.headers["Authorization"]&.split(" ")&.last
begin
decoded = JWT.decode token, Rails.application.secrets.secret_key_base, true, { algorithm: "HS256" }
@current_user = User.find(decoded[0]["user_id"])
rescue JWT::DecodeError
render json: { errors: ["Invalid token"] }, status: unauthrized
rescue JWT::ExpiredSignature
render json: { errors: ['Token has expired'] }, status: :unauthorized
end
end
RailsにおけるJWTの役割
1. APIにおける認証の実装
Railsでは特にフロントエンドとバックエンドが分離されたSPAやモバイルアプリとの連携において、JWTが重要な役割を果たす。
def login
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
token =
JWT.encode({user_id: user.id, exp: 24.hours.from_now.to_i},
Rails.application.credentials.secret.secret_key_base)
render json: { token: token, user: user.as_json(expect: :password_digest) }
else
render json: { error: "Invalid credentials" }, status: unauthorized
end
end
2.セッションレス認証の実現
従来Railsではcookieベースのセッション管理が一般的だったが、JWTを使用することで
- サーバー側でセッション状態を保持する必要がなくなる(ステートレス)
- スケーラビリティ向上
- クロスドメイン認証の容易さ
というメリットが生まれる。
3.マイクロサービスアーキテクチャでの認証
Railsアプリケーションが複数のマイクロサービスと連携する場合、JWTは各サービスの間の認証に使われる
# サービスA(ユーザー認証)
token = JWT.encode({user_id: user.id, permissions: user.permissions}, secret_key)
# サービスB(リソースサーバー)
begin
payload = JWT.decode(token, secret_key, true, algorithm: 'HS256').first
user_permissions = payload['permissions']
# 権限に基づいた処理...
rescue JWT::DecodeError
# 認証エラー処理
end
JWTのいいところ
情報改ざんができない
トークンが正しいものか検証する際に署名アルゴリズムと一致したトークンでなければエラーが発生する。
Userテーブルにトークンを一時的に保有する絡むを作成しなくてもよくなる
署名をした鍵を持つものしかトークンを検証することができない
セキュリティ上の考慮点
- トークンはエンコードされているだけで暗号化されているわけではない
- トークンの有効期限を適切に設定(expクレーム)
- 機密情報をペイロードに含めない
- HTTPS通信
- 定期的な鍵のローテーション
参考記事
特に関係ないけど出てきたわからないコード
HashWithIndifferentAccess.new(decoded)
HashWithIndifferentAccess
Rails(activesupport)で提供されているHashのサブクラス
SybolとStringを区別せず同一のキーとして取り扱う
hogehoge(dev)> normal_hash = { "user_id" => 1 }
=> {"user_id"=>1}
hogehoge(dev)> normal_hash["user_id"]
=> 1
hogehoge(dev)> normal_hash[:user_id]
=> nil
hogehoge(dev)> indifferent_hash = HashWithIndifferentAccess.new({ "user_id" => 1 })
=> {"user_id"=>1}
hogehoge(dev)> indifferent_hash["user_id"]
=> 1
hogehoge(dev)> indifferent_hash[:user_id]
=> 1