🔨
Rails8 APIモードでJWT認証をやってみた
Viewを使わずに認証する
Rails8を使用して以前ノートにメモしておいたコードを使用してJWT認証ができるREST APIを作ってみました。
今回使用したGemはこちらになります。
Viewは使用せずにモデルとコントローラーだけでアプリケーションを作成していきましょう。
必要なコマンド(API モード)
APIモードの新規アプリケーション作成(既存アプリケーションの場合はスキップ)
rails new rails_jwt_auth --api
ユーザーモデルの作成
rails generate model User email:string password_digest:string
マイグレーションの実行
rails db:migrate
API用のコントローラーディレクトリとファイルの作成
rails generate controller api/v1/Users create
rails generate controller api/v1/Sessions create
rails generate controller api/v1/ProtectedResource index
1. ユーザーモデル
ユーザー情報を扱うビジネスロジック。
# app/models/user.rb
class User < ApplicationRecord
has_secure_password
validates :email, presence: true, uniqueness: true
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :password, length: { minimum: 6 }, if: -> { new_record? || !password.nil? }
# JWTトークン用にユーザー情報をペイロードに変換
def to_token_payload
{
sub: id,
email: email
}
end
end
2. APIコントローラーの基底クラス
HTTP通信を使用してモデルにリクエストを送るコントローラーです。分けて作成。
# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
include ActionController::HttpAuthentication::Token::ControllerMethods
# 認証用のヘルパーメソッド
def current_user
@current_user ||= authenticate_token
end
def logged_in?
!!current_user
end
def authenticate_user!
render json: { error: '認証が必要です' }, status: :unauthorized unless logged_in?
end
private
def authenticate_token
authenticate_with_http_token do |token, options|
begin
decoded = JWT.decode(token, Rails.application.credentials.secret_key_base, true, { algorithm: 'HS256' })
User.find(decoded[0]["sub"])
rescue JWT::DecodeError, ActiveRecord::RecordNotFound
nil
end
end
end
end
3. API用のユーザーコントローラー(サインアップ)
新規登録用のコントローラー。
# app/controllers/api/v1/users_controller.rb
module Api
module V1
class UsersController < ApplicationController
# POST /api/v1/signup
def create
@user = User.new(user_params)
if @user.save
token = generate_token(@user)
render json: {
user: { id: @user.id, email: @user.email },
token: token
}, status: :created
else
render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity
end
end
private
def user_params
params.require(:user).permit(:email, :password, :password_confirmation)
end
def generate_token(user)
payload = user.to_token_payload
JWT.encode(payload, Rails.application.credentials.secret_key_base, 'HS256')
end
end
end
end
4. API用のセッションコントローラー(サインイン/サインアウト)
ログインとログアウト用ののコントローラー。
# app/controllers/api/v1/sessions_controller.rb
module Api
module V1
class SessionsController < ApplicationController
# POST /api/v1/signin
def create
user = User.find_by(email: params[:email])
if user && user.authenticate(params[:password])
token = generate_token(user)
render json: {
user: { id: user.id, email: user.email },
token: token
}
else
render json: { error: "メールアドレスまたはパスワードが無効です" }, status: :unauthorized
end
end
# トークンベースの認証ではサーバーサイドでのログアウト処理は不要
# クライアントがトークンを破棄すれば良い
# 必要に応じてトークンのブラックリスト化などを実装可能
private
def generate_token(user)
payload = user.to_token_payload
JWT.encode(payload, Rails.application.credentials.secret_key_base, 'HS256')
end
end
end
end
5. API用のルーティング
routes.rb
を以下のように修正。これでモデルへアクセスすることができるようになる。
# config/routes.rb
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
# サインアップ
post 'signup', to: 'users#create'
# サインイン
post 'signin', to: 'sessions#create'
# 認証が必要なAPI
resources :protected_resource, only: [:index], constraints: lambda { |req| req.format == :json }
end
end
end
6. 認証済みAPIリソースの例
# app/controllers/api/v1/protected_resource_controller.rb
module Api
module V1
class ProtectedResourceController < ApplicationController
before_action :authenticate_user!
# GET /api/v1/protected_resource
def index
render json: {
message: "認証済みAPIにアクセスしました",
user: { id: current_user.id, email: current_user.email }
}
end
end
end
end
7. JWTのGemを追加
JWTを使用するために、Gemfileに以下を追加する必要があります:
# # Gemfileに以下を追加
gem "jwt"
gem "rack-cors"
gem "bcrypt"
JWTとCORSのGem追加後に実行
bundle install
8. CORS設定
CORS設定をしておきましょう。これがないと外の世界からアクセスするのを拒否されます。
CORSについて知りたい方は、MDNを読んでみてください。
API用にCORS設定を追加:
# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins '*' # 本番環境では特定のオリジンに制限することを推奨
resource '*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head],
expose: ['Authorization']
end
end
API リクエスト例
curlコマンドを使用してAPIリクエストを送信する例:
サインアップ(ユーザー登録):
curl -X POST http://localhost:3000/api/v1/signup \
-H "Content-Type: application/json" \
-d '{"user": {"email": "test@example.com", "password": "password123", "password_confirmation": "password123"}}'
トークンが含まれているレスポンスが返ってくれば成功。
curl -X POST http://localhost:3000/api/v1/signup \
-H "Content-Type: application/json" \
-d '{"user": {"email": "test@example.com", "password": "password123", "password_confirmation": "password123"}}'
{"user":{"id":1,"email":"test@example.com"},"token":"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOjEsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSJ9.Mb3aEdeFcWtvB9ke8JmcwZ_STfa5ZO-oGuLK0m7aTS4"}%
サインイン(ログイン):
curl -X POST http://localhost:3000/api/v1/signin \
-H "Content-Type: application/json" \
-d '{"email": "test@example.com", "password": "password123"}'
まとめ
今回は、テンプレートエンジンとDeviceではなくJWTを使用した認証機能を作ってみました。開発現場だとフロントエンドでAuth.jsを使ったりバックエンド側で認証機能を作ったりと分かれていることがあるようですが、JWT認証するのは最近はよくある方法のようです。
外部サービスで、Firebase/Supabaseの認証機能を使うこともありますが規模が大きいサービスとなると、バックエンド側で自作していることが多いです。
Discussion