解決する課題
従来のWebアプリケーションでは、認証情報をCookiesを利用して保持し、ログイン状態を保つことを実現しています。
APIではブラウザを介さないことからCookiesを使用したセッションの保持は行えないため、認証時に生成したトークンを用いて認証済み(ログイン状態)であることを表現できます。
JSON Web Tokenの利用
認証時に生成するトークンは慣例通りJSON Web Token(JWT)を利用します。
Railsでは"jwt"というGemがあるので、簡単に扱うことができます。
アプリケーションによりますが、controller/concernsに認証用モジュールを設け、そこにJWT関連の処理をまとめて記載すると扱いやすいです。
暗号化・復号化の実装
手始めに、暗号化・復号化の実装例は下記のようになります。
app/controller/concerns下に作成しています。
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
ログインの実装
上記の暗号化処理を用いて生成したトークンをレスポンスヘッダに含めて渡します。
ログインを行うコントローラの実装例は下記です。
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を認証する実装例は下記の通りです。
先ほどのモジュールに処理を追加しています。
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などで実装します。
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の前に認証を行うようにできます。
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
コントローラ側
class UsersController < ApplcationController
include JwtAuthenticator
jwt_authenticate except: :create
...
end
さらに、ApplicationControllerに"include JwtAuthenticator"を記載すれば、省くこともできます。
都合に合わせて実装しましょう。
あとがき
実際に動作させている処理を元に記事を書きましたが、はしょったり部分的に切り出して書いた例なので、引用する際はご注意ください。
認証処理はアプリ開発最初の壁だと思います。
また、なあなあにしていたら後々に響いてくる部分でもあると思います。
少しでも参考になれば幸いです。