Rails
devise
warden
reCAPTCHA

devise に CAPTCHA 認証を追加する方法

More than 3 years have passed since last update.


2014/12/11 追記

新しくなった reCAPTCHA が良さそうだったので、それに対応した。


やりたいことは、Twitter や Facebook みたいに数回ログインに失敗したら、そのユーザーでログインするのに CAPTCHA 認証を通さないといけない機能。

devise とかいうクソ gem 1 を使ってしまったせいで苦労したので、書き留めておく。

なお、使う CAPTCHA は reCAPTCHA


devise の Lockable の設定


config/initializers/devise.rb

Devise.setup do |config|

# ...

# ==> Configuration for :lockable
config.lock_strategy = :failed_attempts
config.unlock_strategy = :none
config.maximum_attempts = 2 # お好きな値に

#...
end



CAPTCHA の表示

アカウントロックされているかどうかを知るだけでも工夫がいる。

ログインの検証を行ってるのは、Devise::SessionsController#createwarden.authenticate! だが、warden.authenticate に変えたところで、アカウントロックされているユーザーのモデルは取得できない。

最初は、warden の Strategy を拡張する方法や、custom failure を使う方法とか小難しいこと考えたけど、Devise::Strategies#validatefail! に渡している認証失敗メッセージを確認することにした。


app/controllers/devise/sessions_controller.rb

class SessionsController < Devise::SessionsController

helper_method :access_locked?

# アカウントロックでエラーになったか調べる
def access_locked?
# Devise::Models::Lockable#unauthenticated_message の戻り値が入ってる
warden.message == :locked
end
end


あとは、View に表示させる。


app/views/devise/sessions/new.html.haml

-# CAPTCHA を表示させたいところに追加

- if access_locked?
render "devise/sessions/recapture"


app/views/devise/sessions/_recapture.html.haml

%script{:type => "text/javascript", :src => "//www.google.com/recaptcha/api.js", :defer => true, :async => true}

.g-recaptcha{:"data-sitekey" => your_site_key}


まあ自分で書かなくても、ruby-recaptcha なんかの gem を使ってもいいと思います。


CAPTCHA 認証とロック解除

warden の Strategy を拡張する。


lib/devise/strategy/captcha_authenticatable.rb

require 'cgi'

require 'net/http'
require 'uri'
require 'devise/strategies/authenticatable'

module Devise
module Strategies
# Devise 標準の DatabaseAuthenticatable に、reCAPTCHA 認証の機能を追加。
class CaptchaAuthenticatable < Authenticatable
def authenticate!
resource = valid_password? && mapping.to.find_for_database_authentication(authentication_hash)
encrypted = false

if validate(resource){ encrypted = true; validate_resource(resource) }
resource.after_database_authentication
success!(resource)
end

mapping.to.new.password = password if !encrypted && Devise.paranoid
fail(:not_found_in_database) unless resource
end

# ロックされたユーザーの CAPTCHA 認証と、パスワードの検証を行う。
def validate_resource(resource)
if resource.access_locked?
return false unless validate_recaptcha
resource.unlock_access! # CAPTCHA 認証が通ったらロック解除

# パスワードまで合ってないとロック解除しないとか、
# いろんな要件があると思うが、このメソッド内に記述すればよい
end
resource.valid_password?(password)
end

protected

# CAPTCHA の検証を行います。
def validate_recaptcha
# CAPTCHA のパラメータがある場合のみ検証する
return false unless params.has_key?("g-recaptcha-response")

res = Net::HTTP.post_form(URI.parse("https://www.google.com/recaptcha/api/siteverify"),
{ :secret => your_secret_key,
:remoteip => CGI.escape(request.remote_ip),
:response => CGI.escape(params["g-recaptcha-response"]) })
results = JSON.parse(res.body)
Rails.logger.info("reCAPTCHA: #{results}")
results["success"]
end
end
end
end

Warden::Strategies.add(:captcha_authenticatable, Devise::Strategies::CaptchaAuthenticatable)


前述した ruby-recaptcha なんかを使ってるなら、validate_recaptcha の代わりに、その gem で用意されたメソッドを使えばよい。

拡張した Strategy を使うように devise を設定する。


config/initializers/devise.rb

require 'devise/strategies/captcha_authenticatable'

Devise.setup do |config|
# ...

# ==> Warden configuration
config.warden do |manager|
manager.default_strategies(:scope => :user).unshift :captcha_authenticatable
end

#...
end



2013/12/26 追記

lib/devise/hooks/lockable.rb でリセット処理を行っているので、下記処理は不要だった。


ログイン成功した時に、ログイン失敗のカウントをリセットする。


app/model/user.rb

class User < ActiveRecord::Base

# override Devise::Models::DatabaseAuthenticatable#after_database_authentication
def after_database_authentication
# lib/devise/hooks/lockable.rb の処理と重複していたのでコメントアウト
#unlock_access! if failed_attempts > 0
end
end


以上。


1. 個人の主観が入っています。