nyauthという認証gemを作ってた

MoneyForward Advent Calendar 2015 - Qiitaの14日目」の記事です。

今年、一体何やっていたかなと振り返ってみたところ、nyauthというgemを作っていたことに気づいたので、雑に紹介します。

「お前エモい記事しか書かなくなったな」みたいな声もチラホラ聞くのでたまにはプログラミングの記事を。

f:id:naoto5959:20151213124149p:plain

こうして振り返ってみると、何故かコントリビューターも現れ始めて嬉しかったです。

何なの

一般ユーザー向けと管理者用向けといった複数のコンテクストに対応した認証gemです。

猫の「にゃん」(って何)と「Authentication」を合わせて「にゃおーす」つまりnyauthです。

なんで作ったの

個人プロジェクトでとあるWebアプリケーションを作る際に、せっかくだし deviseから卒業しようってのと、単純になるべくメタプロし過ぎないシンプルなものを作りたかったのです。

ただそれだけです。学びが多かった。それが収穫です。

どんなもの

ざっと

  • 登録
  • 認証
  • パスワード変更
  • 本人確認(メールが届くかどうか)
  • パスワード変更

といった機能があります。一般ユーザーにはこれら全てを提供して、管理者ユーザーには、「認証だけ」みたいなユースケースに対応してます。

一般ユーザーの認証URLは

/session/new

管理者ユーザーの認証URLは

/admin/session/new

と言った感じで、現在のコンテクストをURLのパスで判断してます。

ルーティング

config/routes.rb

Rails.application.routes.draw do
  mount Nyauth::Engine => "/"
end

/にマウントすると以下の様なroutesが定義されます。/にマウントした時にはデフォルトでUserモデルが認証の対象となります。

Prefix Verb URI Pattern Controller#Action
nyauth      /nyauth     Nyauth::Engine

Routes for Nyauth::Engine:
              registration POST   /registration(.:format)                             nyauth/registrations#create
          new_registration GET    /registration/new(.:format)                         nyauth/registrations#new
                   session POST   /session(.:format)                                  nyauth/sessions#create
               new_session GET    /session/new(.:format)                              nyauth/sessions#new
                           DELETE /session(.:format)                                  nyauth/sessions#destroy
             edit_password GET    /password/edit(.:format)                            nyauth/passwords#edit
                  password PATCH  /password(.:format)                                 nyauth/passwords#update
                           PUT    /password(.:format)                                 nyauth/passwords#update
     confirmation_requests POST   /confirmation_requests(.:format)                    nyauth/confirmation_requests#create
  new_confirmation_request GET    /confirmation_requests/new(.:format)                nyauth/confirmation_requests#new
              confirmation GET    /confirmations/:confirmation_key(.:format)          nyauth/confirmations#update
   reset_password_requests POST   /reset_password_requests(.:format)                  nyauth/reset_password_requests#create
new_reset_password_request GET    /reset_password_requests/new(.:format)              nyauth/reset_password_requests#new
       edit_reset_password GET    /reset_passwords/:reset_password_key/edit(.:format) nyauth/reset_passwords#edit
            reset_password PATCH  /reset_passwords/:reset_password_key(.:format)      nyauth/reset_passwords#update
                           PUT    /reset_passwords/:reset_password_key(.:format)      nyauth/reset_passwords#update

config/routes.rb

Rails.application.routes.draw do
  mount Nyauth::Engine => "/hoge"
end

/hogeにマウントすれば、Hogeモデルが認証の対象となります。

複数のモデルを認証したいときのルーティング

config/routes.rb

Rails.application.routes.draw do
  # for admin
  namespace :nyauth, path: :admin, as: :admin do
    # concerns :nyauth_registrable
    concerns :nyauth_authenticatable
    # concerns :nyauth_confirmable
  end

  # for user
  mount Nyauth::Engine => "/"
end

本当は、2箇所に違う名前でマウントしたいのだけども

