├── log └── .gitkeep ├── lib ├── tasks │ └── .gitkeep └── assets │ └── .gitkeep ├── public ├── favicon.ico ├── robots.txt ├── 500.html ├── 422.html └── 404.html ├── app ├── mailers │ └── .gitkeep ├── models │ ├── .gitkeep │ ├── user_session.rb │ └── user.rb ├── views │ ├── users │ │ ├── new.html.erb │ │ ├── edit.html.erb │ │ ├── _qr_code.html.erb │ │ ├── index.html.erb │ │ ├── _form.html.erb │ │ └── show.html.erb │ ├── welcome │ │ └── index.html.erb │ ├── user_sessions │ │ ├── confirm.html.erb │ │ └── new.html.erb │ └── layouts │ │ └── application.html.erb ├── assets │ ├── images │ │ └── rails.png │ ├── stylesheets │ │ ├── users.css.scss │ │ ├── qr_code.css.scss │ │ ├── application.css │ │ └── scaffolds.css.scss │ └── javascripts │ │ ├── users.js.coffee │ │ └── application.js ├── controllers │ ├── welcome_controller.rb │ ├── users_controller.rb │ ├── user_sessions_controller.rb │ └── application_controller.rb └── helpers │ └── error_messages_helper.rb ├── vendor ├── plugins │ └── .gitkeep └── assets │ ├── javascripts │ └── .gitkeep │ └── stylesheets │ └── .gitkeep ├── .rspec ├── config.ru ├── config ├── environment.rb ├── boot.rb ├── initializers │ ├── mime_types.rb │ ├── backtrace_silencers.rb │ ├── secret_token.rb │ ├── wrap_parameters.rb │ ├── inflections.rb │ └── session_store.rb ├── locales │ └── en.yml ├── routes.rb ├── database.yml ├── environments │ ├── development.rb │ ├── test.rb │ └── production.rb └── application.rb ├── doc └── README_FOR_APP ├── Rakefile ├── script └── rails ├── db ├── migrate │ ├── 20120219185441_add_user_two_factor.rb │ └── 20120218201855_create_users.rb ├── seeds.rb └── schema.rb ├── spec ├── factories │ └── users.rb ├── support │ └── auth_helper.rb ├── controllers │ ├── welcome_controller_spec.rb │ ├── users_controller_spec.rb │ └── user_sessions_controller_spec.rb ├── watchr.rb ├── spec_helper.rb └── models │ └── user_spec.rb ├── .gitignore ├── LICENSE ├── Gemfile ├── Guardfile ├── Gemfile.lock └── README.markdown /log/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/mailers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/plugins/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/assets/stylesheets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | #--format progress 3 | --format documentation 4 | -------------------------------------------------------------------------------- /app/views/users/new.html.erb: -------------------------------------------------------------------------------- 1 |
This is the test application's unprotected home page
3 | <% if current_user -%> 4 |You are authorized
5 | <% else -%> 6 |You need authorization to go beyond this page. Please login.
7 | <%= link_to "Login", login_url %> 8 | <% end -%> 9 | -------------------------------------------------------------------------------- /app/views/users/_qr_code.html.erb: -------------------------------------------------------------------------------- 1 || 7 | <% else %> 8 | | 9 | <% end %> 10 | <% end %> 11 | |
Please enter your security code from Google Authenticator
2 | 3 | <%= form_for :user_session, :url => {:action => :validate}, :html => {:method => :put} do |f| %> 4 | <%= f.text_field :validation_code, :autocomplete => "off" %> 5 | <%= f.submit 'Validate' %> 6 | <%= link_to "cancel", :logout, {:method => :delete} %> 7 | <% end %> 8 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /app/views/user_sessions/new.html.erb: -------------------------------------------------------------------------------- 1 |<%= flash[:error] %>
20 |<%= flash[:notice] %>
21 | 22 | <%= yield %> 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |Maybe you tried to change something you didn't have access to.
24 || First name | 7 |Last name | 8 |Login | 9 |Failure count | 10 |11 | | 12 | | 13 | | |
|---|---|---|---|---|---|---|---|
| <%= user.email %> | 18 |<%= user.first_name %> | 19 |<%= user.last_name %> | 20 |<%= user.login %> | 21 |<%= user.two_factor_failure_count %> | 22 |<%= link_to 'Show', user %> | 23 |<%= link_to 'Edit', edit_user_path(user) %> | 24 |<%= link_to 'Destroy', user, :confirm => 'Are you sure?', :method => :delete %> | 25 |
You may have mistyped the address or the page may have moved.
24 |2 | Login: 3 | <%= @user.login %> 4 |
5 | 6 |7 | Email: 8 | <%= @user.email %> 9 |
10 | 11 |12 | First name: 13 | <%= @user.first_name %> 14 |
15 | 16 |17 | Last name: 18 | <%= @user.last_name %> 19 |
20 | 21 |22 | Login count: 23 | <%=h @user.login_count %> 24 |
25 | 26 |27 | Last request at: 28 | <%=h @user.last_request_at %> 29 |
30 | 31 |32 | Last login at: 33 | <%=h @user.last_login_at %> 34 |
35 | 36 |37 | Current login at: 38 | <%=h @user.current_login_at %> 39 |
40 | 41 |42 | Last login ip: 43 | <%=h @user.last_login_ip %> 44 |
45 | 46 |47 | Current login ip: 48 | <%=h @user.current_login_ip %> 49 |
50 | 51 |52 | Two factor secret: 53 | <%=h @user.two_factor_secret.scan(/.{4}/).join(' ') if @user.two_factor_secret %> 54 |
55 | 56 |57 | Two factor failure count: 58 | <%=h @user.two_factor_failure_count %> 59 |
60 | 61 | <% if @qr -%> 62 | <%= render :partial => 'qr_code', :locals => {:qr_code => @qr} %> 63 | <% end %> 64 | 65 | <%= link_to 'Edit', edit_user_path(@user) %> | 66 | <%= link_to 'Back', users_path %> 67 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rails', '3.2.11' 4 | 5 | # Bundle edge Rails instead: 6 | # gem 'rails', :git => 'git://github.com/rails/rails.git' 7 | 8 | gem 'sqlite3' 9 | 10 | gem 'json' 11 | 12 | # Gems used only for assets and not required 13 | # in production environments by default. 14 | group :assets do 15 | gem 'sass-rails', '~> 3.2.3' 16 | gem 'coffee-rails', '~> 3.2.1' 17 | 18 | # See https://github.com/sstephenson/execjs#readme for more supported runtimes 19 | # gem 'therubyracer' 20 | 21 | gem 'uglifier', '>= 1.0.3' 22 | end 23 | 24 | gem 'jquery-rails' 25 | 26 | # To use ActiveModel has_secure_password 27 | # gem 'bcrypt-ruby', '~> 3.0.0' 28 | 29 | # To use Jbuilder templates for JSON 30 | # gem 'jbuilder' 31 | 32 | # Use unicorn as the web server 33 | # gem 'unicorn' 34 | 35 | # Deploy with Capistrano 36 | # gem 'capistrano' 37 | 38 | # To use debugger 39 | # gem 'ruby-debug' 40 | 41 | gem "authlogic", ">= 3.1.0" 42 | gem "rotp", "~> 1.3.2" 43 | gem "rqrcode", "~> 0.4.2" 44 | gem "ipaddress", "~> 0.8.0" 45 | gem 'uuidtools', "~> 2.1.2" 46 | 47 | group :test, :development do 48 | gem "rspec-rails", "~> 2.8" 49 | gem "factory_girl_rails", "~> 1.6" 50 | gem "capybara", "~> 1.1" 51 | gem "database_cleaner", "~> 0.7.1" 52 | gem "timecop", "= 0.3.5" 53 | gem "shoulda-matchers", "~> 1.0.0" 54 | # guard 55 | gem "guard", "~> 1.0" 56 | gem "guard-rspec", ">= 0.6" 57 | gem "spork-rails", "~> 3.2.0" 58 | gem "guard-spork", "~> 0.7.1" 59 | end 60 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | RailsApp::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Log error messages when you accidentally call methods on nil. 10 | config.whiny_nils = true 11 | 12 | # Show full error reports and disable caching 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger 20 | config.active_support.deprecation = :log 21 | 22 | # Only use best-standards-support built into browsers 23 | config.action_dispatch.best_standards_support = :builtin 24 | 25 | # Raise exception on mass assignment protection for Active Record models 26 | config.active_record.mass_assignment_sanitizer = :strict 27 | 28 | # Log the query plan for queries taking more than this (works 29 | # with SQLite, MySQL, and PostgreSQL) 30 | config.active_record.auto_explain_threshold_in_seconds = 0.5 31 | 32 | # Do not compress assets 33 | config.assets.compress = false 34 | 35 | # Expands the lines which load the assets 36 | config.assets.debug = true 37 | end 38 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | RailsApp::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Configure static asset server for tests with Cache-Control for performance 11 | config.serve_static_assets = true 12 | config.static_cache_control = "public, max-age=3600" 13 | 14 | # Log error messages when you accidentally call methods on nil 15 | config.whiny_nils = true 16 | 17 | # Show full error reports and disable caching 18 | config.consider_all_requests_local = true 19 | config.action_controller.perform_caching = false 20 | 21 | # Raise exceptions instead of rendering exception templates 22 | config.action_dispatch.show_exceptions = false 23 | 24 | # Disable request forgery protection in test environment 25 | config.action_controller.allow_forgery_protection = false 26 | 27 | # Tell Action Mailer not to deliver emails to the real world. 28 | # The :test delivery method accumulates sent emails in the 29 | # ActionMailer::Base.deliveries array. 30 | config.action_mailer.delivery_method = :test 31 | 32 | # Raise exception on mass assignment protection for Active Record models 33 | config.active_record.mass_assignment_sanitizer = :strict 34 | 35 | # Print deprecation notices to the stderr 36 | config.active_support.deprecation = :stderr 37 | end 38 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # starts up/reloads the spork server 2 | guard 'spork', :cucumber_env => { 'RAILS_ENV' => 'test' }, :rspec_env => { 'RAILS_ENV' => 'test' } do 3 | watch('config/application.rb') 4 | watch('config/environment.rb') 5 | watch(%r{^config/environments/.+\.rb$}) 6 | watch(%r{^config/initializers/.+\.rb$}) 7 | watch('Gemfile') 8 | watch('Gemfile.lock') 9 | watch('spec/spec_helper.rb') { :rspec } 10 | watch('spec/framework_spec_helper.rb') { :rspec } 11 | watch('spec/shoulda_spec_helper.rb') { :rspec } 12 | watch('test/test_helper.rb') { :test_unit } 13 | watch(%r{features/support/}) { :cucumber } 14 | end 15 | 16 | group :specs do 17 | guard 'rspec', 18 | :all_after_pass => false, 19 | :all_on_start => false, 20 | :bundler => false, 21 | :cli => '--drb --color --format nested', 22 | :version => 2 do 23 | 24 | watch('spec/spec_helper.rb') { "spec" } 25 | watch('config/routes.rb') { "spec/routing" } 26 | watch('app/controllers/application_controller.rb') { "spec/controllers" } 27 | 28 | watch(%r{^spec/.+_spec\.rb}) 29 | watch(%r{^app/(.+)\.rb}) { |m| "spec/#{m[1]}_spec.rb" } 30 | watch(%r{^lib/(.+)\.rb}) { |m| "spec/lib/#{m[1]}_spec.rb" } 31 | watch(%r{^app/controllers/(.+)_(controller)\.rb}) { |m| [ "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb" ] } 32 | watch(%r{^app/views/(.+)/}) { |m| "spec/controllers/#{m[1]}_controller_spec.rb" } 33 | 34 | watch(%r{^spec/factories/(.*)\.rb} ) { |m| "spec/controllers/%s_controller_spec.rb" % m[1] } 35 | watch(%r{^app/helpers/(.*)/.*} ) { |m| "spec/controllers/%s_controller_spec.rb" % m[1] } 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # Note that this schema.rb definition is the authoritative source for your 6 | # database schema. If you need to create the application database on another 7 | # system, you should be using db:schema:load, not running all the migrations 8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 9 | # you'll amass, the slower it'll run and the greater likelihood for issues). 10 | # 11 | # It's strongly recommended to check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(:version => 20120219185441) do 14 | 15 | create_table "users", :force => true do |t| 16 | t.string "email" 17 | t.string "first_name" 18 | t.string "last_name" 19 | t.string "login" 20 | t.integer "lock_version", :default => 0 21 | t.string "crypted_password" 22 | t.string "password_salt" 23 | t.string "persistence_token" 24 | t.string "single_access_token" 25 | t.string "perishable_token" 26 | t.integer "login_count", :default => 0, :null => false 27 | t.integer "failed_login_count", :default => 0, :null => false 28 | t.datetime "last_request_at" 29 | t.datetime "current_login_at" 30 | t.datetime "last_login_at" 31 | t.string "current_login_ip" 32 | t.string "last_login_ip" 33 | t.boolean "active", :default => true 34 | t.boolean "approved", :default => true 35 | t.boolean "confirmed", :default => true 36 | t.datetime "created_at", :null => false 37 | t.datetime "updated_at", :null => false 38 | t.string "two_factor_secret" 39 | t.string "two_factor_confirmed_at" 40 | t.integer "two_factor_failure_count", :default => 0, :null => false 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | attr_accessible :login, :email, :password, :password_confirmation, :first_name, :last_name 3 | 4 | acts_as_authentic do |c| 5 | end 6 | 7 | before_validation :assign_two_factor_secret, :on => :create 8 | 9 | def assign_two_factor_secret 10 | self.two_factor_secret = ROTP::Base32.random_base32 11 | end 12 | 13 | def confirm_two_factor! 14 | reset_two_factor_failure_count! 15 | value = UUIDTools::UUID.timestamp_create.to_s 16 | update_attribute :two_factor_confirmed_at, value 17 | value 18 | end 19 | 20 | def two_factor_confirmed_at_valid? 21 | return false unless self.two_factor_confirmed_at 22 | begin 23 | uuid = UUIDTools::UUID.parse(self.two_factor_confirmed_at) 24 | uuid.valid? && lambda { (Time.now.utc < (uuid.timestamp.utc + two_factor_confirmed_at_valid_for)) }.call 25 | rescue 26 | return false 27 | end 28 | end 29 | 30 | # length of time in seconds the TFA confirmation is valid 31 | def two_factor_confirmed_at_valid_for 32 | 12.hours 33 | end 34 | 35 | def reset_two_factor_failure_count! 36 | update_attribute :two_factor_failure_count, 0 37 | end 38 | 39 | def increment_two_factor_failure_count! 40 | count = self.two_factor_failure_count 41 | count += 1 42 | update_attribute :two_factor_failure_count, count 43 | end 44 | 45 | # lock out TFA if true 46 | def two_factor_failure_count_exceeded? 47 | self.two_factor_failure_count >= 5 48 | end 49 | 50 | # QRCode suitable for display 51 | # 52 | # CSS 53 | # 54 | # app/assets/stylesheets/qr_code.css.scss 55 | # 56 | # Template 57 | # 58 | # app/views/users/_qr_code.html.erb 59 | # 60 | def get_two_factor_secret_qr_code(size = 9, level = :h) 61 | secret = self.two_factor_secret 62 | if secret 63 | totp = ROTP::TOTP.new(secret) 64 | raw_string = totp.provisioning_uri("RailsApp #{self.email}") 65 | # at the default size of 9, we can accomodate ~ 100 8 bit characters 66 | return nil if raw_string.length >= 100 67 | RQRCode::QRCode.new(raw_string, :size => size, :level => level) 68 | end 69 | end 70 | 71 | end 72 | -------------------------------------------------------------------------------- /app/controllers/users_controller.rb: -------------------------------------------------------------------------------- 1 | class UsersController < ApplicationController 2 | 3 | # GET /users 4 | # GET /users.json 5 | def index 6 | @users = User.all 7 | 8 | respond_to do |format| 9 | format.html # index.html.erb 10 | format.json { render :json => @users } 11 | end 12 | end 13 | 14 | # GET /users/1 15 | # GET /users/1.json 16 | def show 17 | @user = User.find(params[:id]) 18 | @qr = @user.get_two_factor_secret_qr_code 19 | 20 | respond_to do |format| 21 | format.html # show.html.erb 22 | format.json { render :json => @user } 23 | end 24 | end 25 | 26 | # GET /users/new 27 | # GET /users/new.json 28 | def new 29 | @user = User.new 30 | 31 | respond_to do |format| 32 | format.html # new.html.erb 33 | format.json { render :json => @user } 34 | end 35 | end 36 | 37 | # GET /users/1/edit 38 | def edit 39 | @user = User.find(params[:id]) 40 | end 41 | 42 | # POST /users 43 | # POST /users.json 44 | def create 45 | @user = User.new(params[:user]) 46 | 47 | respond_to do |format| 48 | if @user.save 49 | format.html { redirect_to @user, :notice => 'User was successfully created.' } 50 | format.json { render :json => @user, :status => :created, :location => @user } 51 | else 52 | format.html { render :action => "new" } 53 | format.json { render :json => @user.errors, :status => :unprocessable_entity } 54 | end 55 | end 56 | end 57 | 58 | # PUT /users/1 59 | # PUT /users/1.json 60 | def update 61 | @user = User.find(params[:id]) 62 | 63 | respond_to do |format| 64 | if @user.update_attributes(params[:user]) 65 | format.html { redirect_to @user, :notice => 'User was successfully updated.' } 66 | format.json { head :no_content } 67 | else 68 | format.html { render :action => "edit" } 69 | format.json { render :json => @user.errors, :status => :unprocessable_entity } 70 | end 71 | end 72 | end 73 | 74 | # DELETE /users/1 75 | # DELETE /users/1.json 76 | def destroy 77 | @user = User.find(params[:id]) 78 | @user.destroy 79 | 80 | respond_to do |format| 81 | format.html { redirect_to users_url } 82 | format.json { head :no_content } 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | RailsApp::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # Code is not reloaded between requests 5 | config.cache_classes = true 6 | 7 | # Full error reports are disabled and caching is turned on 8 | config.consider_all_requests_local = false 9 | config.action_controller.perform_caching = true 10 | 11 | # Disable Rails's static asset server (Apache or nginx will already do this) 12 | config.serve_static_assets = false 13 | 14 | # Compress JavaScripts and CSS 15 | config.assets.compress = true 16 | 17 | # Don't fallback to assets pipeline if a precompiled asset is missed 18 | config.assets.compile = false 19 | 20 | # Generate digests for assets URLs 21 | config.assets.digest = true 22 | 23 | # Defaults to Rails.root.join("public/assets") 24 | # config.assets.manifest = YOUR_PATH 25 | 26 | # Specifies the header that your server uses for sending files 27 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 28 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 29 | 30 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 31 | config.force_ssl = true 32 | 33 | # See everything in the log (default is :info) 34 | # config.log_level = :debug 35 | 36 | # Prepend all log lines with the following tags 37 | # config.log_tags = [ :subdomain, :uuid ] 38 | 39 | # Use a different logger for distributed setups 40 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 41 | 42 | # Use a different cache store in production 43 | # config.cache_store = :mem_cache_store 44 | 45 | # Enable serving of images, stylesheets, and JavaScripts from an asset server 46 | # config.action_controller.asset_host = "http://assets.example.com" 47 | 48 | # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) 49 | # config.assets.precompile += %w( search.js ) 50 | 51 | # Disable delivery errors, bad email addresses will be ignored 52 | # config.action_mailer.raise_delivery_errors = false 53 | 54 | # Enable threaded mode 55 | # config.threadsafe! 56 | 57 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 58 | # the I18n.default_locale when a translation can not be found) 59 | config.i18n.fallbacks = true 60 | 61 | # Send deprecation notices to registered listeners 62 | config.active_support.deprecation = :notify 63 | 64 | # Log the query plan for queries taking more than this (works 65 | # with SQLite, MySQL, and PostgreSQL) 66 | # config.active_record.auto_explain_threshold_in_seconds = 0.5 67 | end 68 | -------------------------------------------------------------------------------- /spec/watchr.rb: -------------------------------------------------------------------------------- 1 | # Watchr: Autotest like functionality 2 | # 3 | # Run me with: 4 | # 5 | # $ watchr spec/watchr.rb 6 | 7 | require 'term/ansicolor' 8 | 9 | $c = Term::ANSIColor 10 | 11 | def getch 12 | state = `stty -g` 13 | begin 14 | `stty raw -echo cbreak` 15 | $stdin.getc 16 | ensure 17 | `stty #{state}` 18 | end 19 | end 20 | 21 | # -------------------------------------------------- 22 | # Convenience Methods 23 | # -------------------------------------------------- 24 | def all_spec_files 25 | Dir['spec/models/*_spec.rb'] + Dir['spec/controllers/*_spec.rb'] + Dir['spec/framework/*_spec.rb'] 26 | end 27 | 28 | def run(cmd) 29 | 30 | pid = fork do 31 | puts "\n" 32 | print $c.cyan, cmd, $c.clear, "\n" 33 | exec(cmd) 34 | end 35 | Signal.trap('INT') do 36 | puts "sending KILL to pid: #{pid}" 37 | Process.kill("KILL", pid) 38 | end 39 | Process.waitpid(pid) 40 | 41 | prompt 42 | end 43 | 44 | def run_all 45 | run_all_specs 46 | end 47 | 48 | def run_default_spec 49 | cmd = "rspec" 50 | run(cmd) 51 | end 52 | 53 | def run_all_specs 54 | cmd = "rspec #{all_spec_files.join(' ')}" 55 | p cmd 56 | run(cmd) 57 | end 58 | 59 | def run_spec(spec) 60 | cmd = "rspec #{spec}" 61 | $last_spec = spec 62 | run(cmd) 63 | end 64 | 65 | def run_last_spec 66 | run_spec($last_spec) if $last_spec 67 | end 68 | 69 | def prompt 70 | puts "Ctrl-\\ for menu, Ctrl-C to quit" 71 | end 72 | 73 | # init 74 | prompt 75 | # -------------------------------------------------- 76 | # Watchr Rules 77 | # -------------------------------------------------- 78 | watch( '^spec.*/controllers/.*_spec\.rb' ) { |m| run_spec(m[0]) } 79 | watch( '^spec.*/models/.*_spec\.rb' ) { |m| run_spec(m[0]) } 80 | watch( '^spec.*/framework/.*_spec\.rb' ) { |m| run_spec(m[0]) } 81 | 82 | watch( '^spec/factories/(.*)\.rb' ) { |m| run_spec("spec/controllers/%s_controller_spec.rb" % m[1]) } 83 | watch( '^app/models/(.*)\.rb' ) { |m| run_spec("spec/models/%s_spec.rb" % m[1]) } 84 | watch( '^app/controllers/(.*)\.rb' ) { |m| run_spec("spec/controllers/%s_spec.rb" % m[1]) } 85 | watch( '^app/views/(.*)/.*' ) { |m| run_spec("spec/controllers/%s_controller_spec.rb" % m[1]) } 86 | watch( '^app/helpers/(.*)/.*' ) { |m| run_spec("spec/controllers/%s_controller_spec.rb" % m[1]) } 87 | 88 | watch( '^spec/spec_helper\.rb' ) { run_all_specs } 89 | 90 | # -------------------------------------------------- 91 | # Signal Handling 92 | # -------------------------------------------------- 93 | 94 | # Ctrl-\ 95 | Signal.trap('QUIT') do 96 | 97 | puts "\n\nMENU: a = all , s = specs, q = quit\n\n" 98 | c = getch 99 | puts c.chr 100 | if c.chr == "a" 101 | run_all 102 | elsif c.chr == "s" 103 | run_all_specs 104 | elsif c.chr == "q" 105 | abort("exiting\n") 106 | end 107 | 108 | end 109 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | # Pick the frameworks you want: 4 | require "active_record/railtie" 5 | require "action_controller/railtie" 6 | require "action_mailer/railtie" 7 | require "active_resource/railtie" 8 | require "sprockets/railtie" 9 | # require "rails/test_unit/railtie" 10 | 11 | if defined?(Bundler) 12 | # If you precompile assets before deploying to production, use this line 13 | Bundler.require(*Rails.groups(:assets => %w(development test))) 14 | # If you want your assets lazily compiled in production, use this line 15 | # Bundler.require(:default, :assets, Rails.env) 16 | end 17 | 18 | module RailsApp 19 | class Application < Rails::Application 20 | # Settings in config/environments/* take precedence over those specified here. 21 | # Application configuration should go into files in config/initializers 22 | # -- all .rb files in that directory are automatically loaded. 23 | 24 | # Custom directories with classes and modules you want to be autoloadable. 25 | # config.autoload_paths += %W(#{config.root}/extras) 26 | 27 | # Only load the plugins named here, in the order given (default is alphabetical). 28 | # :all can be used as a placeholder for all plugins not explicitly named. 29 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 30 | 31 | # Activate observers that should always be running. 32 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer 33 | 34 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 35 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 36 | # config.time_zone = 'Central Time (US & Canada)' 37 | 38 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 39 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 40 | # config.i18n.default_locale = :de 41 | 42 | # Configure the default encoding used in templates for Ruby 1.9. 43 | config.encoding = "utf-8" 44 | 45 | # Configure sensitive parameters which will be filtered from the log file. 46 | config.filter_parameters += [:password] 47 | 48 | # Use SQL instead of Active Record's schema dumper when creating the database. 49 | # This is necessary if your schema can't be completely dumped by the schema dumper, 50 | # like if you have constraints or database-specific column types 51 | # config.active_record.schema_format = :sql 52 | 53 | # Enforce whitelist mode for mass assignment. 54 | # This will create an empty whitelist of attributes available for mass-assignment for all models 55 | # in your app. As such, your models will need to explicitly whitelist or blacklist accessible 56 | # parameters by using an attr_accessible or attr_protected declaration. 57 | # config.active_record.whitelist_attributes = true 58 | 59 | # Enable the asset pipeline 60 | config.assets.enabled = true 61 | 62 | # Version of your assets, change this if you want to expire all your assets 63 | config.assets.version = '1.0' 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /app/controllers/user_sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class UserSessionsController < ApplicationController 2 | before_filter :require_no_user, :only => [:new, :create] 3 | skip_before_filter :require_user, :only => [:new, :create] 4 | skip_before_filter :require_two_factor 5 | 6 | def new 7 | @user_session = UserSession.new 8 | end 9 | 10 | def create 11 | clear_session 12 | @user_session = UserSession.new(params[:user_session]) 13 | if @user_session.save 14 | if two_factor_required? 15 | flash[:notice] = "Login successful, security token required" 16 | redirect_to confirm_url 17 | else 18 | flash[:notice] = "Login successful!" 19 | redirect_back '/' 20 | end 21 | else 22 | render :action => :new 23 | end 24 | end 25 | 26 | def destroy 27 | current_user_session.destroy 28 | reset_session 29 | flash[:notice] = "Logout successful!" 30 | redirect_back login_url 31 | end 32 | 33 | def confirm 34 | end 35 | 36 | def validate 37 | two_factor_secret = current_user.two_factor_secret 38 | validation_code = params[:user_session][:validation_code] 39 | 40 | if !two_factor_secret 41 | current_user_session.destroy 42 | reset_session 43 | flash[:error] = "Two factor authentication is not setup on your account. Please contact the admin." 44 | redirect_back login_url 45 | elsif current_user.two_factor_failure_count_exceeded? 46 | current_user_session.destroy 47 | reset_session 48 | flash[:error] = "Two factor confirmation failure count exceeded. Please contact the admin." 49 | redirect_to :root 50 | elsif validate_code(validation_code, two_factor_secret) 51 | session[:two_factor_confirmed_at] = current_user.confirm_two_factor! 52 | flash[:notice] = 'Your session has been confirmed' 53 | redirect_back :root 54 | else 55 | current_user.increment_two_factor_failure_count! 56 | flash[:error] = "Token invalid!" 57 | redirect_to :action => :confirm 58 | end 59 | end 60 | 61 | private 62 | 63 | # clear the entire session except for the return_to redirect 64 | def clear_session 65 | return_to = session[:return_to] 66 | reset_session 67 | session[:return_to] = return_to if return_to 68 | end 69 | 70 | # True if code validates within the sliding window 71 | # 72 | # @return [Boolean] 73 | def validate_code(validation_code, two_factor_secret) 74 | valid_codes = [] 75 | valid_codes << ROTP::TOTP.new(two_factor_secret).now 76 | (1..sliding_window_width).each do |index| 77 | valid_codes << ROTP::TOTP.new(two_factor_secret).at(Time.now.ago(30 * index)) 78 | valid_codes << ROTP::TOTP.new(two_factor_secret).at(Time.now.in(30 * index)) 79 | end 80 | 81 | valid_codes.include?(validation_code.to_i) 82 | end 83 | 84 | # Use a sliding time window to validate tokens. System clock inaccuracy can 85 | # be tolerated at the expense a small decrease in security. A value of 0 86 | # will disable the sliding window 87 | # 88 | # A value of 2 will check tokens in two windows before and after the current 89 | # 30 second window. ie. +/- 60 seconds surrounding the current window. 90 | # 91 | # @return [Integer] width of the window in 30 second increments 92 | def sliding_window_width 93 | 1 94 | end 95 | 96 | end 97 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery 3 | helper :all 4 | helper_method :current_user_session, :current_user 5 | before_filter :require_user # must be logged in, redirect to login if not 6 | before_filter :require_two_factor # verify two factor token 7 | 8 | private 9 | 10 | def current_user_session 11 | return @current_user_session if defined?(@current_user_session) 12 | @current_user_session = UserSession.find 13 | end 14 | 15 | def current_user 16 | return @current_user if defined?(@current_user) 17 | @current_user = current_user_session && current_user_session.record 18 | end 19 | 20 | def require_user 21 | unless current_user 22 | store_location 23 | flash[:notice] = "You must be logged in to access this page" 24 | redirect_to login_url 25 | return false 26 | end 27 | end 28 | 29 | def require_no_user 30 | if current_user 31 | store_location 32 | flash[:notice] = "You must be logged out to access this page" 33 | redirect_to '/' 34 | return false 35 | end 36 | end 37 | 38 | def require_two_factor 39 | return unless current_user && two_factor_required? 40 | unless two_factor_confirmed? 41 | redirect_to confirm_url 42 | return false 43 | end 44 | end 45 | 46 | def two_factor_required? 47 | request_ip = IPAddress.parse(request.ip) 48 | two_factor_excluded_ip_addresses.each do |excluded_ip| 49 | return false if excluded_ip.include?(request_ip) 50 | end 51 | return true 52 | end 53 | 54 | # two factor exclude IP addresses, these addresses will bypass TFA 55 | # 56 | # @example disable TFA, allow any IP address to bypass 57 | # 58 | # [IPAddress.parse("0.0.0.0/0")] 59 | # 60 | # @example allow localhost 127.0.0.0 -> 127.0.0.255 to bypass TFA 61 | # 62 | # [IPAddress.parse("127.0.0.1/24")] 63 | # 64 | # @example allow localhost and private LAN addresses to bypass TFA 65 | # 66 | # [IPAddress.parse("127.0.0.0/8"), 67 | # IPAddress.parse("10.0.0.0/8"), 68 | # IPAddress.parse("172.16.0.0/12"), 69 | # IPAddress.parse("192.168.0.0/16") 70 | # ] 71 | # 72 | # @example allow no addresses to bypass TFA 73 | # 74 | # [] 75 | # 76 | # @return [Array] of exclude IP address objects 77 | def two_factor_excluded_ip_addresses 78 | [] 79 | end 80 | 81 | # NOTE: 82 | # 'two_factor_confirmed?' doesn't persist with "remember_me", it dies 83 | # with the session. 84 | # 85 | # NOTE: 86 | # If the Authlogic session expires/goes stale, the entire session (except for 87 | # :return_to) will be reset on the next redirect to a new user session. 88 | # 89 | # @return [Boolean] true if two factor confirmed 90 | def two_factor_confirmed? 91 | #current_user.two_factor_confirmed_at && session[:two_factor_confirmed_at] == current_user.two_factor_confirmed_at 92 | current_user.two_factor_confirmed_at_valid? && session[:two_factor_confirmed_at] == current_user.two_factor_confirmed_at 93 | end 94 | 95 | def store_location 96 | session[:return_to] = request.fullpath 97 | end 98 | 99 | def redirect_back(default) 100 | redirect_to(session[:return_to] || default) 101 | session[:return_to] = nil 102 | end 103 | 104 | end 105 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'spork' 2 | #uncomment the following line to use spork with the debugger 3 | #require 'spork/ext/ruby-debug' 4 | 5 | # --- Instructions --- 6 | # Sort the contents of this file into a Spork.prefork and a Spork.each_run 7 | # block. 8 | # 9 | # The Spork.prefork block is run only once when the spork server is started. 10 | # You typically want to place most of your (slow) initializer code in here, in 11 | # particular, require'ing any 3rd-party gems that you don't normally modify 12 | # during development. 13 | # 14 | # The Spork.each_run block is run each time you run your specs. In case you 15 | # need to load files that tend to change during development, require them here. 16 | # With Rails, your application modules are loaded automatically, so sometimes 17 | # this block can remain empty. 18 | # 19 | # Note: You can modify files loaded *from* the Spork.each_run block without 20 | # restarting the spork server. However, this file itself will not be reloaded, 21 | # so if you change any of the code inside the each_run block, you still need to 22 | # restart the server. In general, if you have non-trivial code in this file, 23 | # it's advisable to move it into a separate file so you can easily edit it 24 | # without restarting spork. (For example, with RSpec, you could move 25 | # non-trivial code into a file spec/support/my_helper.rb, making sure that the 26 | # spec/support/* files are require'd from inside the each_run block.) 27 | # 28 | # Any code that is left outside the two blocks will be run during preforking 29 | # *and* during each_run -- that's probably not what you want. 30 | # 31 | # These instructions should self-destruct in 10 seconds. If they don't, feel 32 | # free to delete them. 33 | 34 | # Loading more in this block will cause your tests to run faster. However, 35 | # if you change any configuration or code from libraries loaded here, you'll 36 | # need to restart spork for it take effect. 37 | Spork.prefork do 38 | 39 | # This file is copied to spec/ when you run 'rails generate rspec:install' 40 | ENV["RAILS_ENV"] ||= 'test' 41 | require File.expand_path("../../config/environment", __FILE__) 42 | require 'rspec/rails' 43 | require 'rspec/autorun' 44 | 45 | require 'authlogic/test_case' 46 | include Authlogic::TestCase 47 | 48 | # Requires supporting ruby files with custom matchers and macros, etc, 49 | # in spec/support/ and its subdirectories. 50 | Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f} 51 | include AuthHelper 52 | 53 | RSpec.configure do |config| 54 | # Focus specs: 55 | # it "does something", :focus => true do 56 | config.filter_run :focus => true 57 | config.run_all_when_everything_filtered = true 58 | 59 | # ## Mock Framework 60 | # 61 | # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line: 62 | # 63 | # config.mock_with :mocha 64 | # config.mock_with :flexmock 65 | # config.mock_with :rr 66 | 67 | # If you're not using ActiveRecord, or you'd prefer not to run each of your 68 | # examples within a transaction, remove the following line or assign false 69 | # instead of true. 70 | config.use_transactional_fixtures = true 71 | 72 | # If true, the base class of anonymous controllers will be inferred 73 | # automatically. This will be the default behavior in future versions of 74 | # rspec-rails. 75 | config.infer_base_class_for_anonymous_controllers = false 76 | 77 | config.before(:suite) do 78 | find_or_create_user("user") 79 | end 80 | 81 | end 82 | end 83 | 84 | # This code will be run each time you run your specs. 85 | Spork.each_run do 86 | FactoryGirl.reload 87 | end 88 | -------------------------------------------------------------------------------- /spec/models/user_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe User do 4 | 5 | describe "validations" do 6 | it { should validate_uniqueness_of(:login) } 7 | it { should ensure_length_of(:password).is_at_least(4) } 8 | end 9 | 10 | describe "two factor authentication" do 11 | 12 | describe "being created" do 13 | 14 | it "should create a two_factor_secret automatically" do 15 | user = Factory.build(:user) 16 | user.two_factor_secret.should be_nil 17 | user.should be_valid 18 | user.save! 19 | user.two_factor_secret.should_not be_nil 20 | end 21 | it "should set two_factor_failure_count to zero" do 22 | user = Factory.build(:user) 23 | user.two_factor_secret.should be_nil 24 | user.should be_valid 25 | user.two_factor_failure_count.should == 0 26 | end 27 | end 28 | 29 | describe "reset_two_factor_failure_count!" do 30 | 31 | it "should set two_factor_failure_count to zero and save the record" do 32 | user = Factory.build(:user) 33 | user.two_factor_failure_count = 5 34 | user.should be_changed 35 | user.save! 36 | user.two_factor_failure_count.should == 5 37 | user.should_not be_changed 38 | user.reset_two_factor_failure_count! 39 | user.two_factor_failure_count.should == 0 40 | user.should_not be_changed 41 | end 42 | end 43 | 44 | describe "increment_two_factor_failure_count!" do 45 | 46 | it "should increment two_factor_failure_count by 1 and save the record" do 47 | user = Factory.build(:user) 48 | user.two_factor_failure_count = 5 49 | user.save! 50 | user.increment_two_factor_failure_count! 51 | user.two_factor_failure_count.should == 6 52 | user.should_not be_changed 53 | end 54 | end 55 | 56 | describe "confirm_two_factor!" do 57 | 58 | it "should update two_factor_confirmed_at with a time based UUID" do 59 | user = Factory.build(:user) 60 | user.two_factor_confirmed_at.should be_nil 61 | user.confirm_two_factor! 62 | user.should_not be_changed 63 | UUIDTools::UUID.parse(user.two_factor_confirmed_at).should_not be_nil_uuid 64 | end 65 | 66 | it "should reset two_factor_failure_count" do 67 | user = Factory.build(:user) 68 | user.two_factor_failure_count = 5 69 | user.save! 70 | user.two_factor_failure_count.should == 5 71 | user.confirm_two_factor! 72 | user.two_factor_failure_count.should == 0 73 | user.should_not be_changed 74 | end 75 | end 76 | 77 | describe "two_factor_confirmed_at_valid_for" do 78 | 79 | it "should return Fixnum duration in seconds" do 80 | user = Factory.build(:user) 81 | user.two_factor_confirmed_at_valid_for.is_a?(Fixnum).should be_true 82 | end 83 | end 84 | 85 | describe "two_factor_confirmed_at_valid?" do 86 | 87 | it "should not be valid if nil" do 88 | user = Factory.build(:user) 89 | user.two_factor_confirmed_at.should be_nil 90 | user.two_factor_confirmed_at_valid?.should be_false 91 | user.confirm_two_factor! 92 | user.two_factor_confirmed_at_valid?.should be_true 93 | end 94 | 95 | it "should not be valid if garbage" do 96 | user = Factory.build(:user) 97 | user.two_factor_confirmed_at = "12abc" 98 | user.save! 99 | user.two_factor_confirmed_at_valid?.should be_false 100 | end 101 | 102 | it "should not be valid if expired" do 103 | User.any_instance.stub(:two_factor_confirmed_at_valid_for).and_return(1.hour) 104 | user = Factory.build(:user) 105 | user.confirm_two_factor! 106 | user.two_factor_confirmed_at_valid?.should be_true 107 | Timecop.travel Time.now + 1.hour + 1.second 108 | user.two_factor_confirmed_at_valid?.should be_false 109 | Timecop.return 110 | end 111 | end 112 | end 113 | 114 | end 115 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actionmailer (3.2.11) 5 | actionpack (= 3.2.11) 6 | mail (~> 2.4.4) 7 | actionpack (3.2.11) 8 | activemodel (= 3.2.11) 9 | activesupport (= 3.2.11) 10 | builder (~> 3.0.0) 11 | erubis (~> 2.7.0) 12 | journey (~> 1.0.4) 13 | rack (~> 1.4.0) 14 | rack-cache (~> 1.2) 15 | rack-test (~> 0.6.1) 16 | sprockets (~> 2.2.1) 17 | activemodel (3.2.11) 18 | activesupport (= 3.2.11) 19 | builder (~> 3.0.0) 20 | activerecord (3.2.11) 21 | activemodel (= 3.2.11) 22 | activesupport (= 3.2.11) 23 | arel (~> 3.0.2) 24 | tzinfo (~> 0.3.29) 25 | activeresource (3.2.11) 26 | activemodel (= 3.2.11) 27 | activesupport (= 3.2.11) 28 | activesupport (3.2.11) 29 | i18n (~> 0.6) 30 | multi_json (~> 1.0) 31 | addressable (2.3.2) 32 | arel (3.0.2) 33 | authlogic (3.2.0) 34 | activerecord (>= 3.0.0) 35 | activesupport (>= 3.0.0) 36 | builder (3.0.4) 37 | capybara (1.1.2) 38 | mime-types (>= 1.16) 39 | nokogiri (>= 1.3.3) 40 | rack (>= 1.0.0) 41 | rack-test (>= 0.5.4) 42 | selenium-webdriver (~> 2.0) 43 | xpath (~> 0.1.4) 44 | childprocess (0.3.5) 45 | ffi (~> 1.0, >= 1.0.6) 46 | coffee-rails (3.2.2) 47 | coffee-script (>= 2.2.0) 48 | railties (~> 3.2.0) 49 | coffee-script (2.2.0) 50 | coffee-script-source 51 | execjs 52 | coffee-script-source (1.3.3) 53 | database_cleaner (0.7.2) 54 | diff-lcs (1.1.3) 55 | erubis (2.7.0) 56 | execjs (1.4.0) 57 | multi_json (~> 1.0) 58 | factory_girl (2.6.4) 59 | activesupport (>= 2.3.9) 60 | factory_girl_rails (1.7.0) 61 | factory_girl (~> 2.6.0) 62 | railties (>= 3.0.0) 63 | ffi (1.1.5) 64 | guard (1.3.2) 65 | listen (>= 0.4.2) 66 | thor (>= 0.14.6) 67 | guard-rspec (1.2.1) 68 | guard (>= 1.1) 69 | guard-spork (0.7.1) 70 | guard (>= 0.10.0) 71 | spork (>= 0.8.4) 72 | hike (1.2.1) 73 | i18n (0.6.1) 74 | ipaddress (0.8.0) 75 | journey (1.0.4) 76 | jquery-rails (2.1.1) 77 | railties (>= 3.1.0, < 5.0) 78 | thor (~> 0.14) 79 | json (1.7.6) 80 | libwebsocket (0.1.5) 81 | addressable 82 | listen (0.5.0) 83 | mail (2.4.4) 84 | i18n (>= 0.4.0) 85 | mime-types (~> 1.16) 86 | treetop (~> 1.4.8) 87 | mime-types (1.19) 88 | multi_json (1.5.0) 89 | nokogiri (1.5.5) 90 | polyglot (0.3.3) 91 | rack (1.4.3) 92 | rack-cache (1.2) 93 | rack (>= 0.4) 94 | rack-ssl (1.3.2) 95 | rack 96 | rack-test (0.6.2) 97 | rack (>= 1.0) 98 | rails (3.2.11) 99 | actionmailer (= 3.2.11) 100 | actionpack (= 3.2.11) 101 | activerecord (= 3.2.11) 102 | activeresource (= 3.2.11) 103 | activesupport (= 3.2.11) 104 | bundler (~> 1.0) 105 | railties (= 3.2.11) 106 | railties (3.2.11) 107 | actionpack (= 3.2.11) 108 | activesupport (= 3.2.11) 109 | rack-ssl (~> 1.3.2) 110 | rake (>= 0.8.7) 111 | rdoc (~> 3.4) 112 | thor (>= 0.14.6, < 2.0) 113 | rake (10.0.3) 114 | rdoc (3.12) 115 | json (~> 1.4) 116 | rotp (1.3.3) 117 | rqrcode (0.4.2) 118 | rspec (2.11.0) 119 | rspec-core (~> 2.11.0) 120 | rspec-expectations (~> 2.11.0) 121 | rspec-mocks (~> 2.11.0) 122 | rspec-core (2.11.1) 123 | rspec-expectations (2.11.3) 124 | diff-lcs (~> 1.1.3) 125 | rspec-mocks (2.11.2) 126 | rspec-rails (2.11.0) 127 | actionpack (>= 3.0) 128 | activesupport (>= 3.0) 129 | railties (>= 3.0) 130 | rspec (~> 2.11.0) 131 | rubyzip (0.9.9) 132 | sass (3.2.1) 133 | sass-rails (3.2.5) 134 | railties (~> 3.2.0) 135 | sass (>= 3.1.10) 136 | tilt (~> 1.3) 137 | selenium-webdriver (2.25.0) 138 | childprocess (>= 0.2.5) 139 | libwebsocket (~> 0.1.3) 140 | multi_json (~> 1.0) 141 | rubyzip 142 | shoulda-matchers (1.0.0) 143 | spork (1.0.0rc3) 144 | spork-rails (3.2.0) 145 | rails (>= 3.0.0, < 3.3.0) 146 | spork (>= 1.0rc0) 147 | sprockets (2.2.2) 148 | hike (~> 1.2) 149 | multi_json (~> 1.0) 150 | rack (~> 1.0) 151 | tilt (~> 1.1, != 1.3.0) 152 | sqlite3 (1.3.6) 153 | thor (0.16.0) 154 | tilt (1.3.3) 155 | timecop (0.3.5) 156 | treetop (1.4.12) 157 | polyglot 158 | polyglot (>= 0.3.1) 159 | tzinfo (0.3.35) 160 | uglifier (1.3.0) 161 | execjs (>= 0.3.0) 162 | multi_json (~> 1.0, >= 1.0.2) 163 | uuidtools (2.1.3) 164 | xpath (0.1.4) 165 | nokogiri (~> 1.3) 166 | 167 | PLATFORMS 168 | ruby 169 | 170 | DEPENDENCIES 171 | authlogic (>= 3.1.0) 172 | capybara (~> 1.1) 173 | coffee-rails (~> 3.2.1) 174 | database_cleaner (~> 0.7.1) 175 | factory_girl_rails (~> 1.6) 176 | guard (~> 1.0) 177 | guard-rspec (>= 0.6) 178 | guard-spork (~> 0.7.1) 179 | ipaddress (~> 0.8.0) 180 | jquery-rails 181 | json 182 | rails (= 3.2.11) 183 | rotp (~> 1.3.2) 184 | rqrcode (~> 0.4.2) 185 | rspec-rails (~> 2.8) 186 | sass-rails (~> 3.2.3) 187 | shoulda-matchers (~> 1.0.0) 188 | spork-rails (~> 3.2.0) 189 | sqlite3 190 | timecop (= 0.3.5) 191 | uglifier (>= 1.0.3) 192 | uuidtools (~> 2.1.2) 193 | -------------------------------------------------------------------------------- /spec/controllers/users_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | # This spec was generated by rspec-rails when you ran the scaffold generator. 4 | # It demonstrates how one might use RSpec to specify the controller code that 5 | # was generated by Rails when you ran the scaffold generator. 6 | # 7 | # It assumes that the implementation code is generated by the rails scaffold 8 | # generator. If you are using any extension libraries to generate different 9 | # controller code, this generated spec may or may not pass. 10 | # 11 | # It only uses APIs available in rails and/or rspec-rails. There are a number 12 | # of tools you can use to make these specs even more expressive, but we're 13 | # sticking to rails and rspec-rails APIs to keep things simple and stable. 14 | # 15 | # Compared to earlier versions of this generator, there is very limited use of 16 | # stubs and message expectations in this spec. Stubs are only used when there 17 | # is no simpler way to get a handle on the object needed for the example. 18 | # Message expectations are only used when there is no simpler way to specify 19 | # that an instance is receiving a specific message. 20 | 21 | describe UsersController do 22 | render_views 23 | let(:page) { Capybara::Node::Simple.new(@response.body) } 24 | 25 | before(:each) do 26 | login_as("user") 27 | end 28 | 29 | # This should return the minimal set of attributes required to create a valid 30 | # User. As you add validations to User, be sure to 31 | # update the return value of this method accordingly. 32 | def valid_attributes 33 | { 34 | :login => 'someone', 35 | :email => 'someone@example.com', 36 | :first_name => 'someone', 37 | :last_name => 'new', 38 | :password => 'test', 39 | :password_confirmation => 'test' 40 | } 41 | end 42 | 43 | describe "GET show" do 44 | it "assigns the requested user as @user" do 45 | user = User.create! valid_attributes 46 | get :show, {:id => user.to_param} 47 | assigns(:user).should eq(user) 48 | end 49 | end 50 | 51 | describe "GET new" do 52 | it "assigns a new user as @user" do 53 | get :new, {} 54 | assigns(:user).should be_a_new(User) 55 | end 56 | end 57 | 58 | describe "GET edit" do 59 | it "assigns the requested user as @user" do 60 | user = User.create! valid_attributes 61 | get :edit, {:id => user.to_param} 62 | assigns(:user).should eq(user) 63 | end 64 | end 65 | 66 | describe "POST create" do 67 | describe "with valid params" do 68 | it "creates a new User" do 69 | expect { 70 | post :create, {:user => valid_attributes} 71 | }.to change(User, :count).by(1) 72 | end 73 | 74 | it "assigns a newly created user as @user" do 75 | post :create, {:user => valid_attributes} 76 | assigns(:user).should be_a(User) 77 | assigns(:user).should be_persisted 78 | end 79 | 80 | it "redirects to the created user" do 81 | post :create, {:user => valid_attributes} 82 | response.should redirect_to(User.last) 83 | end 84 | end 85 | 86 | describe "with invalid params" do 87 | it "assigns a newly created but unsaved user as @user" do 88 | # Trigger the behavior that occurs when invalid params are submitted 89 | User.any_instance.stub(:save).and_return(false) 90 | post :create, {:user => {}} 91 | assigns(:user).should be_a_new(User) 92 | end 93 | 94 | it "re-renders the 'new' template" do 95 | # Trigger the behavior that occurs when invalid params are submitted 96 | User.any_instance.stub(:save).and_return(false) 97 | post :create, {:user => {}} 98 | response.should render_template("new") 99 | end 100 | end 101 | end 102 | 103 | describe "PUT update" do 104 | describe "with valid params" do 105 | it "updates the requested user" do 106 | user = User.create! valid_attributes 107 | # Assuming there are no other users in the database, this 108 | # specifies that the User created on the previous line 109 | # receives the :update_attributes message with whatever params are 110 | # submitted in the request. 111 | User.any_instance.should_receive(:update_attributes).with({'these' => 'params'}) 112 | put :update, {:id => user.to_param, :user => {'these' => 'params'}} 113 | end 114 | 115 | it "assigns the requested user as @user" do 116 | user = User.create! valid_attributes 117 | put :update, {:id => user.to_param, :user => valid_attributes} 118 | assigns(:user).should eq(user) 119 | end 120 | 121 | it "redirects to the user" do 122 | user = User.create! valid_attributes 123 | put :update, {:id => user.to_param, :user => valid_attributes} 124 | response.should redirect_to(user) 125 | end 126 | end 127 | 128 | describe "with invalid params" do 129 | it "assigns the user as @user" do 130 | user = User.create! valid_attributes 131 | # Trigger the behavior that occurs when invalid params are submitted 132 | User.any_instance.stub(:save).and_return(false) 133 | put :update, {:id => user.to_param, :user => {}} 134 | assigns(:user).should eq(user) 135 | end 136 | 137 | it "re-renders the 'edit' template" do 138 | user = User.create! valid_attributes 139 | # Trigger the behavior that occurs when invalid params are submitted 140 | User.any_instance.stub(:save).and_return(false) 141 | put :update, {:id => user.to_param, :user => {}} 142 | response.should render_template("edit") 143 | end 144 | end 145 | end 146 | 147 | describe "DELETE destroy" do 148 | it "destroys the requested user" do 149 | user = User.create! valid_attributes 150 | expect { 151 | delete :destroy, {:id => user.to_param} 152 | }.to change(User, :count).by(-1) 153 | end 154 | 155 | it "redirects to the users list" do 156 | user = User.create! valid_attributes 157 | delete :destroy, {:id => user.to_param} 158 | response.should redirect_to(users_url) 159 | end 160 | end 161 | 162 | 163 | end 164 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Two Factor Authentication Example 2 | ================================= 3 | 4 | A bare bones example Rails 3.2 application and test suite demonstrating the use of the 5 | [Authlogic](https://github.com/binarylogic/authlogic) gem and custom 6 | [two-factor authentication (TFA)](http://en.wikipedia.org/wiki/Two-factor_authentication>) 7 | with [Google authenticator](http://code.google.com/p/google-authenticator/) support. 8 | 9 | 10 | Typical Use Case 11 | ----------------- 12 | 13 | Intranet application with username/password authentication for LAN users. Remote users 14 | are required to use TFA. 15 | 16 | ### Demo Features 17 | 18 | * Authlogic handles authentication, there are no changes to Authlogic, TFA is 19 | completely separate and invoked after Authlogic authorization. 20 | * Google Authenticator QR code secret entry without generating images (RQRCode) 21 | or sending secrets to the charting services. 22 | * TFA confirmation will expire with the session but can expire before the 23 | session if required. 24 | * TFA confirmation code brute force protection applies to the user, not just 25 | the session. Lockout after 5 failures, requires manual reset. 26 | 27 | ### Demo Limitations 28 | 29 | Correctable in a production application 30 | 31 | * Production implementations must use SSL otherwise this implementation, and 32 | Authlogic itself, is vulnerable to [session 33 | hijacking](http://guides.rubyonrails.org/security.html#session-hijacking). 34 | See below for configuration options. 35 | * TFA secret and TFA failure count reset is not implemented 36 | * Users can view other user secrets, they should be scoped to the current user, 37 | i.e. a 'My Account' page. 38 | * TFA Google Authenticator secret setup requires viewing the user record, the 39 | demo has all IP addresses including the localhost address restricted by 40 | default. This can be changed. See configuration options below. 41 | 42 | Example Application Usage 43 | ------------------------- 44 | 45 | git clone http://github.com/robertwahler/two_factor_authentication_example 46 | cd two_factor_authentication_example 47 | 48 | bundle install 49 | 50 | bundle exec rake db:migrate 51 | bundle exec rake db:seed 52 | 53 | bundle exec rails server 54 | 55 | login creditials for the admin user 56 | 57 | login: admin 58 | password: admin 59 | Google Authenticator time based two_factor_secret (spaces are optional): v6na sf4k fe45 qxbq 60 | 61 | run the app 62 | 63 | firefox http://localhost:3000 64 | 65 | run the RSpec test suite 66 | 67 | bundle exec rake db:test:prepare 68 | 69 | bundle exec rspec 70 | 71 | for development, start-up the Spork process via Guard 72 | 73 | bundle exec guard 74 | 75 | ### Demo Configuration Options 76 | 77 | #### TFA configuration 78 | 79 | Change TFA brute force failure count in app/models/user.rb 80 | 81 | def two_factor_failure_count_exceeded? 82 | self.two_factor_failure_count >= 5 83 | end 84 | 85 | Change length of time the TFA confirmation is valid in app/models/user.rb 86 | 87 | def two_factor_confirmed_at_valid_for 88 | 12.hours 89 | end 90 | 91 | Change the sliding window width from the default of one 30 second window in 92 | app/controllers/user_sessions_controller.rb 93 | 94 | # Use a sliding time window to validate tokens. System clock inaccuracy can 95 | # be tolerated at the expense a small decrease in security. A value of 0 96 | # will disable the sliding window 97 | # 98 | # A value of 2 will check tokens in two windows before and after the current 99 | # 30 second window. ie. +/- 60 seconds surrounding the current window. 100 | # 101 | # @return [Integer] width of the window in 30 second increments 102 | def sliding_window_width 103 | 1 104 | end 105 | 106 | #### Excluding IP Ranges from TFA 107 | 108 | Change ApplicationController to allow all logins to bypass TFA 109 | 110 | def two_factor_excluded_ip_addresses 111 | [IPAddress.parse("0.0.0.0/0")] 112 | end 113 | 114 | Change ApplicationController to allow a localhost subnet to access without TFA 115 | 116 | def two_factor_excluded_ip_addresses 117 | [IPAddress.parse("127.0.0.1/24")] 118 | end 119 | 120 | Change ApplicationController to allow LAN subnet to access without TFA 121 | 122 | def two_factor_excluded_ip_addresses 123 | [IPAddress.parse("10.0.0.1/24")] 124 | end 125 | 126 | Change ApplicationController to allow both localhost and LAN subnet to access without TFA 127 | 128 | def two_factor_excluded_ip_addresses 129 | [IPAddress.parse("127.0.0.1/24"), IPAddress.parse("10.0.0.1/24")] 130 | end 131 | 132 | ### Forcing SSL in Production 133 | 134 | config/environments/production.rb 135 | 136 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 137 | config.force_ssl = true 138 | 139 | app/models/user_session.rb 140 | 141 | # Should the cookie be set as httponly? If true, the cookie will not be 142 | # accessable from javascript 143 | httponly true 144 | 145 | # Should the cookie be set as secure? If true, the cookie will only be sent 146 | # over SSL connections 147 | # 148 | # Secure the cookie when the session_store is secure (production SSL) 149 | secure true 150 | 151 | config/initializers/session_store.rb 152 | 153 | add these options to the session_store 154 | 155 | :httponly => true, 156 | :secure => Rails.env.production? 157 | 158 | Dependencies 159 | ------------ 160 | 161 | ### Runtime 162 | 163 | * Authlogic for authentication