Dmitrii Golub
13 years ago
commit
e4c812ad6d
19 changed files with 320 additions and 0 deletions
@ -0,0 +1,20 @@ |
|||||||
|
*.gem |
||||||
|
.bundle |
||||||
|
Gemfile.lock |
||||||
|
pkg/* |
||||||
|
|
||||||
|
# Temporary files of every sort |
||||||
|
.DS_Store |
||||||
|
.idea |
||||||
|
.rvmrc |
||||||
|
.stgit* |
||||||
|
*.swap |
||||||
|
*.swo |
||||||
|
*.swp |
||||||
|
*~ |
||||||
|
bin/* |
||||||
|
nbproject |
||||||
|
patches-* |
||||||
|
capybara-*.html |
||||||
|
dump.rdb |
||||||
|
*.ids |
@ -0,0 +1,4 @@ |
|||||||
|
source "http://rubygems.org" |
||||||
|
|
||||||
|
# Specify your gem's dependencies in devise_ip_filter.gemspec |
||||||
|
gemspec |
@ -0,0 +1,19 @@ |
|||||||
|
Copyright (C) 2012 Dmitrii Golub |
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of |
||||||
|
this software and associated documentation files (the "Software"), to deal in |
||||||
|
the Software without restriction, including without limitation the rights to |
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies |
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do |
||||||
|
so, subject to the following conditions: |
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all |
||||||
|
copies or substantial portions of the Software. |
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||||
|
SOFTWARE. |
@ -0,0 +1,48 @@ |
|||||||
|
## Two factor authentication for Devise |
||||||
|
|
||||||
|
## Features |
||||||
|
|
||||||
|
* control sms code pattern |
||||||
|
* configure max login attempts |
||||||
|
* per user level control if he really need two factor authentication |
||||||
|
* your own sms logic |
||||||
|
|
||||||
|
## Configuration |
||||||
|
|
||||||
|
To enable two factor authentication for User model, you should add two_factor_authentication to your devise line, like: |
||||||
|
|
||||||
|
```ruby |
||||||
|
devise :database_authenticatable, :registerable, |
||||||
|
:recoverable, :rememberable, :trackable, :validatable, :two_factor_authenticatable |
||||||
|
``` |
||||||
|
|
||||||
|
Two default parameters |
||||||
|
|
||||||
|
```ruby |
||||||
|
config.login_code_random_pattern = /\w+/ |
||||||
|
config.max_login_attempts = 3 |
||||||
|
``` |
||||||
|
|
||||||
|
Possible random patterns |
||||||
|
```ruby |
||||||
|
/\d{5}/ |
||||||
|
/\w{4,8}/ |
||||||
|
``` |
||||||
|
|
||||||
|
see more https://github.com/benburkert/randexp |
||||||
|
|
||||||
|
By default second factor authentication enabled for each user, you can change it with this method in your User mdoel: |
||||||
|
```ruby |
||||||
|
def need_two_factor_authentication?(request) |
||||||
|
request.ip != '127.0.0.1' |
||||||
|
end |
||||||
|
``` |
||||||
|
this will disable two factor authentication for local users |
||||||
|
|
||||||
|
Your send sms logic should be in this method in your User model: |
||||||
|
```ruby |
||||||
|
def send_two_factor_authentication_code(code) |
||||||
|
puts code |
||||||
|
end |
||||||
|
``` |
||||||
|
This example just puts code in logs |
@ -0,0 +1,43 @@ |
|||||||
|
class Devise::TwoFactorAuthenticationController < DeviseController |
||||||
|
prepend_before_filter :authenticate_scope! |
||||||
|
before_filter :prepare_and_validate, :handle_two_factor_authentication |
||||||
|
|
||||||
|
def show |
||||||
|
end |
||||||
|
|
||||||
|
def update |
||||||
|
render :show and return if params[:code].nil? |
||||||
|
md5 = Digest::MD5.hexdigest(params[:code]) |
||||||
|
if md5.eql?(resource.second_factor_pass_code) |
||||||
|
warden.session(resource_name)[:need_two_factor_authentication] = false |
||||||
|
sign_in resource_name, resource, :bypass => true |
||||||
|
redirect_to stored_location_for(resource_name) || :root |
||||||
|
resource.update_attribute(:second_factor_attempts_count, 0) |
||||||
|
else |
||||||
|
resource.second_factor_attempts_count += 1 |
||||||
|
resource.save |
||||||
|
set_flash_message :notice, :attempt_failed |
||||||
|
if resource.max_login_attempts? |
||||||
|
sign_out(resource) |
||||||
|
render :template => 'devise/two_factor_authentication/max_login_attempts_reached' and return |
||||||
|
else |
||||||
|
render :show |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
private |
||||||
|
|
||||||
|
def authenticate_scope! |
||||||
|
self.resource = send("current_#{resource_name}") |
||||||
|
end |
||||||
|
|
||||||
|
def prepare_and_validate |
||||||
|
redirect_to :root and return if resource.nil? |
||||||
|
@limit = resource.class.max_login_attempts |
||||||
|
if resource.max_login_attempts? |
||||||
|
sign_out(resource) |
||||||
|
render :template => 'devise/two_factor_authentication/max_login_attempts_reached' and return |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,3 @@ |
|||||||
|
<h2>Access completly denied as you have reached your attempts limit = <%= @limit %></h2> |
||||||
|
<p>Please contact your system administrator</p> |
||||||
|
|
@ -0,0 +1,10 @@ |
|||||||
|
<h2>Enter your personal code</h2> |
||||||
|
|
||||||
|
<p><%= flash[:notice] %></p> |
||||||
|
|
||||||
|
<%= form_tag([resource_name, :two_factor_authentication], :method => :put) do %> |
||||||
|
<%= text_field_tag :code %> |
||||||
|
<%= submit_tag "Submit" %> |
||||||
|
<% end %> |
||||||
|
|
||||||
|
<%= link_to "Sign out", destroy_user_session_path, :method => :delete %> |
@ -0,0 +1,4 @@ |
|||||||
|
en: |
||||||
|
devise: |
||||||
|
two_factor_authentication: |
||||||
|
attempt_failed: "Attemp failed" |
@ -0,0 +1,27 @@ |
|||||||
|
require 'two_factor_authentication/version' |
||||||
|
require 'randexp' |
||||||
|
require 'devise' |
||||||
|
require 'digest' |
||||||
|
require 'active_support/concern' |
||||||
|
|
||||||
|
module Devise |
||||||
|
mattr_accessor :login_code_random_pattern |
||||||
|
@@login_code_random_pattern = /\w+/ |
||||||
|
|
||||||
|
mattr_accessor :max_login_attempts |
||||||
|
@@max_login_attempts = 3 |
||||||
|
end |
||||||
|
|
||||||
|
module TwoFactorAuthentication |
||||||
|
autoload :Schema, 'two_factor_authentication/schema' |
||||||
|
module Controllers |
||||||
|
autoload :Helpers, 'two_factor_authentication/controllers/helpers' |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
Devise.add_module :two_factor_authenticatable, :model => 'two_factor_authentication/models/two_factor_authenticatable', :controller => :two_factor_authentication, :route => :two_factor_authentication |
||||||
|
|
||||||
|
require 'two_factor_authentication/orm/active_record' |
||||||
|
require 'two_factor_authentication/routes' |
||||||
|
require 'two_factor_authentication/models/two_factor_authenticatable' |
||||||
|
require 'two_factor_authentication/rails' |
@ -0,0 +1,34 @@ |
|||||||
|
module TwoFactorAuthentication |
||||||
|
module Controllers |
||||||
|
module Helpers |
||||||
|
extend ActiveSupport::Concern |
||||||
|
|
||||||
|
included do |
||||||
|
before_filter :handle_two_factor_authentication |
||||||
|
end |
||||||
|
|
||||||
|
module InstanceMethods |
||||||
|
private |
||||||
|
|
||||||
|
def handle_two_factor_authentication |
||||||
|
if not request.format.nil? and request.format.html? and not devise_controller? |
||||||
|
Devise.mappings.keys.flatten.any? do |scope| |
||||||
|
if signed_in?(scope) and warden.session(scope)[:need_two_factor_authentication] |
||||||
|
session["#{scope}_return_tor"] = request.path if request.get? |
||||||
|
redirect_to two_factor_authentication_path_for(scope) |
||||||
|
return |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def two_factor_authentication_path_for(resource_or_scope = nil) |
||||||
|
scope = Devise::Mapping.find_scope!(resource_or_scope) |
||||||
|
change_path = "#{scope}_two_factor_authentication_path" |
||||||
|
send(change_path) |
||||||
|
end |
||||||
|
|
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,10 @@ |
|||||||
|
Warden::Manager.after_authentication do |user, auth, options| |
||||||
|
if user.respond_to?(:need_two_factor_authentication?) |
||||||
|
if auth.session(options[:scope])[:need_two_factor_authentication] = user.need_two_factor_authentication?(auth.request) |
||||||
|
code = user.generate_two_factor_code |
||||||
|
user.second_factor_pass_code = Digest::MD5.hexdigest(code) |
||||||
|
user.save |
||||||
|
user.send_two_factor_authentication_code(code) |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,30 @@ |
|||||||
|
require 'two_factor_authentication/hooks/two_factor_authenticatable' |
||||||
|
module Devise |
||||||
|
module Models |
||||||
|
module TwoFactorAuthenticatable |
||||||
|
extend ActiveSupport::Concern |
||||||
|
|
||||||
|
module ClassMethods |
||||||
|
::Devise::Models.config(self, :login_code_random_pattern, :max_login_attempts) |
||||||
|
end |
||||||
|
|
||||||
|
module InstanceMethods |
||||||
|
def need_two_factor_authentication? |
||||||
|
true |
||||||
|
end |
||||||
|
|
||||||
|
def generate_two_factor_code |
||||||
|
self.class.login_code_random_pattern.gen |
||||||
|
end |
||||||
|
|
||||||
|
def send_two_factor_authentication_code(code) |
||||||
|
p "Code is #{code}" |
||||||
|
end |
||||||
|
|
||||||
|
def max_login_attempts? |
||||||
|
second_factor_attempts_count >= self.class.max_login_attempts |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,14 @@ |
|||||||
|
module TwoFactorAuthentication |
||||||
|
module Orm |
||||||
|
|
||||||
|
module ActiveRecord |
||||||
|
module Schema |
||||||
|
include TwoFactorAuthentication::Schema |
||||||
|
|
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
ActiveRecord::ConnectionAdapters::Table.send :include, TwoFactorAuthentication::Orm::ActiveRecord::Schema |
||||||
|
ActiveRecord::ConnectionAdapters::TableDefinition.send :include, TwoFactorAuthentication::Orm::ActiveRecord::Schema |
@ -0,0 +1,7 @@ |
|||||||
|
module TwoFactorAuthentication |
||||||
|
class Engine < ::Rails::Engine |
||||||
|
ActiveSupport.on_load(:action_controller) do |
||||||
|
include TwoFactorAuthentication::Controllers::Helpers |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,9 @@ |
|||||||
|
module ActionDispatch::Routing |
||||||
|
class Mapper |
||||||
|
protected |
||||||
|
|
||||||
|
def devise_two_factor_authentication(mapping, controllers) |
||||||
|
resource :two_factor_authentication, :only => [:show, :update], :path => mapping.path_names[:two_factor_authentication], :controller => controllers[:two_factor_authentication] |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,8 @@ |
|||||||
|
module TwoFactorAuthentication |
||||||
|
module Schema |
||||||
|
def two_factor_authenticatable |
||||||
|
apply_devise_schema :second_factor_pass_code, String, :limit => 32 |
||||||
|
apply_devise_schema :second_factor_attempts_count, Integer, :default => 0 |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,3 @@ |
|||||||
|
module TwoFactorAuthentication |
||||||
|
VERSION = "0.0.1" |
||||||
|
end |
@ -0,0 +1,26 @@ |
|||||||
|
# -*- encoding: utf-8 -*- |
||||||
|
$:.push File.expand_path("../lib", __FILE__) |
||||||
|
require "two_factor_authentication/version" |
||||||
|
|
||||||
|
Gem::Specification.new do |s| |
||||||
|
s.name = "two_factor_authentication" |
||||||
|
s.version = TwoFactorAuthentication::VERSION |
||||||
|
s.authors = ["Dmitrii Golub"] |
||||||
|
s.email = ["dmitrii.golub@gmail.com"] |
||||||
|
s.homepage = "" |
||||||
|
s.summary = %q{Two factor authentication plugin for devise} |
||||||
|
s.description = s.summary |
||||||
|
|
||||||
|
s.rubyforge_project = "two_factor_authentication" |
||||||
|
|
||||||
|
s.files = `git ls-files`.split("\n") |
||||||
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") |
||||||
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } |
||||||
|
s.require_paths = ["lib"] |
||||||
|
|
||||||
|
s.add_runtime_dependency 'rails', '>= 3.1.1' |
||||||
|
s.add_runtime_dependency 'devise' |
||||||
|
s.add_runtime_dependency 'randexp' |
||||||
|
|
||||||
|
s.add_development_dependency 'bundler' |
||||||
|
end |
Loading…
Reference in new issue