gem sorceryを使ってログイン機能を実装した話

## 何をしたか
`sorcery`というgemを使って、ログイン機能を実装しました。初めて使うgemで、日本語で見られるドキュメントも少なかったので、自分のためにも手順書とマニュアルを作っておこうと思います。

なお、実行環境は下記の通りです。

- `Rails 5.2.3`
- `Ruby 2.6.4`

## どんなgem?

認証のための最小限のロジックを積んだgemで、ユーザーの必要に応じてメソッドを増やしたり拡張をしていくことを目的としています。

▼公式ドキュメントはこちら
[Sorcery/sorcery](https://github.com/Sorcery/sorcery)

## 導入方法

この記事に記す手順は、概ね[公式のチュートリアル](https://github.com/Sorcery/sorcery/wiki/Simple-Password-Authentication)に沿っています。Gemの導入から、ログイン・ログアウト機能の実装までを記載しています。

以下、手順です。
まずは以下をGemfileに記載し...

```Ruby:Gemfile
gem 'sorcery'
```
`bundle install`します。

続けて、以下のコマンドを実行すると

```
$ rails generate sorcery:install
```
以下のファイルが生成されます。

```
create  app/models/user.rb
create  db/migrate/XXXXXXXXX_sorcery_core.rb
```

また、生成されたマイグレーションファイルの中身はこちらです。

```ruby:XXXXXXXXX_sorcery_core.rb
class SorceryCore < ActiveRecord::Migration[5.2]
  def change
    create_table :users do |t|
      t.string :email,            null: false
      t.string :crypted_password
      t.string :salt

      t.timestamps                null: false
    end
    add_index :users, :email, unique: true
  end
end
```

`crypted_password`（暗号化されたパスワード）はまだわかるとして、`salt`カラムが謎ですよね。[WEB上にこんな記事を発見しまして](https://stackoverflow.com/a/52420917)、以下、そのGoogle翻訳です。

> 標準の暗号化では、「ソルト」を使用して、パスワードハッシュの安全性を高めます。 Sorceryでは、パスワードの末尾にランダムな文字列を結合し、その文字列をソルトフィールドに記憶することでこれを行います。

なるほど。。。`rails db:migrate`をして、無事、導入完了です。

## 実装（１）：ユーザーの作成
### View
`crypted_password`や、`salt`カラムのデータはユーザーが直接変更・表示できないようにするため、ビューには書きません。その代わり、ビューでは`password`と`password_confirmation`を利用します。

```ruby:app/views/users/new.html.slim
= form_with model: @user, local: true do |f|
  = f.label :email
  = f.text_field :email
  = f.label :password
  = f.password_field :password
  = f.label :password_confirmation
  = f.password_field :password_confirmation
  = f.submit "登録"
```
### Controller

また、controllerの`strong_paramater`では、`password`と`password_confirmation`をそれぞれ受け取れるようにします。

```ruby:app/controllers/users_controller.rb
class UsersController < ApplicationController
  # ...
  private

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

### Model
モデルファイルは以下のように記述しました。

```ruby:app/models/user.rb
class User < ActiveRecord::Base
  authenticates_with_sorcery!

  validates :password, length: { minimum: 3 }, if: -> { new_record? || changes[:crypted_password] }
  validates :password, confirmation: true, if: -> { new_record? || changes[:crypted_password] }
  validates :password_confirmation, presence: true, if: -> { new_record? || changes[:crypted_password] }

  validates :email, uniqueness: true
end

```
まず、`authenticates_with_sorcery!`で、Uderモデルに`sorcery`による認証機能を持たせます。
`password`や`password_confirmation`のようなモデルにないカラムについては、

```Ruby
validates :password, confirmation: true, if: -> { new_record? || changes[:crypted_password] }
```
こちらの記述で、`sorcery`に、それが`crypted_password`カラムの内容であることを認識させます。

#### 余談
余談ですが、`new_redord?`メソッドは、[インスタンスが新規に作成されるものかどうかを判定するActive Recordのメソッド](https://railsguides.jp/active_record_validations.html#%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E5%AE%9F%E8%A1%8C%E6%99%82%E3%81%AE%E5%8B%95%E4%BD%9C)のようですね...:relaxed:(知らなかった＆＆便利！)

また、`if: ->`以下は[条件付きバリデーションの、シンボルやLambdasてやProcの部分](https://railsguides.jp/active_record_validations.html#%E6%9D%A1%E4%BB%B6%E4%BB%98%E3%81%8D%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3)ですね。苦手な部分なので、頑張って復習しないと。。。

### コンソールで試す
ここまでできたら、`rails c`をし、ユーザーが無事作成できることを確かめてみます。

```
# コンソール
> User.create!(email: "test@email.com", password: "test12345", password_confirmation: "test12345")

> User.last
=> #<User:0x00007fb37b8b4d28
 id: 1,
 email: "test@email.com",
 crypted_password:
  "$2a$10$dOdyPmn65bJ4LKjG5vdL2eU2Hypzpqz5fEgp4LTF8ai0lJtBKL.Q6",
 salt: "QapQbW9crUf1QixW2BD7",
 username: "testuser",
 created_at: Tue, 22 Dec 2020 14:48:37 UTC +00:00,
 updated_at: Tue, 22 Dec 2020 14:48:37 UTC +00:00>
```

無事、`crypted_password`と`salt`を持つユーザーが作成されました:relaxed:

#### 追記
そういえば、Usersコントローラーのメソッドを記載していなかったと気がついたので、users_controllerの全体像を記しておきます。

```Ruby:controllers/users_controller.rb
class UsersController < ApplicationController
  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)
    if @user.save
      redirect_to login_path
      flash[:notice] = 'ユーザーの作成に成功しました'
    else
      flash.now[:alert] = 'ユーザーの作成に失敗しました'
      render :new
    end
  end

  private

  def user_params
    params.require(:user).permit(:username, :email, :password, :password_confirmation)
  end
end
```
これで、画面からもユーザーを作成できるようになりました＾＾

## 実装(2)：ログイン／ログアウト機能の作成
それでは、ログイン・ログアウト機能を実装していきたいと思います。sorceryで提供しているメソッドについては、[公式のこちらの部分](https://github.com/Sorcery/sorcery#api-summary)に記述があります。

それによると、`login`メソッドは、`email`、`password`、`remenber_me`(デフォルト値はfalse)の３つの引数を取ることができると分かります。

```Ruby
login(email, password, remember_me = false)
```
今回は、`remenber_me`機能は利用していないので、`email`と`password`でログイン機能を実装します。まずはターミナルからコントローラーを生成し...

```
$ rails g controller UserSessions new create destroy
```
コントローラーを記載していきます。

```Ruby:app/controllers/user_sessions_controller.rb
class UserSessionsController < ApplicationController
  def new
  end

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

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

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

`redirect_back_or_to`はsorceryで[フレンドリーフォーワーディング](https://www.lanches.co.jp/blog/624)を実現する機能だそうで、[こちらの記述](https://github.com/Sorcery/sorcery/blob/d16fffa6e0eceb1b2e7b0337d53faab8227c01f0/lib/sorcery/controller.rb#L100)によると、`(url, flash_hash = {})`を引数にとるそうです。

`変数名 = {}`は引数としてハッシュをまるっと受け取るときの書き方。[チェリー本](https://www.amazon.co.jp/dp/B077Q8BXHC/)のp.165に書いてありました。

`logout`メソッドは説明は不要ですね。

### Router
ルーティングは以下のように定義しました。

```ruby:config/routes.rb
Rails.application.routes.draw do

  get 'login', to: 'user_sessions#new'  # 追記
  post 'login', to: 'user_sessions#create'  # 追記
  delete 'logout', to: 'user_sessions#destroy'  # 追記
  root 'users#new'

  resources :users, only: %i(new create)
end
```

ここも、特に説明はいらないと思います。

### View

最後に、Viewは以下のように定義しました。ここも、特に気遣いが必要だったことはないです。

```ruby:views/layouts/application.html.slim
  body
    = render 'shared/header'
    = yield
```

下記の`logged_in?`は**`sorcery`のメソッドで、ビューで使えるもの**であると[公式のドキュメント](https://github.com/Sorcery/sorcery#core)には書いてありました。

```ruby:views/shared/_header.html.slim
h1 AppName
nav
  - if logged_in?
    = link_to 'ログアウト', logout_path, method: :delete
  - else
    = link_to 'ログイン', login_path
```

```Ruby:views/user_sessions/new.html.slim
= form_with url: login_path, method: :post do |f|
  = f.label :email
  = f.text_field :email
  = f.label :password
  = f.password_field :password
  = f.submit 'ログイン'
```

## 完成！
以上で、ログイン・ログアウトメソッドが実装できました。
見た目等はこれから整えていきますが、想像以上に簡単でした＾＾
[![Image from Gyazo](https://i.gyazo.com/355275520520faee823bbd5f135f2db0.png)](https://gyazo.com/355275520520faee823bbd5f135f2db0)

[![Image from Gyazo](https://i.gyazo.com/fefe8ec7f9c291d654213a3dd72cf32f.png)](https://gyazo.com/fefe8ec7f9c291d654213a3dd72cf32f)

感想としては、初めて作ったこともあって、特にGem等を使わずに、Railsのデフォルトの機能を使ってシンプルなログインフォームを作ったぐらいの手間な気がしましたが、

deviseの時みたいに実装にクセがある感じもなかったですし、`sorcery`で実現できるメソッド一覧にはSNS認証などもあるようで、これからも色々試していきたいです＾＾