Authlogic + Railsで認証の仕組みをシンプルに作る

[Authlogic](https://github.com/binarylogic/authlogic)というgemを使ってRailsに認証の仕組みを作る方法を紹介します。Authlogicはシンプルなライブラリで、Deviseのようにviewやcontrollerは提供してくれません。自分でマイグレーションファイルを作って、コントローラと認証画面を用意してあげる必要があります。面倒くさそうに感じますが、自分で作りたいようにRailsに乗って作れるという良さがあります。シンプルです。

認可に関しては[Pundit + Railsで認可の仕組みをシンプルに作る](http://qiita.com/zaru/items/8bf7b41b33f3f55bd27d)を参考にしてみてください。

## 前提

- Rails 5.1
- Ruby 2.4
- Authlogic 3.5.0
- RSpec 3.5

### 紹介する実装例

- メールアクチベーション
- パスワードリセット
- WebAPIキー
- テストコード

## 最低限の実装

まずは単純にユーザ登録とログインの実装をしてみます。


### マイグレーション

ユーザ認証に必要なカラムを追加します。認証させたいモデルが存在しない場合は作ります。今回はUserモデルを作ることにします。

```
$ rails g model user email crypted_password password_salt persistence_token
```

ログイン情報などの記録もしたい場合は下記のようにマイグレーションファイルを手動で修正してください。これらのフィールドはAuthlogicが自動でアップデートしてくれます。

```ruby
class CreateUser < ActiveRecord::Migration
  def change
    create_table :users do |t|
      t.string    :email
      t.string    :crypted_password
      t.string    :password_salt
      t.string    :persistence_token
      
      # ログイン情報など
      t.integer   :login_count, default: 0, null: false
      t.integer   :failed_login_count, default: 0, null: false
      t.datetime  :last_request_at
      t.datetime  :current_login_at
      t.datetime  :last_login_at
      t.string    :current_login_ip
      t.string    :last_login_ip

      t.timestamps
    end
  end
end
```

### Authlogicをモデルに適用

#### app/models/user.rb

UserモデルにAuthlogicを適用します。

```ruby
class User < ApplicationRecord
  acts_as_authentic
end
```

`acts_as_authentic` のブロックでオプションを渡せます。例えば暗号アルゴリズムを変更したい場合。

```ruby
class User < ApplicationRecord
  acts_as_authentic do |c|
    c.crypto_provider = Authlogic::CryptoProviders::BCrypt
  end
end
```

なお、デフォルトでは `SCrypt` になっています。

#### app/models/user_session.rb

認証のセッションで使うモデルファイルを作成します。これはActiveRecordではなく、 ` Authlogic::Session::Base` を継承するクラスファイルです。

```ruby
class UserSession < Authlogic::Session::Base
  secure Rails.env.production?
  httponly true
end
```

ここでいくつかのオプションを設定できます。例はプロダクション環境ではCookieにsecure属性を付けて、どの環境でもhttponly属性を付ける設定になっています。これ以外の設定はドキュメントを参照してください。

- [Authlogic::Session::Cookies::Config](http://www.rubydoc.info/gems/authlogic/Authlogic/Session/Cookies/Config)

### ユーザ登録ページ

#### app/controllers/users/registrations_controller.rb

パット見て普通のRailsで作るようなcontrollerと同じです。ここが良いです。誰でも分かるし、変更もしやすい。必要なのは入力画面と作成アクションだけなので `new` と `create` だけ用意しています。

```ruby
class User::RegistrationsController < ApplicationController

  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)

    if @user.save
      redirect_to "/"
    else
      render action: :new
    end
  end

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

#### app/views/user/registrations/new.html.erb

```ruby
<%= form_for @user, url: user_registrations_path do |f| %>
  <p>
    <%= f.label :email %>
    <%= f.text_field :email %>
  </p>
  <p>
    <%= f.label :password %>
    <%= f.password_field :password %>
  </p>
  <p>
    <%= f.label :password_confirmation %>
    <%= f.password_field :password_confirmation %>
  </p>
  <%= f.submit "sign up" %>
<% end %>
```

#### config/routes.rb

```ruby
Rails.application.routes.draw do
  namespace :user do
    resources :registrations, only: [:new, :create]
  end
end
```

以上でユーザ登録の画面ができました。`email` などのバリデーションはAuthlogicが提供しくれるので、特に何もしなくても大丈夫です。

### ログイン画面

#### app/controllers/user/sessions_controller.rb

次はログイン画面です。こちらも先程の登録と同じような感じです。違いはUserSessionクラスにパラメータを投げるだけです。 `current_user_session` というメソッドは後でApplicationControllerに定義します。

```ruby
class User::SessionsController < ApplicationController
  def new
    @user_session = UserSession.new
  end

  def create
    @user_session = UserSession.new(user_session_params.to_h)
    if @user_session.save
      redirect_to "/"
    else
      render :new
    end
  end

  def destroy
    current_user_session.destroy
    redirect_to "/"
  end

  private

  def user_session_params
    params.require(:user_session).permit(:email, :password)
  end
end
```

#### app/views/user/sessions/new.html.erb

```ruby
<%= form_for @user_session do |f| %>
  <% if @user_session.errors.any? %>
  <div id="error_explanation">
    <h2><%= pluralize(@user_session.errors.count, "error") %> prohibited:</h2>
    <ul>
      <% @user_session.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
    </ul>
  </div>
  <% end %>
  <%= f.label :email %><br />
  <%= f.text_field :email %><br />
  <br />
  <%= f.label :password %><br />
  <%= f.password_field :password %><br />
  <br />
  <%= f.submit "Login" %>
<% end %>
```

#### config/routes.rb

```ruby
Rails.application.routes.draw do
  get "sign_in" => "user/sessions#new"
  delete "sign_out" => "user/sessions#destroy"
  namespace :user do
    resources :registrations, only: [:new, :create]
    resources :sessions, only: :create
  end
end
```

#### controllers/application_controller.rb

Deviseで提供されている `current_user` メソッドと同様のことができると便利なので作ります。UserSessionクラスに問い合わせるといまアクセスしているユーザのCookieを読み取ってセッション情報を作ってくれます。その中にUserモデルのオブジェクトが入っているので、それを返しているだけです。

```ruby
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  helper_method :current_user_session, :current_user

  private
    def current_user_session
      return @current_user_session if defined?(@current_user_session)
      @current_user_session = UserSession.find
    end

    def current_user
      return @current_user if defined?(@current_user)
      @current_user = current_user_session && current_user_session.user
    end

    # ログイン必須controllerのbefore_actionで呼ぶ
    def require_login
      return redirect_to sign_in_path unless current_user
    end
end
```

### ログイン済み判定

Userモデルの `persistence_token` と、Cookieの `user_credentials` を比較しています（モデル名によってCookieKeyは変わる）。

Cookieの `user_credentials` には `persistence_token` と `record_id` が `::` で連結されていて、 `record_id` をもとにDBに保存されている `persistence_token` を探して一致すれば認証という流れになっている。

- 該当コード: [authlogic/session/cookies.rb](https://github.com/binarylogic/authlogic/blob/7c03eb79df7f0b05fe3fe1ea13d097ab9c92452f/lib/authlogic/session/cookies.rb#L208)

---

以上で、ユーザ認証の実装は終了です。ソースコードだけで見ると面倒くさそう…って思うかもしれませんが、やってみると普段Railsで開発しているのとさほど変わらない程度の実装です。見えないところが少ないのでコントロールしやすいです。

### ユーザステータス

`Authlogic::Session::MagicStates` クラスがユーザのステータスを管理する機能を提供しています。  `active` / `approved` / `confirmed` の3つ。それぞれ `user.active?` などと問い合わせることができます。enumっぽい。メールでのアクチベーションを実装する場合や、運営側からの承認フラグを実装したいときなどに利用します。DBにフィールドが必要なので、使いたい場合はマイグレーションファイルを作成します。

```
$ rails g migration AddStatesToUser active:boolean approved:boolean confirmed:boolean
```

```ruby
# Authlogic::Session::MagicStates
t.boolean   :active, default: false
t.boolean   :approved, default: false
t.boolean   :confirmed, default: false
```

これらフィールドのどれかがfalseだとAuthlogicでログインを拒否する動作になっています。なので自前で管理する必要はありません。

### メールアクチベーション

メールアドレスの存在確認をするために、本登録用メールを送信して記載されているURLにアクセスするとアカウントを有効化したい場合があります。それも割りとサクッと実装できます。前述した `MagicStates` の `active` フィールドを利用します。

またアクチベーションキーとして `perishable_token` というフィールドを利用するので、こちらも追加します。

```
$ rails g migration AddActiveToUser active:boolean perishable_token
```

```ruby
class AddActiveToUsers < ActiveRecord::Migration[5.0]
  def change
    add_column :users, :active, :boolean, null: false, default: false
    add_column :users, :perishable_token, :string
  end
end
```

#### app/controllers/user/mail_activate_controller.rb

`User.find_using_perishable_token` は渡された文字列を `user.perishable_token` と比較して該当ユーザが指定期間以内に作られたかどうか（updated_atフィールド）を判断しています。つまりトークンの有効期限みたいな感じですね。

```ruby
class User::MailActivateController < ApplicationController

  def create
    @user = User.find_using_perishable_token(params[:activate_code], 1.week)
    raise ActiveRecord::RecordNotFound unless @user

    if @user.activate!
      flash[:notice] = "Your account has been activated!"
      UserSession.create(@user, false) # Log user in manually
      redirect_to "/"
    end
  end

end
```

#### app/controllers/user/registrations_controller.rb

```diff
     if @user.save
 +     @user.deliver_mail_activate_instructions!
       redirect_to "/"
     else
```

#### app/models/user.rb

認証メール送信メソッドと、アクチベーションするメソッドを定義します。

```diff
class User < ApplicationRecord
  acts_as_authentic

  def deliver_mail_activate_instructions!
    NotifierMailer.mail_activate_instructions(self).deliver_now
  end

  def activate!
    self.active = true
    save
  end
end
```

#### app/mailers/notifier_mailer.rb

```ruby
class NotifierMailer < ActionMailer::Base
  default from: 'notifications@example.com'

  def mail_activate_instructions(user)
    @user = user
    mail(to: user.email, subject: "Mail Activate Instructions")
  end
end
```

#### app/views/notifier_mailer/mail_activate_instructions.html.erb

アクチベーションは `perishable_token` フィールドの値を見て行うので、トークンを付与したURLをメールで送付します。

```ruby
<%= user_mail_activate_url(@user.perishable_token) %>
```

#### config/routes.rb

```diff
  namespace :user do
 +    get "mail_activate/:activate_code", controller: "mail_activate", action: "create", as: "mail_activate"
  end
```

### パスワードリセット（リマインダー）

メールアクチベーションでも利用した `perishable_token` をパスワード再設定用にも利用します。仕様としては、パスワードリセット画面にメールアドレスを入力してもらって存在すれば、該当メールに対して `perishable_token` 付きのリセットURLを送付します。そのURLにアクセスすると、新しいパスワードが設定できるようにする感じにします。

#### app/controllers/user/password_resets_controller.rb

メールアドレスを入力する画面と、パスワードを再設定する画面が必要なのでそれぞれのアクションを作ります。

```ruby
class User::PasswordResetsController < ApplicationController
  before_filter :load_user_using_perishable_token, :only => [ :edit, :update ]

  def new
  end

  def create
    @user = User.find_by_email(params[:email])
    if @user
      @user.deliver_password_reset_instructions!
      flash[:notice] = "Instructions to reset your password have been emailed to you"
      redirect_to "/"
    else
      flash.now[:error] = "No user was found with email address #{params[:email]}"
      render :new
    end
  end

  def edit
  end

  def update
    @user.password = params[:password]
    @user.password_confirmation = params[:password]

    # 更新後、自動ログインさせたくないのであれば下記を使う
    # if @user.save_without_session_maintenance
    if @user.save
      flash[:success] = "Your password was successfully updated"
      redirect_to "/"
    else
      render :edit
    end
  end


  private

  def load_user_using_perishable_token
    @user = User.find_by_perishable_token(params[:id])
    unless @user
      flash[:error] = "We're sorry, but we could not locate your account"
      redirect_to "/"
    end
  end
end
```

#### app/mailers/notifier_mailer.rb

```diff
 +  def password_reset_instructions(user)
 +    @user = user
 +    mail(to: user.email, subject: "Password Reset Instructions")
 +  end
```

#### app/models/user.rb

`reset_perishable_token!` を呼んで強制的にperishable_tokenを更新します。これをしないとURLが流出してしまった時に好きなようにパスワードを変えられてしまいます。ちなみにAuthlogicはデフォルトでサインイン・アウト時にperishable_tokenを更新します。

```diff
 +  def deliver_password_reset_instructions!
 +    reset_perishable_token!
 +    NotifierMailer.password_reset_instructions(self).deliver_now
 +  end
```

#### app/views/notifier_mailer/password_reset_instructions.html.erb

```ruby
<%= edit_user_password_reset_url(@user.perishable_token) %>
```

#### app/views/user/password_resets/new.html.erb

```ruby
<%= form_tag user_password_resets_path do %>
  <%= text_field_tag :email %>
  <%= submit_tag "Reset Password" %>
<% end %>
```

#### app/views/user/password_resets/edit.html.erb

```ruby
<%= form_tag user_password_reset_path, :method => :put do %>
  <%= password_field_tag :password %>
  <%= submit_tag "Update Password" %>
<% end %>
```

#### config/routes.rb

```diff
  namespace :user do
 +    resources :password_resets, onlye: [:new, :create, :edit, :update]
  end
```

### WebAPIキー

ユーザ情報にアクセスするようなWebAPIを開発した時に認証が必要になりますが、意外と面倒です。簡易なAPIキーで良ければAuthlogicで提供されているSingle access tokenを利用できます。

まず `single_access_token` フィールドを利用するので作ります。

```
$ rails g migration AddSingleAccessTokenToUser single_access_token
```

```ruby
class AddSingleAccessTokenToUsers < ActiveRecord::Migration[5.0]
  def change
    add_column :users, :single_access_token, :string, null: false
  end
end
```

あとはUserSessionクラスに `single_access_allowed_request_types` オプションを渡せば完了です。

```ruby
class UserSession < Authlogic::Session::Base
  single_access_allowed_request_types :all
  # クエリストリングのキーを変更したい場合（デフォルトは user_credentials）
  # params_key :api_key
end
```

これで以下のようなURLでログイン済み扱いとして振る舞えます。

- http://example.com/secret?user_credentials=znEke01EDYwmeTQGXbtN

### テストコード

認証周りは不安要素が多いのでテストしておきたいですよね。今回はリクエストテストを書くことにしました。認証ロジック自体はAuthlogicでテストされているので、利用者側としては自分で実装したログイン画面をテストする方針にします。なおRail5からコントローラテストよりもリクエストテストを推奨しているので、コントローラテストは書きません。

今回はRSpecとFactoryGirlを使います。

単純にログイン画面にIDとパスワードをPOSTすればいいので下記のように書きます。

```ruby
def login admin
  post new_sign_in_url, :params => { user_session: { :email => user.email, :password => "password" } }
end

login create(:user)
```

そしてログインできるできないのテストを書いてみます。

```ruby
require 'rails_helper'

RSpec.describe "Console::Hoge", type: :request do
  describe "GET /console/hoge" do
    it "ログインしていないとアクセスできない" do
      get console_hoge_path
      expect(response).to have_http_status(302)
    end

    it "ログインするとアクセスできる" do
      login create(:user)
      get console_hoge_path
      expect(response).to have_http_status(200)
    end
  end
end
```

以上です。提供していることがシンプルなので理解もしやすく使いやすいと思います。