commit e4c812ad6dabfef6d34e2d829fa30590a24cd7c2 Author: Dmitrii Golub Date: Tue Jan 31 06:57:35 2012 +0400 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a14fa2 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..0c560a1 --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +source "http://rubygems.org" + +# Specify your gem's dependencies in devise_ip_filter.gemspec +gemspec diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0bd3cec --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..17c9605 --- /dev/null +++ b/README.md @@ -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 diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..2995527 --- /dev/null +++ b/Rakefile @@ -0,0 +1 @@ +require "bundler/gem_tasks" diff --git a/app/controllers/devise/two_factor_authentication_controller.rb b/app/controllers/devise/two_factor_authentication_controller.rb new file mode 100644 index 0000000..cb91685 --- /dev/null +++ b/app/controllers/devise/two_factor_authentication_controller.rb @@ -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 diff --git a/app/views/devise/two_factor_authentication/max_login_attempts_reached.html.erb b/app/views/devise/two_factor_authentication/max_login_attempts_reached.html.erb new file mode 100644 index 0000000..094bbf4 --- /dev/null +++ b/app/views/devise/two_factor_authentication/max_login_attempts_reached.html.erb @@ -0,0 +1,3 @@ +

Access completly denied as you have reached your attempts limit = <%= @limit %>

+

Please contact your system administrator

+ diff --git a/app/views/devise/two_factor_authentication/show.html.erb b/app/views/devise/two_factor_authentication/show.html.erb new file mode 100644 index 0000000..32ce38b --- /dev/null +++ b/app/views/devise/two_factor_authentication/show.html.erb @@ -0,0 +1,10 @@ +

Enter your personal code

+ +

<%= flash[:notice] %>

+ +<%= 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 %> diff --git a/config/locales/en.yml b/config/locales/en.yml new file mode 100644 index 0000000..0665ab6 --- /dev/null +++ b/config/locales/en.yml @@ -0,0 +1,4 @@ +en: + devise: + two_factor_authentication: + attempt_failed: "Attemp failed" diff --git a/lib/two_factor_authentication.rb b/lib/two_factor_authentication.rb new file mode 100644 index 0000000..4e218fb --- /dev/null +++ b/lib/two_factor_authentication.rb @@ -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' diff --git a/lib/two_factor_authentication/controllers/helpers.rb b/lib/two_factor_authentication/controllers/helpers.rb new file mode 100644 index 0000000..201b3a4 --- /dev/null +++ b/lib/two_factor_authentication/controllers/helpers.rb @@ -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 diff --git a/lib/two_factor_authentication/hooks/two_factor_authenticatable.rb b/lib/two_factor_authentication/hooks/two_factor_authenticatable.rb new file mode 100644 index 0000000..f6fa893 --- /dev/null +++ b/lib/two_factor_authentication/hooks/two_factor_authenticatable.rb @@ -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 diff --git a/lib/two_factor_authentication/models/two_factor_authenticatable.rb b/lib/two_factor_authentication/models/two_factor_authenticatable.rb new file mode 100644 index 0000000..0d1a9c7 --- /dev/null +++ b/lib/two_factor_authentication/models/two_factor_authenticatable.rb @@ -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 diff --git a/lib/two_factor_authentication/orm/active_record.rb b/lib/two_factor_authentication/orm/active_record.rb new file mode 100644 index 0000000..f3ec7ce --- /dev/null +++ b/lib/two_factor_authentication/orm/active_record.rb @@ -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 diff --git a/lib/two_factor_authentication/rails.rb b/lib/two_factor_authentication/rails.rb new file mode 100644 index 0000000..206c0b2 --- /dev/null +++ b/lib/two_factor_authentication/rails.rb @@ -0,0 +1,7 @@ +module TwoFactorAuthentication + class Engine < ::Rails::Engine + ActiveSupport.on_load(:action_controller) do + include TwoFactorAuthentication::Controllers::Helpers + end + end +end diff --git a/lib/two_factor_authentication/routes.rb b/lib/two_factor_authentication/routes.rb new file mode 100644 index 0000000..942c9d1 --- /dev/null +++ b/lib/two_factor_authentication/routes.rb @@ -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 diff --git a/lib/two_factor_authentication/schema.rb b/lib/two_factor_authentication/schema.rb new file mode 100644 index 0000000..0b63078 --- /dev/null +++ b/lib/two_factor_authentication/schema.rb @@ -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 diff --git a/lib/two_factor_authentication/version.rb b/lib/two_factor_authentication/version.rb new file mode 100644 index 0000000..4d7129d --- /dev/null +++ b/lib/two_factor_authentication/version.rb @@ -0,0 +1,3 @@ +module TwoFactorAuthentication + VERSION = "0.0.1" +end diff --git a/two_factor_authentication.gemspec b/two_factor_authentication.gemspec new file mode 100644 index 0000000..1cee8b6 --- /dev/null +++ b/two_factor_authentication.gemspec @@ -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