Zenn
🔨

Rails8 APIモードでJWT認証をやってみた

2025/02/27に公開

Viewを使わずに認証する

Rails8を使用して以前ノートにメモしておいたコードを使用してJWT認証ができるREST APIを作ってみました。

こちらが完成品

今回使用したGemはこちらになります。
https://rubygems.org/gems/bcrypt/versions/3.1.12?locale=en
https://rubygems.org/gems/rack-cors/versions/1.1.1?locale=en
https://rubygems.org/gems/jwt

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を読んでみてください。

https://developer.mozilla.org/ja/docs/Web/HTTP/CORS

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

ログインするとコメントできます