Sorceryで認証機能を実装してみた

## 1.はじめに
[Railsチュートリアル](https://railstutorial.jp/)や[Devise](https://github.com/heartcombo/devise)を使ったログイン機能の記事はよく見るけど、[Sorcery](https://github.com/Sorcery/sorcery/)の記事は比較的少ないと思ったので、この記事を書きました。

皆様の学習の一助になれれば幸いです🙏

**目次**
[1. はじめに](#1-はじめに) 
[2. 導入方法](#2-導入方法) 
[3. ログイン機能の実装](#3-ログイン機能の実装) 
[4. 動作確認](#4-動作確認) 
[5. 終わりに](#5-終わりに) 
[6. 参考記事](#6-参考記事) 

**開発環境**
Ruby 3.2.3
Rails 7.1.3
Sorcery(gem)
jQuery 3.7.1

## 2. 導入方法

### sorcery
[sorceryの公式ドキュメント](https://github.com/Sorcery/sorcery/?tab=readme-ov-file#installation)に沿って導入していきます。

```.Gemfile
gem 'sorcery'
```

今回はログイン情報を記憶する機能も実装するため、remember_me を付与しています。
```
$ rails generate sorcery:install remember_me 
```

作成されたマイグレーションファイルは以下になります。
```db/migrate/20240211071358_sorcery_core.rb
class SorceryCore < ActiveRecord::Migration[7.0]
  def change
    create_table :users do |t|
      t.string :email,            null: false, index: { unique: true }
      t.string :crypted_password
      t.string :salt

      t.timestamps                null: false
    end
  end
end
```

```db/migrate/20240216115807_sorcery_remember_me.rb
class SorceryRememberMe < ActiveRecord::Migration[7.0]
  def change
    add_column :users, :remember_me_token, :string, default: nil
    add_column :users, :remember_me_token_expires_at, :datetime, default: nil

    add_index :users, :remember_me_token
  end
end
```


```
$ rails generate migration AddColumnUsers name:string
```

```db/migrate/20240217004047_add_column_users.rb
class AddColumnUsers < ActiveRecord::Migration[7.0]
  def change
    add_column :users, :name, :string
  end
end
```
必要に応じて手動でカラムを追加してください。
上記の場合はnameカラムを追加しています。


```config/initializers/sorcery.rb
Rails.application.config.sorcery.submodules = [:remember_me]

# Here you can configure each submodule's features.
Rails.application.config.sorcery.configure do |config|
  config.user_config do |user|
    user.stretches = 1 if Rails.env.test?
    user.remember_me_for = 180 #変更
    end
    config.user_class = "User"
end

```
remember_me_tokenの有効期限を設定します。
期限は例として180秒で動作確認用に設定しています。

### jQuery
flashメッセージを実装するためにjQueryを導入します。
以下はjQueryのCDN最新バージョン（2024年4月時点）を読み込む方法です。
Rails7では[Hotwire(Turbo)](https://zenn.dev/shita1112/books/cat-hotwire-turbo/viewer/abstract)がデフォルトで実装されているため、そちらを使用しています。
```app/views/layouts/application.html.erb
<html>
  <head>
    <title>minapp</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
    <%= javascript_importmap_tags %>
    <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script> <%#追加 %>
  </head>

    <body>
     <div>
      <% flash.each do |message_type, message| %>
        <div class="flash-message <%= message_type %>"><%= message %></div>
      <% end %>
     </div>
     <div id="nav">
      <% if logged_in? %>
        <%= link_to "ログアウト", logout_path, data: { turbo_method: :delete } %>
      <% else %>
        <%= link_to "新規登録", new_user_path %> |
        <%= link_to "ログイン", login_path %>
      <% end %>
    </div>
    <%= yield %>
  </body>
</html>
```

FlashメッセージのCSS
```app/assets/stylesheets/application.css
.flash-message {
  border-radius: 5px;
  padding: 16px 24px;
  margin: 16px 0px;
}
 
.flash-message.notice {
  background-color: #bcdfff;
  color: #0067C0;
  border: solid 1px #0067C0;
}
 
.flash-message.alert {
  background-color: #ffd4d1;
  color: #930403;
  border: solid 1px #930403;
}
```

```app/javascript/application.js
import $ from 'jquery';
import "jquery-ujs";
import "@hotwired/turbo-rails";
import "controllers";

$(function() {
  $('.flash-message').fadeOut(4000);
  console.log($('.flash-message'));
});
```

```app/assets/config/manifest.js
//以下を追加
//= require jquery
//= require jquery_ujs
//= require_tree .
```

## 3. ログイン機能の実装

### モデル
```app/models/user.rb
class User < ApplicationRecord
  authenticates_with_sorcery!

  validates :email, uniqueness: true, presence: true
  validates :password, length: { minimum: 3 }, if: -> { new_record? || changes[:crypted_password] }
end
```
```authenticates_with_sorcery!```でsorceryの認証を有効にしています。

### ルーティング
```config/routes.rb
Rails.application.routes.draw do
  get 'login' => 'sessions#new', as: :login
  post 'login' => "sessions#create"
  delete 'logout' => 'sessions#destroy', as: :logout

  root to: "samples#index"

  resources :users, except: [:index]

end
```

### コントローラー
```app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  skip_before_action :require_login, only: [:new, :create]

  def create
    @user = login(params[:email], params[:password], params[:remember_me])

    if @user
      redirect_back_or_to(root_path, notice: 'ログインに成功しました')
    else
      flash.now[:alert] = 'ログインに失敗しました'
      render action: 'new'
    end
  end

  def new
    @user = User.new
  end

  def destroy
    remember_me!
    forget_me!
    logout
    redirect_to(login_path, notice: 'ログアウトしました')
  end
end

```
- createアクションにて```params[:remember_me]```を追加することで、ログイン時にremember_me_tokenを発行し、ログイン情報を記憶できます。
- require_loginメソッドはloginしているかどうかを検証します。ログインを必要としないページには、このskip_before_actionを適用しています。
- destroyアクションにて ```remember_me!```、```forget_me!```を追加することでログアウト前にremember_meトークンを削除しています。

### ビュー
```app/views/sessions/new.html.erb
<h1>ログイン</h1>

<%= form_with url: login_path, method: :post, data: { turbo: false } do |f| %>
  <div class="field">
    <%= f.label "メールアドレス" %><br />
    <%= f.text_field :email %>
  </div>
  <div class="field">
    <%= f.label "パスワード" %><br />
    <%= f.password_field :password %>
  </div>
  <div class="field">  
    <%= check_box_tag :remember_me, params[:remember_me], true %>  
    <%= label_tag :remember_me %>  
  </div>  
  <div class="actions">
    <%= f.submit "ログイン", data: { turbo: false } %>
  </div>
<% end %>

<%= link_to 'トップページへ戻る', root_path, data: { turbo: false } %>
```
ログインページのビューです。
remember_meのチェックボックスを追加しています（デフォルトでTrueにしています）

## 4. 動作確認
 1. http://localhost:3000/login でブラウザにアクセスします。
 2. デベロッパーツールを開く。
 3. メールアドレス、パスワード、remember_meのチェックボックスをONにしてログインする。
 4. ログイン成功し、Flashメッセージが表示される。この時、デベロッパーツールの「アプリケーションタブ」のCookie内を確認すると、session情報とremember_meトークンが発行されます。
 5. ログアウトする
 

 ![sorcery.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/677000/04a5fcef-5fd8-cebb-f211-fb347cddc43a.gif)


## 5. 終わりに
- deviseは機能が豊富な反面、カスタマイズするためにはdevise内部の機能を追う必要があり、苦労しました。
- 対してsorceryは軽量で必要最低限の実装で実現できました。比較的シンプルにコードが書けて、認証に関する知識が必要になるものの、カスタマイズはdeviseよりも柔軟に行いやすいという印象でした。
- どちらもメリットデメリットがあり、プロジェクトに応じてライブラリを選定することが大事だと思いました。

## 6. 参考記事
[Sorcery（公式ドキュメント）](https://github.com/Sorcery/sorcery/?tab=readme-ov-file#installation)
[[Ruby on Rails]sorceryによる認証 – (2)ユーザ名による認証とRemember-Me](https://dev.classmethod.jp/articles/ruby-on-rails_sorcery_auth_no2/#toc-4)
[devise（公式ドキュメント）](https://github.com/heartcombo/devise)