Rails8のauthenticationはdeviseの代わりになる？

## はじめに
この記事は[ソニックガーデン 若手プログラマ Advent Calendar](https://qiita.com/advent-calendar/2024/sonicgarden-junior) 2024の4日目の記事です。

Rails8からauthenticationという認証ジェネレーターが搭載されましたね。
もし基本的な実装の内容を知らないよという方はこちらの記事をおすすめします！
実装の内容が分かりやすく書いてます。

https://techracho.bpsinc.jp/hachi8833/2024_10_21/145343

機能も必要最低限ですし、本当にシンプルな設計ですね。
次から[devise](https://github.com/heartcombo/devise)の代わりとして使えるのでは？と思い、今回はカスタマイズを試みました。
自分と同じように考えている方の判断材料になれば嬉しいです。

## 開発環境
- ruby 3.3.6
- rails 8.0.0

## 満たしたい要件

### アカウント作成機能
- これは必須ですよね。デフォルトでは存在しません。

### ログイン状態でアクセスできるアクションを制御したい
ログイン状態でアカウント作成画面、ログイン画面、パスワード変更画面にアクセスできないようにしたい
- deviseだとデフォルトでこの仕様なので、違和感がありました。ログイン状態でこれらの画面にアクセスできるアプリって存在するんでしょうか？

### userとadminの2つのモデルで使えるようにしたい
- `rails g authentication`直後ではUserモデルが生成され、Userモデルだけでログインできるようになってますが、Adminというモデルでも同じように認証機能を使えるようにしたいです。

### userとadminの2つのモデルでログイン状態を管理できるようにしたい

- 管理者としては同じブラウザで管理者とユーザの両方でログインしたいことって結構ありますよね。こちらも生成された時点では複数のモデルに対応させるような作りではないので、カスタマイズが必要そうです。

これらのカスタマイズを実現する上で、あまりにも変更がありそうなら、やめにします。実装コストが増えるのもそうですし、ログインやアカウント作成などの元々保証されていた機能のテストを書く羽目になるので。

## 設計

### アカウント作成機能
ここはサインイン機能を参考にしてサクッと実装できそうです。

```diff_ruby:routes.rb
+   resources :users, only: %i[new create]
```

```app/controllers/users_controller.rb
class UsersController < ApplicationController
  allow_unauthenticated_access only: %i[new create]

  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)
    if @user.save
      start_new_session_for! @user
      redirect_to after_authentication_url, notice: 'アカウント作成しました'
    else
      render :new
    end
  end

  private

  def user_params
    params.require(:user).permit(:email_address, :password, :password_confirmation)
  end
end
```

```app/views/users/new.html.haml
= tag.div(flash[:alert], style: "color:red") if flash[:alert]
= tag.div(flash[:notice], style: "color:green") if flash[:notice]
= form_with model: @user do |form|
  = form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address]
  = form.password_field :password, required: true, autocomplete: "password", placeholder: "Enter your password", maxlength: 72
  = form.password_field :password_confirmation, required: true, autocomplete: "password_confirmation", placeholder: "Enter your password confirmation", maxlength: 72
  = form.submit "Sign up"
= link_to "Already have account?", new_session_path
```
### ログイン状態でアクセスできるアクションを制御したい

```diff_ruby:app/controllers/concerns/authentication.rb
class_methods do
  def allow_unauthenticated_access(**options)
    skip_before_action :require_authentication, **options
  end

+ def redirect_if_authenticated_access(**options)
+   before_action :redirect_if_authentication, **options
+ end

 private

 # 省略
 
+ def redirect_if_authentication
+   resume_session && redirect_to(root_url)
+ end
end
```

```diff_ruby:app/controllers/users_controller.rb
class UsersController < ApplicationController
  allow_unauthenticated_access only: %i[new create]
+ redirect_if_authenticated_access only: %i[new create]

```

### userとadminの2つのモデルで使えるようにしたい
こちらはポリモーフィック関連を利用して、adminモデルもuserモデルと同じようにsessionと1対多の関連付けできるようにします。
具体的な実装はこんな感じです。
- adminモデルを作成(userモデルと同じカラム)
- adminの認証用のページを作成する(userのroutes、コントローラ、ビューファイルをコピー)
- Current.userのようにCurrent.adminでadminを取得できるように
    ```diff_ruby:app/models/current.rb
    class Current < ActiveSupport::CurrentAttributes
      attribute :session
      delegate :user, to: :session, allow_nil: true
    + delegate :admin, to: :session, allow_nil: true
    ```
- adminとuserをsessionsモデルと関連づける
    ```diff_ruby:app/models/admin.rb
    class Admin < ApplicationRecord
      has_secure_password
    + has_many :sessions, as: :person, dependent: :destroy 
    
      normalizes :email_address, with: ->(e) { e.strip.downcase }
    ```
    ```diff_ruby:app/models/user.rb
    class User < ApplicationRecord
      has_secure_password
    - has_many :sessions, dependent: :destroy
    + has_many :sessions, as: :person, dependent: :destroy
    
      normalizes :email_address, with: ->(e) { e.strip.downcase }
    ```
    ```diff_ruby:app/models/session.rb
    class Session < ApplicationRecord
    - belongs_to :admin
      belongs_to :person, polymorphic: true
    ```
これぐらいの変更ならまだ許容できる…？
### userとadminの2つのモデルでログイン状態を管理できるようにしたい(断念)
- まずuserとadminで別のsessionで管理するようにする
```diff_ruby:app/controllers/concerns/authentication.rb
- def start_new_session_for!(user)
-   user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
-     Current.session = session
-     cookies.signed.permanent[:session_id] = { value: session.id, httponly: true, same_site: :lax }
+ def start_new_session_for!(person)
+   person.sessions.create!(person_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
+     Current.session = session
+     cookies.signed.permanent[:"session#{person.class}_id"] = { value: session.id, httponly: true, same_site: :lax }
    end
  end
```
- `require_authentication`メソッドにおいて、adminなのかuserなのか検証したいモデルを把握する必要がある
  - このメソッドに対して検証したい情報を外部から送る必要があるので、includedではなくclass_methodsにする
- ログアウトやログイン状態を確認するメソッドにも変更が必要になってくる…

ちょっと変更が多そうですね・・・
やめ。

## まとめ
現状のauthenticationだと複数モデルに対応しておらず、かなりカスタマイズが必要そうなので、もう少しdeiviseにお世話になろうと思います。シンプルな設計であること、Railsの標準機能であることは個人的にはかなりポイントが高いので、今後のアップデートに期待してます！

最後まで読んでいただき、ありがとうございました〜！
明日は [@gotchane](https://qiita.com/gotchane) さんが執筆予定です。お楽しみに〜！