Deviseに独自のstrategyを入れる
結論
deviseのデフォルトに沿わない場合は、deviseはいらない。
結局 ほぼwardenだけの話
config/initializers/devise.rb
require 'devise/strategies/media_authenticatable' ~~~ config.warden do |manager| manager.default_strategies(scope: :user).unshift :user_authenticatable manager.strategies.add(:user_authenticatable, Devise::Strategies::UserAuthenticatable) end
app/models/devise/strategies/user_authenticatable.rb
require 'devise/strategies/authenticatable' module Devise module Strategies class MediaAuthenticatable < Authenticatable def authenticate! # 認証ロジックをかく end def valid? # 認証をする条件を書く end end end end
これだけかな。
残念なところ
Devise.add_moduleの流儀に従って、モジュールを追加し、strategyだけを追加したくても、モデルが必須だったり(空実装でも定義だけ必要になる)、 DeviseControllerを継承したコントローラーを作ろうとして、routingをadd_moduleから設定しようとした場合、ルーティングを設定するメソッドの実装がActionDispatch::Routingを拡張して書く必要があったり。
まとめ
なんか、そこまでがんばり必要がなさすぎるな。と。 moduleは自分で拡張するものではないんだろうな。
wardenに直接追加するんだったら deviseとは?となる。 もやもやするライブラリだ。
Railsの認証Gem、Deviseとはなんなのか
目的
deivseについての理解を深めて、デフォルト動作ではない認証を作れるようにする。
背景
用意されすぎているライブラリは苦手。 挙動を変えたい場合に出来ること、出来ないことがわかりにくい。 理解したいってこと。
deviseとは
RailsEngineで作られている、Railsの認証ライブラリ。 サクっと認証を用意できる。ために、色々とデフォルト動作が決まっている。 手厚いサポート(機能)があるが、実際そんなにいらないことも多いはず。 ただ、有名すぎてこれを使っちゃうだろう。 wardenをみたあとでは、別にwardenでもいいよね。って気持ちはある。
だがデフォルトの動作を理解し自分の必要な部分だけを残し、ほかはいい感じにカスタマイズが出来るのであれが、deviseを使ったほうが楽だろうなというところ。 それが可能か検証する。
defaultで含まれているモジュール
- Database Authenticatable
- 一般的なやつ。passwordで認証をする。DBには暗号化されたパスワードを保存してる
- Omniauthable
- omniAuth gemとの連携
- confirmable
- 登録時に、確認メールを送る。ログイン時に確認済がどうかを判断する?
- Recoverable
- パスワードリセット
- Registerable
- 登録、編集、削除
- Rememberable
- ユーザーを覚えておくためのトークンを発行したりする。
- remember me のやつ
- Trackable
- サインインしたときのユーザーのIPとかとっておく
- Timeoutable
- 時間内にアクティブではないユーザーのセッションを期限切れにする
- Validatable
- mailアドレスとパスワードで認証するってことかな?
- Lockable
- 指定回数、ログインに失敗したらアカウントをロックする
揃っている。 これらは、(User)モデルで使用するかどうかを指定できるため、簡単につけ外しができる。 ※Tableにカラムがあれば。カラムを消したりしたりすると簡単ではないが(めんどい)。
devise module
Devise.add_moduleメソッドでモジュールを登録できる。 optionは以下。
※以下のリストは正直全然わかってない。
- model
- moduleで使用するモデルへのぱす
- controller
- moduleで使うコントローラー
- route
- moduleの使う route helper
- strategy
- strategyを持っているか。 true or false
- insert_at
- moduleが含まれる位置
model以外はすべて bool 値でもよく、module名と同じになる。
Deviseの既存のモジュールは lib/devise/modules.rb
で読み込まれている。
Devise.with_options model: true do |d| # Strategies first d.with_options strategy: true do |s| routes = [nil, :new, :destroy] s.add_module :database_authenticatable, controller: :sessions, route: { session: routes } s.add_module :rememberable, no_input: true end # Other authentications d.add_module :omniauthable, controller: :omniauth_callbacks, route: :omniauth_callback # Misc after routes = [nil, :new, :edit] d.add_module :recoverable, controller: :passwords, route: { password: routes } d.add_module :registerable, controller: :registrations, route: { registration: (routes << :cancel) } d.add_module :validatable # The ones which can sign out after routes = [nil, :new] d.add_module :confirmable, controller: :confirmations, route: { confirmation: routes } d.add_module :lockable, controller: :unlocks, route: { unlock: routes } d.add_module :timeoutable # Stats for last, so we make sure the user is really signed in d.add_module :trackable end
これらは、(User)modelで devise メソッドで定義することで使用できるようになる。
devise :database_authenticatable, :trackable, :omniauthable, omniauth_providers: [:google_oauth2]
このdeviseメソッドはどこからきているのか。
lib/devise/orm/active_record.rb
で、active_recordが読み込まれたタイミングのcallbackで、Devise::Models
が extends されている。
require 'orm_adapter/adapters/active_record' ActiveSupport.on_load(:active_record) do extend Devise::Models end
deviseメソッドはなにをしているか。
include Devise::Models::Authenticatable
Models::Authenticatableクラスのinclude
deviseメソッドで指定されたモジュール群の、 include
ClassMethods
が定義されていた場合、 extends
もする。
たとえば、 database_authenticatable
の場合だとこんな感じで定義されていて、
Devise::Models.config
を呼んで、find_for_database_authentication
をモデルに定義する。
module ClassMethods Devise::Models.config(self, :pepper, :stretches, :send_email_changed_notification, :send_password_change_notification) # We assume this method already gets the sanitized values from the # DatabaseAuthenticatable strategy. If you are using this method on # your own, be sure to sanitize the conditions hash to only include # the proper fields. def find_for_database_authentication(conditions) find_for_authentication(conditions) end end
このモデルに定義したモジュールたちがどう使われるのか。を見ていきたい。
lib/devise/rails/routes.rb
の devise_for
メソッドから見ていく。
このメソッドは config/routes.rb
で以下のような形で呼ぶことになる。
devise_for :users, controllers: { sessions: 'users/sessions' }
devise_forの責任としては、指定したリソース(モデル)を使用した認証関連のルーティングの設定をすること。
devise_for :users
とだけ定義した場合は、 User
モデルを見て、ルーティングを設定する。
ルーティングの他は Devise::Mapping
の生成をする。
mappingはscopeごとに生成され、以下のようなものになる。
mapping = Devise.mappings[:user] mapping.name #=> :user mapping.as #=> "users" mapping.to #=> User mapping.modules #=> [:authenticatable]
認証ロジックを自作したい。
ここまで見てきたわかったことは、 deviseに処理を追加したい場合は、モジュールを作る。 モジュールには、スコープのモデルに追加したいメソッド、 スコープを使用してのルーティングの追加、 ルーティングに使用するコントローラーの追加、 スコープを認証する際のstrategyの定義ができる。
単純に考えると、モジュールを作るかー。となりそうなものだが、
database_authenticatable
を継承したりしていい感じに出来ないの?って思ったりする。
そこらへんを見ていきたい。
モデルに対して、 database_authenticatable
を deviseメソッドで指定すると、以下のルーティングが作成される。
new_user_session GET /sign_in(.:format) devise/sessions#new user_session POST /sign_in(.:format) devise/sessions#create destroy_user_session DELETE /sign_out(.:format) devise/sessions#destroy
Deviseに用意されているControllerは DeviseController
を継承している。
DeviseController
は ApplicationController
を継承している。
DeviseControllerは warden
へアクセスするヘルパーメソッドが用意されている。
自作するために。
strategyモジュールを作って、モデルに読み込ませる。
認証をするControllerでDeviseControllerを継承し、 warden.authenticate
, sign_in
等のヘルパーメソッドを呼ぶ。
こんな感じ。
というかこれだけの事をするのに devise
が必要か?というといらない。
devise とはなんだったのか
wardenのヘルパー。
deviseの決めた挙動での認証システムの高速な実装。
strategy, routing等まで含めた認証実装の簡易化。
たとえば、社内システムでSSOを実装する。となった場合に、1度実装すれば簡単に使いまわすことが可能。 という感じだろうか。
まとめ
読んでいないソースが大半なので、確定ではないが、 deviseの標準を大きく外れるようであれば、無理にdeviseを使う必要もないとう判断になった。 wardenで十分ぽい。
RackとWardenについて
Rackについてはこちらをみた。
じぶんの解釈
Rackとはwebサーバーを作る際のお作法(インターフェース)。
インターフェースに従ってWebサーバーを作成することで、サーバーを交換可能。 ミドルウェアを共通化できる。 という感じだろうか
Warden
General Rack Authentication Framework
Rackの上で構築される認証のフレームワーク。 有名なdeviseはwardenのラッパーになる。
Rackミドルウェアとして作られている。
session情報は env['rack.session']
Wardenは env[warden]
にオブジェクトを入れる。
このオブジェクトを用いて、認証を行うことができる。
env['warden'].authenticated? # Ask the question if a request has been previously authenticated env['warden'].authenticated?(:foo) # Ask the question if a request is authenticated for the :foo scope env['warden'].authenticate(:password) # Try to authenticate via the :password strategy. If it fails proceed anyway. env['warden'].authenticate!(:password) # Ensure authentication via the password strategy. If it fails, bail.
認証が成功されると、 user
オブジェクトへアクセスが出来る。
これは nil
以外なら何でもよい。
env['warden'].authenticate(:password)
この記述ではパスワードで認証を行っている。
この、どのように認証を行うか。を strategy
と呼んでいて、これを拡張して独自で認証機構を実装することができる。
Rackアプリからwadenを利用していく
ためしにやっていく
Rackアプリ
config.ru
というファイル名で以下を作成
require 'rack' app = Proc.new do |env| ['200', {'Content-Type' => 'text/html'}, ['A barebones rack app.']] end run app
rackup
コマンドで config.ru
を読み込んでサーバーを起動する。
vagrant-ubuntu-trusty-64% rackup Puma starting in single mode... * Version 3.8.2 (ruby 2.3.0-p0), codename: Sassy Salamander * Min threads: 0, max threads: 16 * Environment: development * Listening on tcp://localhost:9292 Use Ctrl-C to stop
vagrant-ubuntu-trusty-64% curl http://localhost:9292/ A barebones rack app.%
curlでリクエストを投げると、レスポンスが確認できる。
wardenの追加
require 'rack' require 'warden' app = Proc.new do |env| ['200', {'Content-Type' => 'text/html'}, ['A barebones rack app.']] end failure_app = Proc.new do |env| ['401', {'Content-Type' => 'text/html'}, ['fail.']] end use Rack::Session::Cookie, :secret => "replace this with some secret key" use Warden::Manager do |manager| manager.default_strategies :password, :basic manager.failure_app = failure_app end run app
wardenを使う際には、デフォルトのstrategyと認証が失敗した場合に呼ばれる、rack endpoint を指定する。
sessionシリアライズロジックと認証するユーザー
config.ru の run.app の前あたりに以下を入れる
Warden::Manager.serialize_into_session do |user| user.id end Warden::Manager.serialize_from_session do |id| User.get(id) end
Userクラスの定義もその下らへんに書く。
class User attr_accessor :id, :password USER_MAPPING = { a: 'hogehoge', b: 'mogemoge', } def initialize(id, pass) self.id = id self.passworkd = pass end def self.get(id) new(id, USER_MAPPING[id.to_sym]) end end
これで認証のもとなるUserの準備ができたので、認証ロジックを作りたいとおもう。
認証をしてみる
strategyは複数設定が可能。 その中の1つでも成功するか、すべての戦略を通るか、戦略がfailするまで呼ばれる。
strategy
は Warden::Strategies::Base
を継承して作られる。
実装が必要なのは、 valid?
と authenticate!
の2つ。
valid?
valid?
メソッドは strategy
の実行条件(ガード)。
宣言シない場合、strategy
は常に実行される。
上記の例では、 username, passwordのどちらか1つでもあれば戦略を実行します。
authenticate!
認証ロジック。 利用可能なメソッドがいくつかある。
- request
- session
- params
env
halt!
- strategyの実行を止める。後続のstrategyは実行されない
- pass
- strategyを実行しない
- success!
- 認証に成功。userオブジェクトをsessionに格納し、ログインする。halt!する
- fail!
- 認証失敗。halt!
- redirect!
- 別のURLへリダイレクトする。halt!
custom!
- わからん
headers
- headerを設定する
- errors
- errorオブジェクトへのアクセス
では、実際に strategy
を書いてみる。
こんな感じのコードをまた run.app の前あたりにいれる。
Warden::Strategies.add(:password) do def valid? params['id'] || params['password'] end def authenticate! if User.authenticate(params['id'], params['password']) success! User.get(params['id']) else fail!('failll') end end end
User.authenticateを実装する
def self.authenticate(id, pass) return false unless password = USER_MAPPING[id.to_sym] !!(password == pass) end
Rack endpointを修正する
app = Proc.new do |env| env['warden'].authenticate! ['200', {'Content-Type' => 'text/html'}, ['A barebones rack app.']] end
やってみる
vagrant-ubuntu-trusty-64% curl "http://localhost:9292/?id=a&password=hogehoge" A barebones rack app.% vagrant-ubuntu-trusty-64% curl "http://localhost:9292/?id=b&password=hogehoge" fail.%
id=a, password=hogehogeは認証が成功し、id=b, password=hogehogeはfailしている。
scope
複数のユーザー(タイプ?)をログイン可能にする。
defautユーザーとAdminユーザーで認証ロジックを変えたい場合など、 スコープ(defalt, admin)でそれぞれ定義が可能。
callbacks
- after_set_user
- after_authentication
- after_fetch
- before_failure
- after_failed_fetch
- before_logout
- on_request
これらのタイミングにcallbackを仕込める。
Warden::Manager.after_set_user do |user, auth, opts| unless user.active? auth.logout throw(:warden, :message => "User not active") end end
set_userは認証に成功し、env[‘warden’].user にユーザーオブジェクトを入れる時のこと。 そのあとに上記のcallbackが呼ばれる。
まとめ
これだけ見ると非常にシンプル。 deviseを理解するために、wardenを見た。 次はdeviseを理解していくぞ。
CircleCi2.0が最高かもしれん
ローカルの環境をdockerで整えたので、次はCI環境を整備する。 dockerで構築したのだから、テストもdockerにしたい。 ちょうどCircleCi2.0がBetaテスト中で、さらにネイティブでdockerサポートが入っている。 これは試すしかないということでやってみた。
CricleCi2.0を使う
プロジェクトのホームディレクトリに .circleci
ディレクトリを作成。 config.yml
を作成する
config.yml
の中身
version: 2 jobs: build: working_directory: ~/app docker: - image: hogehoge/mogemoge:latest steps: - checkout - run: echo "hello world"
これでCircleCiを通すだけの準備は完了する。
imageはDockerHubのpublicディレクトリであれば普通に取ってこれる。privateリポは別途設定が必要。
config.yml
の設定は、try & error で直して、pushして、CircleCI流して〜ってやるとかなり消耗する。
複雑なテストをやろうとすると死ねる。
CircleCi2.0ではローカルでほぼ同様のことが可能になっている。 (docker executorのみ使える。従来のCircleCiの主流であったmachine executorは使えない)
circleci コマンド
install
vagrant-ubuntu-trusty-64% sudo curl -o /usr/local/bin/circleci https://circle-downloads.s3.amazonaws.com/releases/build_agent_wrapper/circleci && sudo chmod +x /usr/local/bin/circleci
usage はこんな感じ
vagrant-ubuntu-trusty-64% circleci Receiving latest version of circleci... The CLI tool to be used in CircleCI. Usage: circleci [flags] circleci [command] Available Commands: build run a full build locally config validate and update configuration files tests collect and split files with tests version output version info Flags: -c, --config string config file (default is .circleci/config.yml) --taskId string TaskID --verbose emit verbose logging output
config fileのvalidate
vagrant-ubuntu-trusty-64% circleci config validate config file is valid
buildのhelp
vagrant-ubuntu-trusty-64% circleci build --help run a full build locally Usage: circleci build [flags] Flags: --branch string Git branch --checkout-key string Git Checkout key (default "~/.ssh/id_rsa") --config string config file (default ".circleci/config.yml") --index int node index of parallelism --job string job to be executed (default "build") --parallelism int parallelism level (default 1) --repo-url string Git Url --revision string Git Revision --skip-checkout use local path as-is (default true) -v, --volume value Volume bind-mounting (default []) Global Flags: --taskId string TaskID --verbose emit verbose logging output
circleci build
をふつうに叩いたらふつうに落ちた。
どうやら working_directroyが相対パスだとダメらしい。最初のymlで相対パスで設定させといてそれ?w
working_directory: /app
に変更して通った。
最終的なかたち
version: 2 jobs: build: working_directory: /app docker: - image: docker image name environment: RACK_ENV: 'test' - image: mysql:5.7.17 environment: MYSQL_ALLOW_EMPTY_PASSWORD: 'yes' MYSQL_ROOT_HOST: '%' steps: - checkout - run: bundle install - run: bundle exec rails db:create - run: bundle exec rails db:migrate - run: bundle exec rspec - deploy: name: run deploy command: | if [ "${CIRCLE_BRANCH}" == "master" ]; then apk add curl curl --user ${CIRCLE_API_TOKEN}: \ --data build_parameters[CIRCLE_JOB]=deploy \ --data revision=$CIRCLE_SHA1 \ https://circleci.com/api/v1.1/project/github/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME/tree/$CIRCLE_BRANCH \ >> /dev/null fi deploy: working_directory: /app docker: - image: docker image name environment: RACK_ENV: 'production' steps: - checkout - run: bundle install - setup_remote_docker - deploy: name: deploy command: echo 'hello'
docker-composeのように、imageがリンクされるわけではないので 127.0.0.01
でアクセスが出来るようにする必要があった。MYSQL_ROOT_HOST: '%'
config/database.yml
にはこんな感じにかく host: <%= ENV['DB_HOST'] || ENV['MYSQL_ROOT_HOST'] || '127.0.0.1' %>
以下のサポートを参照した。 https://discuss.circleci.com/t/rails-mysql-container-connection/10604
deploy
こちらを参考にさせていただきました。 http://h3poteto.hatenablog.com/entry/2017/03/31/231410
別ジョブで実行するという方式。 残念ながら別ジョブはCLIコマンドではテストが出来ないので、 pushして try & error になってしまった。 ハックだし、いずれより良い形で公式にサポートがされるだろう。 現時点では docker executorでdeployするimageをbuildするいい方法がないかもしれない。
deployタスクは1.0のときのように、デフォルトで branchで別けれるようにしてほしかった。
【追記】 公式ドキュメントに増えた?昨日はなかった気がするのにw
This approach is a temporary workaround for the current features available during Beta. Soon we’ll be adding a much more elegant way to manage multiple jobs.
より良い方法をそのうち公開するっていってるのでよかったね
まとめ
deploy抜きで考えるならば、テスト、構築ともに速くなって非常にいいと思った。 そもそもここでdeploy用のimageをbuildするという方式も違うのかもしれない。 testが通ったwebhookで別のところでやってもいいかもしれない。 それも考えてみよう。