【JWT】もう怖くない、JWT認証(RailsAPI編)

はじめに

今回はVueとRailsでJWT認証の実装ができず、泣いていた数日前の自分にあててかくラブレターです。
いわゆる「JWT完全に理解した」状態の今の私にできる、最大限の愛を込めた説明をしていきたいと思います。

用語一覧

この後の説明を理解するために、用語を確認しておきます。

用語 説明
JWT JSON Web Tokenの略。
属性情報をJSONデータ構造で表現したトークンの仕様。
<ヘッダー>.<ペイロード>.<署名>をエンコードする。
Bearer セキュリティトークンのうちその利用可否が「トークンの所有」のみで決定されるもの。
持参人トークン・署名なしトークンとも呼ばれる。
Authorization 認証情報を指定するヘッダ。
X-CSRF-Token CSRF対策としてリクエス送信先のページで正規のページからリクエストが送信されたか確認するために発行するトークン。

JWT認証の流れ(Vue.jsとRails

f:id:voyag:20210326173816p:plain
JWT

  1. ユーザーはemailとpasswordを送信してログイン処理を開始

  2. サーバー側でemailとpasswordからユーザーを認証

  3. 認証が完了したら、JWS tokenを生成してクライアント側に返却

  4. クライアント側は、JWS tokenをLocalStorage(Cookieなど)に保存

  5. LocalStorageに保存したJWS tokenをAuthorizationリクエストヘッダに付与して、サーバ側に送信

  6. サーバー側で、JWS tokenを認証し、ログイン中のユーザー情報などをクライアント側に返す

この記事では、Rails(server)側の処理である赤色の部分を解説します。

環境

gem

今回使用する主要なgemはこちら。

gem 'bcrypt', '~> 3.1.7' # Passwordの暗号化
gem 'sorcery' # ユーザー認証
gem 'jwt' # JWTtokenの生成

Sorceryの注意点

いつも通り、rails g sorcery:installをすると失敗するので注意!
今回はrails g sorcery:install --model Userで必要なファイルを生成しておきます。

大きな違いは、生成されるテーブルの名前。
rails g install:sorceryではUserテーブルが生成されますが、rails g sorcery:sorcery --model Userではusersテーブルが生成されます。

*このissueでも上がっているため、今後のバージョンによっては、修正されるかも?

github.com

Userモデル

rails g sorcery:install --model Userで生成されるmigrationファイルに、nameカラムを追加して、migrateしておきます!
なので、Userモデルはこんな感じ。

User
id
name: string
email: string
crypted_password: string
salt: string
created_at: datetime
updated_at: datetime

Userコントローラ

Userコントローラにはユーザー登録の処理を記述していきます!
また、ログイン後にユーザー情報を取得するshowメソッドも合わせて定義しています。

今回RailsAPIとして使用するため、コントローラのディレクトリは名前空間を使って区切っています。

rails g controller api/v1/user create show
class Api::V1::UsersController < ApplicationController
  before_action :authenticate!, only: %i[show]

  def create
    user = User.new(user_params)

    if user.save
      render json: user
    else
      render json: user.errors, status: :bad_request
    end
  end

  def info
    render json: current_user
  end

  private

  def user_params
    params.require(:user).permit( :name, :email, :password, :password_confirmation)
  end
end
  • 未定義のcurrent_userを使ってしまいましたが、後ほど定義していきます。

認証周りの処理をConcernに切り出す

module Api::UserAuthenticator
  extend ActiveSupport::Concern

  def current_user
    return @current_user if @current_user
    return unless bearer_token

    payload, = User.decode bearer_token
    @current_user ||= User.find_by(id: payload['user_id'])
  end

  def authenticate!
    return if current_user

    head :unauthorized
  end

  def bearer_token
    header = request.headers['Authorization']

    header.gsub(/^Bearer /, '') if header&.match(/^Bearer /)
  end
end
  • bearer_tokenAuthorizationに付与されているので、そこから取り出す処理
  • current_userbearer_tokenをデコードして取り出したuser_idを元にUserから探す処理
class ApplicationController < ActionController::Base
  include Api::UserAuthenticator
  protect_from_forgery with: :null_session
end
  • UserAuthenticatorはApplicationControllerにincludeしてどのControllerからでも使えるようにしておく
  • protect_from_forgery with: :null_sessionRailsAPIとして使う際の、CSRFのための設定

Sessionsコントローラ

続いて、ユーザー認証を行うSessionsコントローラを定義していきます。

class Api::V1::SessionsController < ApplicationController
  def create
    user = User.authenticate(params[:email], params[:password]) 

    if user
      token = user.create_tokens
      render json: { token: token }
    else
      head :unauthorized
    end
  end
end
  • emailとpasswordからUserを認証
  • 認証できたら、tokenを発行

ここで、また未定義のメソッドcreate_tokensが出てきましたので、定義していきます。

JwtTokenモジュール

JwtTokenモジュールは、modelのconcernsに切り出しておきます。

module JwtToken
  extend ActiveSupport::Concern

  SECRET_KEY = Rails.application.secrets.secret_key_base

  class_methods do
    def decode(token)
      JWT.decode(token, SECRET_KEY, true, algorithm: 'HS256')
    end
  end

  def create_tokens
    expires_in = 1.month.from_now.to_i # 再ログインを必要とするまでの期間を1ヶ月とした場合
    payload = { user_id: id }
    issue_token(payload.merge(exp: expires_in))
  end

  private

  def issue_token(payload)
    JWT.encode(payload, SECRET_KEY, 'HS256')
  end
end

ルーティング

最後にルーティングの設定をして、Rails側の設定は終了です。
routes.rbはこのように設定しておきます。

Rails.application.routes.draw do
  root to: 'home#index'

  namespace 'api' do
    namespace 'v1' do
      resources :sessions
      resources :users, only: %i[create info] do
        collection do
          get 'info'
        end
    end
  end
end

おわりに

今回の設定で、Rails(server)側のユーザーの新規登録・認証の設定が完了しました!
次回はVue(client)側の処理を説明します!

参考

当サイトは、Amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイトプログラムである、Amazonアソシエイト・プログラムの参加者です。