JWT Ruby
ライブラリ
JWT Ruby
概要
JWT Rubyは、RFC 7519標準に準拠したJSON Web Token(JWT)のRuby実装で、安全な認証とトークン管理を提供します。
詳細
JWT Ruby gemは、Ruby/Rails アプリケーションでJWT認証を実装するための包括的なライブラリです。HMAC、RSASSA、ECDSA、RSASSA-PSS、EdDSAアルゴリズムをサポートし、OpenSSLライブラリを通じてネイティブ実装を提供します。Ruby 3.3までの最新バージョンをサポートし、高いパフォーマンスとセキュリティを実現します。トークンの生成、検証、デコード機能を提供し、カスタムクレームの処理や期限切れ検証も自動的に行います。Rails アプリケーションとの統合が容易で、多くのサードパーティライブラリからも利用されています。GitHub の jwt/ruby-jwt リポジトリで活発に開発が続けられており、エンタープライズグレードの認証システムに適用できる信頼性の高いライブラリです。
メリット・デメリット
メリット
- RFC 7519準拠: 標準的なJWT仕様への完全準拠
- 豊富なアルゴリズム: HMAC、RSA、ECDSA、EdDSA対応
- 高パフォーマンス: OpenSSLネイティブ実装による高速処理
- Rails統合: Ruby on Railsとの自然な統合
- 拡張性: カスタムアルゴリズムとエクステンション対応
- 成熟したライブラリ: 長期にわたる開発とメンテナンス実績
デメリット
- 設定複雑性: セキュリティ設定の複雑さと学習コスト
- Ruby固有: Ruby以外のランタイム環境では使用不可
- 依存関係: OpenSSLライブラリへの依存
- アルゴリズム選択: 適切な暗号化アルゴリズムの選択が必要
- 古いRuby: Ruby 2.5以下のサポート終了
主要リンク
書き方の例
基本的なトークン生成と検証
require 'jwt'
# シークレットキー
secret = 'my_secret_key'
# ペイロード
payload = {
user_id: 123,
email: '[email protected]',
exp: Time.now.to_i + 3600 # 1時間後に期限切れ
}
# トークンを生成
token = JWT.encode(payload, secret, 'HS256')
puts "Generated token: #{token}"
# トークンをデコード・検証
begin
decoded_payload = JWT.decode(token, secret, true, algorithm: 'HS256')
puts "Decoded payload: #{decoded_payload[0]}"
puts "Header: #{decoded_payload[1]}"
rescue JWT::DecodeError => e
puts "Decode error: #{e.message}"
end
RSA秘密鍵/公開鍵での署名
require 'jwt'
require 'openssl'
# RSA鍵ペアを生成
rsa_private = OpenSSL::PKey::RSA.generate(2048)
rsa_public = rsa_private.public_key
# ペイロード
payload = {
user_id: 456,
role: 'admin',
iat: Time.now.to_i,
exp: Time.now.to_i + 3600
}
# RSA-256で署名
token = JWT.encode(payload, rsa_private, 'RS256')
puts "RSA signed token: #{token}"
# 公開鍵で検証
begin
decoded = JWT.decode(token, rsa_public, true, algorithm: 'RS256')
puts "Successfully verified with public key"
puts "User ID: #{decoded[0]['user_id']}"
puts "Role: #{decoded[0]['role']}"
rescue JWT::VerificationError => e
puts "Verification failed: #{e.message}"
end
Rails認証システムの実装
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_action :authenticate_request
private
def authenticate_request
header = request.headers['Authorization']
if header.present?
token = header.split(' ').last
begin
decoded = JWT.decode(token, Rails.application.secrets.secret_key_base, true, algorithm: 'HS256')
@current_user_id = decoded[0]['user_id']
rescue JWT::DecodeError => e
render json: { error: 'Invalid token' }, status: :unauthorized
end
else
render json: { error: 'Missing token' }, status: :unauthorized
end
end
def current_user
@current_user ||= User.find(@current_user_id) if @current_user_id
end
end
# app/controllers/auth_controller.rb
class AuthController < ApplicationController
skip_before_action :authenticate_request, only: [:login]
def login
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
payload = {
user_id: user.id,
email: user.email,
role: user.role,
iat: Time.now.to_i,
exp: Time.now.to_i + 24.hours.to_i
}
token = JWT.encode(payload, Rails.application.secrets.secret_key_base, 'HS256')
render json: {
token: token,
user: {
id: user.id,
email: user.email,
name: user.name
}
}
else
render json: { error: 'Invalid credentials' }, status: :unauthorized
end
end
def me
render json: current_user
end
end
JWTサービスクラス
# app/services/jwt_service.rb
class JwtService
ALGORITHM = 'HS256'.freeze
class << self
def encode(payload, expiration = 24.hours.from_now)
payload[:exp] = expiration.to_i
payload[:iat] = Time.now.to_i
JWT.encode(payload, secret_key, ALGORITHM)
end
def decode(token)
decoded = JWT.decode(token, secret_key, true, algorithm: ALGORITHM)
decoded[0].with_indifferent_access
rescue JWT::DecodeError => e
Rails.logger.error "JWT Decode Error: #{e.message}"
nil
end
def verify_and_decode(token)
payload = decode(token)
return nil unless payload
# 追加のカスタム検証
return nil if payload[:exp] < Time.now.to_i
payload
end
def refresh_token(old_token)
payload = decode(old_token)
return nil unless payload
# リフレッシュ可能期間のチェック(例:期限切れから1週間以内)
expiry_time = Time.at(payload[:exp])
return nil if expiry_time < 1.week.ago
# 新しいトークンを生成
new_payload = payload.except(:exp, :iat)
encode(new_payload)
end
private
def secret_key
Rails.application.secrets.secret_key_base
end
end
end
# 使用例
token = JwtService.encode({ user_id: 1, role: 'user' })
payload = JwtService.verify_and_decode(token)
new_token = JwtService.refresh_token(token)
権限管理とミドルウェア
# app/controllers/concerns/authorization.rb
module Authorization
extend ActiveSupport::Concern
def authorize_admin
unless current_user&.admin?
render json: { error: 'Admin access required' }, status: :forbidden
end
end
def authorize_owner_or_admin(resource)
unless current_user&.admin? || resource.user_id == current_user.id
render json: { error: 'Access denied' }, status: :forbidden
end
end
end
# app/middleware/jwt_middleware.rb
class JwtMiddleware
def initialize(app)
@app = app
end
def call(env)
request = Rack::Request.new(env)
# 認証が不要なパスをスキップ
unless requires_authentication?(request.path)
return @app.call(env)
end
auth_header = request.get_header('HTTP_AUTHORIZATION')
if auth_header&.start_with?('Bearer ')
token = auth_header[7..-1]
payload = JwtService.verify_and_decode(token)
if payload
env['jwt.payload'] = payload
env['jwt.user_id'] = payload['user_id']
else
return unauthorized_response
end
else
return unauthorized_response
end
@app.call(env)
end
private
def requires_authentication?(path)
# 認証が必要ないパスを定義
excluded_paths = ['/auth/login', '/auth/register', '/health']
!excluded_paths.any? { |excluded| path.start_with?(excluded) }
end
def unauthorized_response
[401, { 'Content-Type' => 'application/json' },
[{ error: 'Unauthorized' }.to_json]]
end
end
カスタムクレームとバリデーション
# app/models/jwt_token.rb
class JwtToken
include ActiveModel::Model
include ActiveModel::Validations
attr_accessor :user_id, :email, :role, :permissions, :iat, :exp, :jti
validates :user_id, presence: true
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :role, inclusion: { in: %w[admin user guest] }
def self.from_token(token)
payload = JwtService.decode(token)
return nil unless payload
new(payload)
end
def to_token
return nil unless valid?
payload = {
user_id: user_id,
email: email,
role: role,
permissions: permissions,
jti: jti || SecureRandom.uuid
}
JwtService.encode(payload)
end
def expired?
return true unless exp
Time.at(exp) < Time.now
end
def admin?
role == 'admin'
end
def can?(permission)
return true if admin?
permissions&.include?(permission.to_s)
end
end
# 使用例
jwt_token = JwtToken.new(
user_id: 1,
email: '[email protected]',
role: 'user',
permissions: ['read_posts', 'write_comments']
)
if jwt_token.valid?
token = jwt_token.to_token
# トークンから復元
restored_token = JwtToken.from_token(token)
puts restored_token.can?('read_posts') # => true
puts restored_token.can?('delete_users') # => false
end
テストでの使用例
# spec/support/jwt_helpers.rb
module JwtHelpers
def generate_jwt_token(user, additional_claims = {})
payload = {
user_id: user.id,
email: user.email,
role: user.role
}.merge(additional_claims)
JwtService.encode(payload)
end
def auth_headers(user)
token = generate_jwt_token(user)
{ 'Authorization' => "Bearer #{token}" }
end
end
# spec/requests/api/posts_spec.rb
RSpec.describe 'API::Posts', type: :request do
include JwtHelpers
let(:user) { create(:user) }
let(:admin) { create(:user, :admin) }
describe 'GET /api/posts' do
it 'returns posts for authenticated user' do
get '/api/posts', headers: auth_headers(user)
expect(response).to have_http_status(:ok)
expect(JSON.parse(response.body)).to be_an(Array)
end
it 'returns unauthorized for invalid token' do
get '/api/posts', headers: { 'Authorization' => 'Bearer invalid_token' }
expect(response).to have_http_status(:unauthorized)
end
end
describe 'DELETE /api/posts/:id' do
let(:post) { create(:post, user: user) }
it 'allows owner to delete their post' do
delete "/api/posts/#{post.id}", headers: auth_headers(user)
expect(response).to have_http_status(:no_content)
end
it 'allows admin to delete any post' do
delete "/api/posts/#{post.id}", headers: auth_headers(admin)
expect(response).to have_http_status(:no_content)
end
end
end