Ruby
Rails
Authlogic

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

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

認可に関してはPundit + Railsで認可の仕組みをシンプルに作るを参考にしてみてください。


前提


  • 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が自動でアップデートしてくれます。

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を適用します。

class User < ApplicationRecord

acts_as_authentic
end

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

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 を継承するクラスファイルです。

class UserSession < Authlogic::Session::Base

secure Rails.env.production?
httponly true
end

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


ユーザ登録ページ


app/controllers/users/registrations_controller.rb

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

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

<%= 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

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に定義します。

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

<%= 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

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モデルのオブジェクトが入っているので、それを返しているだけです。

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_tokenrecord_id:: で連結されていて、 record_id をもとにDBに保存されている persistence_token を探して一致すれば認証という流れになっている。


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


ユーザステータス

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

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

# Authlogic::Session::MagicStates

t.boolean :active, default: false
t.boolean :approved, default: false
t.boolean :confirmed, default: false

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


メールアクチベーション

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

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

$ rails g migration AddActiveToUser active:boolean perishable_token

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フィールド)を判断しています。つまりトークンの有効期限みたいな感じですね。

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

     if @user.save

+ @user.deliver_mail_activate_instructions!
redirect_to "/"
else


app/models/user.rb

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

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

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をメールで送付します。

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


config/routes.rb

  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

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

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

 +  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を更新します。

 +  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

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


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

<%= form_tag user_password_resets_path do %>

<%=
text_field_tag :email %>
<%= submit_tag "Reset Password" %>
<% end %>


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

<%= form_tag user_password_reset_path, :method => :put do %>

<%= password_field_tag :password %>

<%= submit_tag "Update Password" %>
<% end %>


config/routes.rb

  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

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 オプションを渡せば完了です。

class UserSession < Authlogic::Session::Base

single_access_allowed_request_types :all
# クエリストリングのキーを変更したい場合(デフォルトは user_credentials)
# params_key :api_key
end

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


テストコード

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

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

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

def login admin

post new_sign_in_url, :params => { user_session: { :email => user.email, :password => "password" } }
end

login create(:user)

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

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

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