Rails.application.routes.draw do
  # for admin
  mount Nyauth::Engine => "/admin", as: 'admin'

  # for user
  mount Nyauth::Engine => "/", as: 'user'
end

とは出来ません。Railsが2箇所にEngineをマウントはできてもURLヘルパーを上手く扱えず後で定義した方のasで上書いてしまうので、今回はmountは1つ、2つ目以降は

namespace :nyauth, path: :admin, as: :admin do
    # concerns :nyauth_registrable
    concerns :nyauth_authenticatable
    # concerns :nyauth_confirmable
end

と言った感じで定義します。もちろん、1つ目からこの形式でも大丈夫です。

ここで、nyauthでは以下のRouting Conrcenを使えます。

concern :nyauth_registrable do
  resource :registration, only: %i(new create)
end

concern :nyauth_authenticatable do
  resource :session, only: %i(new create destroy)
  resource :password, only: %i(edit update)
  resources :reset_password_requests, only: %i(new create)
  resources :reset_passwords, param: :reset_password_key, only: %i(edit update)
end

concern :nyauth_confirmable do
  resources :confirmation_requests, only: %i(new create)
  get '/confirmations/:confirmation_key' => 'confirmations#update', as: :confirmation
end

コントローラー

application_controller.rb

class ApplicationController < ActionController::Base
  include Nyauth::ControllerConcern
  before_action -> { require_authentication! as: :user }
  helper_method :current_user

  private

  def current_user
    current_authenticated(as: :user)
  end
end

使いたいコントローラーに、include Nyauth::ControllerConcernと書いておくと、require_authentication!というメソッドが生えるので、

before_action -> { require_authentication! as: :user }

てな感じで、フィルターをかけるイメージです。

管理者向けのコントローラーにも同じように、

admin/base_controller.rb

class Admin::BaseController < ActionController::Base
  include Nyauth::ControllerConcern
  before_action -> { require_authentication! as: :admin }
  helper_method :current_admin

  private

  def current_admin
    current_authenticated(as: :admin)
  end
end

テーブル定義

class CreateUsers < ActiveRecord::Migration
  def change
    create_table :users do |t|
      t.string :nickname
      # Authenticatable
      t.string :email, null: false
      t.string :password_digest, null: false
      t.string :password_salt, null: false
      t.string :reset_password_key
      t.datetime :reset_password_key_expired_at
      # Confirmable
      t.datetime :confirmed_at
      t.string :confirmation_key
      t.datetime :confirmation_key_expired_at

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

モデル

適切なカラムを定義した上で、モデルにmoduleincludeしておきます。

app/models/user.rb

class User < ActiveRecord::Base
  include Nyauth::Authenticatable
  include Nyauth::Confirmable
end

app/models/admin.rb

class Admin < ActiveRecord::Base
  include Nyauth::Authenticatable
end

なんとなく見れば分かるかな。更に詳しくは、READMEやソースコードを読んで頂けると良いかと嬉しいです。

学び

  • 同じEngineを複数のパスにマウントする事を想定していないようでつらかった。もうちょいソースを読み込んでコントリビュートしたい気持ちはある。
  • そのため、helperの生成がつらかった。実装的にもシンプルじゃないのでいけてない。
  • テスト時に、sign_in(user)みたいなヘルパーを使うために、wardenを大いに参考にさせていただいた。Rack層の段階で次のrequestにhookするという発想がとても勉強になった。
  • generator生成のノウハウを得た。
  • respondersの拡張ノウハウを得た。
  • gemのconfigを用意してゴニョゴニョカスタマイズするノウハウを得た。

個人プロジェクトの進捗どうですか

なんと肝心の個人プロジェクトのWebアプリは、このgemを作ることに寄り道したり、React.jsをこねくり回したり、RailsとJavaScriptのモダンな共存などを模索していたせいで公開がまで来てません。個人プロジェクトは、yak shavingばかりしていた1年でしたね。

github.com

今回の記事でnyauthにご興味持たれた方は是非Pull Requestでフィードバック頂けると嬉しいです。