概要
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