LoginSignup
69
71

More than 3 years have passed since last update.

JSON Web Token(JWT)を用いたAPIの認証の実装(Rails)

Posted at

解決する課題

従来のWebアプリケーションでは、認証情報をCookiesを利用して保持し、ログイン状態を保つことを実現しています。

APIではブラウザを介さないことからCookiesを使用したセッションの保持は行えないため、認証時に生成したトークンを用いて認証済み(ログイン状態)であることを表現できます。

JSON Web Tokenの利用

認証時に生成するトークンは慣例通りJSON Web Token(JWT)を利用します。
Railsでは"jwt"というGemがあるので、簡単に扱うことができます。

アプリケーションによりますが、controller/concernsに認証用モジュールを設け、そこにJWT関連の処理をまとめて記載すると扱いやすいです。

暗号化・復号化の実装

手始めに、暗号化・復号化の実装例は下記のようになります。
app/controller/concerns下に作成しています。

jwt_authenticator.rb
module JwtAuthenticator
  require "jwt"

  SECRET_KEY = Rails.application.secrets.secret_key_base

  # 暗号化処理
  def encode(user_id)
    expires_in = 1.month.from_now.to_i # 再ログインを必要とするまでの期間を1ヶ月とした場合
    preload = { user_id: user_id, exp: expires_in }
    JWT.encode(preload, SECRET_KEY_BASE, 'HS256')
  end

  # 復号化処理
  def decode(encoded_token)
    decoded_dwt = JWT.decode(encoded_token, SECRET_KEY_BASE, true, algorithm: 'HS256')
    decoded_dwt.first
  end

end

暗号化処理では、ユーザーのIDをカスタムクレームとして含め、jwt標準のクレームのexpと合わせてPayloadを作成しています。
これを秘密鍵を用いて、HS256のアルゴリズムで暗号化します。

復号化処理では、秘密鍵を用いて復号化を行います。
第三引数のTrueはトークンの検証を行うかどうかなので、Trueにしましょう。
復号化を行うと、配列が返却され、1つ目の要素がPayloadとなっているので、それを返却しています。

ちなみに、もしPayloadに各種標準クレームを含めた場合、第四引数以降には標準クレームの検証を行うかどうか、verify_iat: trueのように指定できます。
この辺りの詳細はJWTについての資料を参考にしてください。
個人的にはこのサイトを参考にさせていただきました。
https://ruby-rails.hatenadiary.com/entry/20181118/1542521671

ログインの実装

上記の暗号化処理を用いて生成したトークンをレスポンスヘッダに含めて渡します。
ログインを行うコントローラの実装例は下記です。

sessions_controller.rb
class SessionsController < ApplicationController
  include JwtAuthenticator

  def create
    # ログインIDを元にユーザーを取得
    @current_user = User.find_by(sign_in_id: params[:sign_in_id])
    # パスワードによる認証
    if @current_user&.authenticate(params[:password])
      # jwtの発行
      jwt_token = encode(@current_user.id)
      # レスポンスヘッダーにトークンを設定
      response.headers['X-Authentication-Token'] = jwt_token
      # 任意のレスポンスを返す
      render json: @current_user
    else
      raise UnableAuthorizationError.new("ログインIDまたはパスワードが誤っています。") 
    end
  end
end

ID、パスワードによる認証ができた場合、レスポンスヘッダに生成したトークンを設定します。
例えばこのAPIを利用したアプリケーションでは、このヘッダからトークンを取得して、以降のAPIに含めて実行します。

JWTの認証

リクエストヘッダーに含まれてきたJWTを認証する実装例は下記の通りです。
先ほどのモジュールに処理を追加しています。

jwt_authenticator.rb
module JwtAuthenticator
  require "jwt"

  SECRET_KEY = Rails.application.secrets.secret_key_base

  # ヘッダーの認証トークンを復号化してユーザー認証を行う
  def jwt_authenticate
    raise UnableAuthorizationError.new("認証情報が不足しています。") if request.headers['Authorization'].blank?
    # 下記のようにヘッダーに設定されているとして、トークンをヘッダーから取得する。
    # headers['Authorization'] = "Bearer XXXXX..."
    encoded_token = request.headers['Authorization'].split('Bearer ').last
    # トークンを復号化する(トークンが復号できない場合、有効期限切れの場合はここで例外が発生します。)
    payload = decode(encoded_token)
    # Payloadから取得したユーザーIDでログインしているユーザー情報を取得
    @current_user = User.find_by(id: payload[:user_id])
    raise UnableAuthorizationError.new("認証できません。") if @current_user.blank?
    @current_user
  end

  # 暗号化処理
  def encode(user_id)
    ...
  end

  # 復号化処理
  def decode(encoded_token)
    ...
  end

end

この認証処理を各コントローラで、before_actionなどで実装します。

users_controller.rb
class UsersController < ApplicationController
  include JwtAuthenticator
  before_action :jwt_authenticate, except: :create

  # 非ログイン状態で行うユーザー登録処理
  def create
    user = User.new(user_params)
    if user.save
      render json: user
    else
      render json: user.errors
    end
  end

  # ログイン中でないと実行してはいけない処理
  def show
    render json: @current_user
  end

  private

  def user_params
    params.require(:user).permit(:name, :sign_in_id, :password)
  end
end

応用

全てのControllerにbefore_actionを設定するのは面倒なので、
認証用のモジュールを下記のように実装すれば、includeして、jwt_authenticateを記述するだけでactionの前に認証を行うようにできます。

jwt_authenticator.rb
module JwtAuthenticator
  require "jwt"
  # ↓を必ずextendする
  extend ActiveSupport::Concern

  # jwt_authenticateを記載したコントローラにprepend_before_actionを定義する
  module ClassMethods
    def jwt_authenticate(**options)
      class_eval do
        prepend_before_action :jwt_authenticate!, options
      end
    end
  end

  ...
end

コントローラ側

users_controller.rb
class UsersController < ApplcationController
  include JwtAuthenticator
  jwt_authenticate except: :create

  ...
end

さらに、ApplicationControllerに"include JwtAuthenticator"を記載すれば、省くこともできます。
都合に合わせて実装しましょう。

あとがき

実際に動作させている処理を元に記事を書きましたが、はしょったり部分的に切り出して書いた例なので、引用する際はご注意ください。

認証処理はアプリ開発最初の壁だと思います。
また、なあなあにしていたら後々に響いてくる部分でもあると思います。
少しでも参考になれば幸いです。

69
71
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
69
71