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

# 解決する課題

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

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

# JSON Web Tokenの利用

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

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

## 暗号化・復号化の実装

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

```ruby: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 # 再ログインを必要とするまでの期間を１ヶ月とした場合
    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にしましょう。
復号化を行うと、配列が返却され、１つ目の要素がPayloadとなっているので、それを返却しています。

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

## ログインの実装

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

```ruby: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を認証する実装例は下記の通りです。
先ほどのモジュールに処理を追加しています。

```ruby: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などで実装します。

```ruby: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の前に認証を行うようにできます。

```ruby: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
```


コントローラ側

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

  ...
end
```

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

# あとがき

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

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