👨‍✈️

#2 初学者がrailsとkotlinでTODOアプリを作成する記録 ~認証・認可~

2023/04/07に公開

前回の続き
https://zenn.dev/kazu_engineer/articles/87cd8efd41c556

認証・認可の設計

  • Railsでの認証機能には、Devise gemを使用する

    • Deviseは、Railsでよく使われる認証機能を提供するgemで、シンプルでセキュアな認証機能を簡単に実装できます。
  • 認可には、Pundit gemを使用する

    • Punditは、Railsアプリケーションでの認可機能を提供するgemです。アクセス権限を制御するポリシーを定義し、簡潔なコードで認可を実現できます。

処理の流れ

  1. クライアント側(Kotlin)でユーザーIDとパスワードを入力し、ログインボタンを押下する。
  2. 入力値のバリデーションを行い、問題がなければサーバー側のログインAPIを呼び出す。
  3. サーバー側(Rails)で入力された値とデータベースの値を照合し、一致した場合はJWTトークンを生成してレスポンスとして返す。一致しない場合はエラーメッセージを返す。
  4. クライアント側でレスポンスを受け取り、成功の場合はトークンを保存し、TOP画面に遷移する。失敗の場合はエラーメッセージを表示する。

サーバー側(Rails)

  1. Devise gemをインストールして、Userモデルを生成する
gem 'devise'
bundle install
rails generate devise:install
rails generate devise User
  1. Pundit gemをインストールして、ポリシーを生成する
gem 'pundit'
bundle install
rails generate pundit:install
  1. JWTを扱うために、jwt gemを追加してインストールする
gem 'jwt'
bundle install
  1. Deviseの設定ファイルを編集し、JWTを使ったトークン認証ができるようにする

  2. API用のコントローラーを作成し、ログイン用のエンドポイントを実装する
    例: sessions_controller.rb

ruby
class SessionsController < ApplicationController
  def create
    user = User.find_by(email: params[:email])
    
    if user && user.valid_password?(params[:password])
      token = generate_jwt(user)
      render json: { token: token }, status: :ok
    else
      render json: { error: 'Invalid email or password' }, status: :unauthorized
    end
  end

  private

  def generate_jwt(user)
    JWT.encode({ user_id: user.id, exp: Time.now.to_i + 3600 }, Rails.application.secret_key_base, 'HS256')
  end
end

ここでは、入力されたメールアドレスとパスワードを使ってユーザーを検索し、認証が成功した場合にはJWTトークンを生成して返します。

  1. ルーティング設定を追加して、ログイン用のエンドポイントを有効にする
    例: config/routes.rb
ruby
Rails.application.routes.draw do
  # ...
  post 'login', to: 'sessions#create'
end
  1. ApplicationControllerに認証・認可機能を追加する
    例: application_controller.rb
ruby
class ApplicationController < ActionController::Base
  include Devise::Controllers::Helpers
  include Pundit

  before_action :authenticate_user!

  private

  def authenticate_user!
    token = request.headers['Authorization']
    return head :unauthorized unless token.present?

    payload = decode_jwt(token)
    return head :unauthorized unless payload

    user_id = payload['user_id']
    @current_user = User.find_by(id: user_id)
    head :unauthorized unless @current_user
  end

  def decode_jwt(token)
    JWT.decode(token, Rails.application.secret_key_base, true, { algorithm: 'HS256' })[0]
  rescue JWT::DecodeError
    nil
  end
end

ここでは、リクエストヘッダーに含まれるJWTトークンを検証して、認証を行います。

アクセス制御の実装(Pundit)

  1. 各リソースに対応するポリシーを生成する
    例:タスクリソースに対するポリシーを生成する場合
rails generate pundit:policy Task
  1. 生成されたポリシーにアクセス制御のルールを記述する
    例:タスクリソースに対するアクセス制御のルールを定義する
    app/policies/task_policy.rb
ruby
class TaskPolicy < ApplicationPolicy
  def create?
    user.present?
  end

  def update?
    user.present? && record.user == user
  end

  def destroy?
    update?
  end
end

この例では、タスクの作成はログインユーザーに限定し、更新・削除はタスクの作成者にのみ許可します。

  1. 各コントローラーでアクセス制御を行う
    例:タスクコントローラーにアクセス制御を実装する
    app/controllers/tasks_controller.rb
ruby
class TasksController < ApplicationController
  before_action :set_task, only: [:update, :destroy]

  def create
    @task = current_user.tasks.build(task_params)
    authorize @task
    # 以降、タスクの保存処理
  end

  def update
    authorize @task
    # 以降、タスクの更新処理
  end

  def destroy
    authorize @task
    # 以降、タスクの削除処理
  end

  private

  def set_task
    @task = Task.find(params[:id])
  end

  def task_params
    params.require(:task).permit(:title, :description, :due_date, :priority, :category)
  end
end

ここでは、authorizeメソッドを使ってアクセス制御を実行します。対象となるリソースに対応するポリシーのメソッドが実行され、アクセスが許可されない場合はPundit::NotAuthorizedErrorが発生します。

  1. Pundit::NotAuthorizedErrorに対する処理を追加する
    app/controllers/application_controller.rb
ruby
class ApplicationController < ActionController::Base
  # ...
  include Pundit
  rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized

  private

  # ...

  def user_not_authorized
    render json: { error: 'You are not authorized to perform this action.' }, status: :forbidden
  end
end

これでサーバー側(Rails)の認証・認可機能が実装できました。
次に、正しく動作するかを確認する手順を説明します。

動作確認手順

  1. ユーザー登録機能を追加する
    Deviseが提供するユーザー登録機能を使って、テスト用のユーザーを作成できるようにします。
    config/routes.rb
ruby
Rails.application.routes.draw do
  devise_for :users, only: :registrations
  # ...
end

これにより、/users/sign_upエンドポイントでユーザー登録ができるようになります。

  1. Postmanやcurlなどのツールを使って、APIの動作確認を行う
    まず、ユーザー登録APIを呼び出してユーザーを作成します。次に、ログインAPIを使って認証トークンを取得し、そのトークンを使って認証が必要なAPIを呼び出して動作を確認します。

例:

  • ユーザー登録API: POST /users/sign_up
  • ログインAPI: POST /login
  • タスク作成API: POST /tasks
  1. Railsのコントローラーでアクションが実行される前に、認証・認可が正しく行われているかを確認する
    デバッガ(例:binding.pry)やRails.loggerを使って、認証・認可処理が正しく行われているかを確認します。

  2. Railsのテスト(RSpecなど)を使って、認証・認可機能のテストを書く
    自動化されたテストを書くことで、今後の開発で認証・認可機能に影響が出る変更があった場合でも、機能が正しく動作するかを確認できます。

以上の手順で、実装した認証・認可機能が正しく動作するかを確認できます。問題があった場合は、エラーメッセージやログを参考にしてデバッグし、実装を修正してください。

Discussion