概要
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でサーバーを立ち上げてからcurlでhttp://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