概要

Rails で JWT を使った認証APIの実装例を紹介します。
トークンにはアクセストークンとリフレッシュトークンの2種類を使用します。

実行環境は以下の通りです。

  • Ruby 3.1.2
  • Rails 7.0.4

ソースコード

今回作成したコードは Github でも公開しています。
nightswinger/jwt-rails

アプリケーションの作成

rails newコマンドでプロジェクトを作成します。
--apiオプションを付けることでAPIに必要なファイルのみ生成されます。

rails new jwt-api --api

ライブラリのインストール

必要なライブラリをインストールします。
以下をGemfileに追加してターミナルでbundle installを実行します。

gem "bcrypt", "~> 3.1.7"
gem "jwt"

また、Rails の APIモードでクッキーが使えるようにします。
app/controllers/application_controller.rbを編集します。

class ApplicationController < ActionController::API
  include ActionController::Cookies
end

config/application.rbを開いて、以下を追加します。

require_relative "boot"

require "rails/all"

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module Workspace
  class Application < Rails::Application
    # (省略)
    config.middleware.use ActionDispatch::Cookies
  end
end

Usersテーブルの作成

ログインに使用するカラムをもつusersテーブルを作成します。
refresh_tokenはログインなしでアクセストークンを再発行する時に使用します。

rails g model User email:string password_digest:string refresh_token:string
rails db:migrate

app/models/user.rbを開いてhas_secure_passwordを追加します。

class User < ApplicationRecord
  has_secure_password
end

こうすることでUserモデルのpassword属性に代入した値をセキュアなハッシュ化した文字列としてpassword_digestへ 自動的に代入してくれるようになります。

user = User.new
user.password = "password"
user.password_digest
# => "$2a$12$wyvPC6jDBD0w.Zr9BBHd1eDJUKfPfx/FIoKZPdFQEoh2iFhhObdhS"

Sessionsコントローラーの作成

ログインのリクエストを受け付けるsessionsコントローラを作成します。

rails g controller sessions

sessionsコントローラーを編集して、createアクションを追加します。

class SessionsController < ApplicationController
  def create
    user = User.find_by(email: params[:email])

    if user&.authenticate(params[:password])
      data = { user_id: user.id }
      access_token = JWT.encode({ data: data, exp: Time.current.since(30.seconds).to_i }, 'secret')
      refresh_token = JWT.encode({ data: data, exp: Time.current.since(1.day).to_i }, 'refresh_secret')

      user.update(refresh_token: refresh_token)

      cookies[:jwt] = refresh_token
      render json: { accessToken: access_token }, status: :ok
    else
      render status: :unauthorized
    end
  end
end

config/routes.rbを開いて、/authへPOSTリクエストを送るとcreateアクションが実行されるようにします。

Rails.application.routes.draw do
  post '/auth', to: 'sessions#create'
end

試しに、/authへリクエストを送りログインできるか確かめてみます。

まずはターミナルで以下を実行しデータベース上にユーザーを作成します。

rails r 'User.create(email: "testuser@example.com", password: "password")'

rails sでサーバーを立ち上げてからcurlhttp://localhost:3000/authへリクエストを送ります。
アクセストークンが返ってくれば成功です。

curl http://localhost:3000/auth -d '{"email": "testuser@example.com", "password": "password" }' -H 'Content-Type: application/json'
# =>{"accessToken":"eyJhbGciOiJIUzI1NiJ9.eyJkYXRhIjp7InVzZXJfaWQiOjF9LCJleHAiOjE2NjM5NDI3Mjl9.IKz7f_FQv5jd55NgUT2z6emphLJjYjXlRvX60Ud4EdE"}

次に、refreshアクションを追加します。
ここではリフレッシュトークンを使ってログインせずにアクセストークンを再発行できるようにします。

内容はクッキーとして送られてきたリフレッシュトークンを確認し、データベースと付き合わせて該当するユーザーが存在すれば新しいアクセストークンを返します。

class SessionsController < ApplicationController
  # (省略)
  def refresh
    return render status: :unauthorized unless cookies[:jwt]

    refresh_token = cookies[:jwt]
    user = User.find_by(refresh_token: refresh_token)
    return render status: :forbidden unless user

    payload, = JWT.decode(refresh_token, 'refresh_secret')
    return render status: :forbidden unless payload['data']['user_id'] == user.id

    data = { user_id: user.id }
    access_token = JWT.encode({ data: data, exp: Time.current.since(30.seconds).to_i }, 'secret')

    render json: { accessToken: access_token } , status: :ok
  end
end

refreshアクションが実行できるようにconfig/routes.rbを編集します。

Rails.application.routes.draw do
  post '/auth', to: 'sessions#create'
  get '/refresh', to: 'sessions#refresh'
end

/refreshへリクエストを送りアクセストークンが返ってくるかテストします。
一度ログインしてクッキーを取得して、その値とともにリクエストします。

curl -c cookie.txt http://localhost:3000/auth -d '{"email": "testuser@example.com", "password": "password" }' -H 'Content-Type: application/json'
curl -b cookie.txt http://localhost:3000/refresh
# => {"accessToken":"eyJhbGciOiJIUzI1NiJ9.eyJkYXRhIjp7InVzZXJfaWQiOjF9LCJleHAiOjE2NjQwMjEzMDR9.s1_xJccDlA6RZxPzUUhL5C2n0zsv08WXGENQZyCCw6M"}

トークンを利用したエンドポイントの作成

これでアクセストークンを生成できるようになりました。
次に、正しいアクセストークンをもつユーザーのみがアクセスできるエンドポイントを作成します。

以下のコマンドを実行して、Homeコントローラーを作成します。

rails g controller home index

Homeコントローラーを編集します。
indexアクションが実行される前にアクセストークンを検証するbefore_actionを追加します。

class HomeController < ApplicationController
  before_action :verify_token

  def index
    render plain: "Hello, Rails"
  end

  private

  def verify_token
    auth_header = request.headers["Authorization"]
    return render status: :unauthorized unless auth_header

    token = auth_header.split(" ")[1]

    begin
      payload, = JWT.decode(token, "secret")
    rescue JWT::ExpiredSignature
      return render status: :forbidden
    end
  end
end

実際に正しく動作するか確認します。

ログインしてからアクセストークンを付与してHome#indexへリクエストを送信します。
Hello, Railsが表示されれば成功です。

curl -c cookie.txt http://localhost:3000/auth -d '{"email": "testuser@example.com", "password": "password" }' -H 'Content-Type: application/json'
# => {"accessToken":"eyJhbGciOiJIUzI1NiJ9.eyJkYXRhIjp7InVzZXJfaWQiOjF9LCJleHAiOjE2NjQxMTgwNjl9.iBBUojCy9yS51Vt8k1cIACExQ2TkMkZqz92N6HBmF9E"}
curl http://localhost:3000/home/index -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJkYXRhIjp7InVzZXJfaWQiOjF9LCJleHAiOjE2NjQxMTgxNjV9.erWlLptgj4VaBS-G_uBrzlPKYTxY7I1b0Btbjpb81BI'
# => Hello, Rails