google-authenticator-rails 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ # Contributing
2
+
3
+ 1. Fork it
4
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
5
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
6
+ 4. Push to the branch (`git push origin my-new-feature`)
7
+ 5. Create new Pull Request
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
- Ï# Google::Authenticator
1
+ # GoogleAuthenticatorRails
2
2
 
3
3
  [![Build Status](https://secure.travis-ci.org/jaredonline/google-authenticator.png)](http://travis-ci.org/jaredonline/google-authenticator)
4
4
 
5
- Rails (ActiveRecord) integration with the Google Authenticator apps for Android and the iPhone.
5
+ Rails (ActiveRecord) integration with the Google Authenticator apps for Android and the iPhone. Uses the Authlogic style for cookie management.
6
6
 
7
7
  ## Installation
8
8
 
@@ -28,9 +28,9 @@ acts_as_google_authenticated
28
28
  end
29
29
 
30
30
  @user = User.new
31
- @user.set_google_secret! # => true
31
+ @user.set_google_secret # => true
32
32
  @user.google_qr_uri # => http://path.to.google/qr?with=params
33
- @user.google_authenticate(123456) # => true
33
+ @user.google_authentic?(123456) # => true
34
34
  ```
35
35
 
36
36
  Google Labels
@@ -86,10 +86,76 @@ class User
86
86
  end
87
87
 
88
88
  @user = User.new
89
- @user.set_google_secret!
89
+ @user.set_google_secret
90
90
  @user.mfa_secret # => "56ahi483"
91
91
  ```
92
92
 
93
+ ## Sample Rails Setup
94
+
95
+ This is a very rough outline of how GoogleAuthenticatorRails is meant to manage the sessions and cookies for a Rails app.
96
+
97
+ ```ruby
98
+ Gemfile
99
+
100
+ gem 'rails'
101
+ gem 'google-authenticator-rails'
102
+ ```
103
+
104
+ ```ruby
105
+ app/models/users.rb
106
+
107
+ class User < ActiveRecord::Base
108
+ acts_as_google_authenticated
109
+ end
110
+ ```
111
+
112
+ If you want to authenticate based on a model called `User`, then you should name your session object `UserMfaSession`.
113
+
114
+ ```ruby
115
+ app/models/user_mfa_session.rb
116
+
117
+ class UserMfaSession < GoogleAuthenticator::Session::Base
118
+ # no real code needed here
119
+ end
120
+ ```
121
+
122
+ ```ruby
123
+ app/controllers/user_mfa_session_controller.rb
124
+
125
+ class UserMfaSessionController < ApplicationController
126
+
127
+ def new
128
+ # load your view
129
+ end
130
+
131
+ def create
132
+ user = current_user # grab your currently logged in user
133
+ if user.google_authentic?(params[:mfa_code])
134
+ UserMfaSession.create(user)
135
+ redirect_to root_path
136
+ else
137
+ flash[:error] = "Wrong code"
138
+ render :new
139
+ end
140
+ end
141
+
142
+ end
143
+ ```
144
+
145
+ ```ruby
146
+ app/controllers/application_controller.rb
147
+
148
+ class ApplicationController < ActionController::Base
149
+ before_filter :check_mfa
150
+
151
+ private
152
+ def check_mfa
153
+ if !(user_mfa_session = UserMfaSession.find) && user_mfa_session.record == current_user
154
+ redirect_to new_user_mfa_session_path
155
+ end
156
+ end
157
+ end
158
+ ```
93
159
 
94
160
  ## Contributing
95
161
 
@@ -15,10 +15,10 @@ Gem::Specification.new do |gem|
15
15
  gem.require_paths = ["lib"]
16
16
  gem.version = Google::Authenticator::Rails::VERSION
17
17
 
18
- gem.add_dependency "activesupport"
19
18
  gem.add_dependency "rotp"
20
19
  gem.add_dependency "activerecord"
21
20
  gem.add_dependency "google-qr"
21
+ gem.add_dependency "actionpack"
22
22
 
23
23
  gem.add_development_dependency "rspec", "~> 2.8.0"
24
24
  gem.add_development_dependency "sqlite3"
@@ -7,7 +7,38 @@ require 'rotp'
7
7
  require 'google-qr'
8
8
 
9
9
  # Stuff the gem is
10
+ #
11
+ GOOGLE_AUTHENTICATOR_RAILS_PATH = File.dirname(__FILE__) + "/google-authenticator-rails/"
12
+
13
+ [
14
+ "version",
15
+
16
+ "action_controller",
17
+ "active_record",
18
+ "session"
19
+ ].each do |library|
20
+ require GOOGLE_AUTHENTICATOR_RAILS_PATH + library
21
+ end
22
+
23
+ # Sets up some basic accessors for use with the ROTP module
10
24
  #
11
- require "google-authenticator-rails/version"
12
- require 'google-authenticator-rails/google'
13
- require 'google-authenticator-rails/active_record'
25
+ module GoogleAuthenticatorRails
26
+ # Drift is set to 6 because ROTP drift is not inclusive. This allows a drift of 5 seconds.
27
+ DRIFT = 6
28
+
29
+ def self.generate_password(secret, iteration)
30
+ ROTP::HOTP.new(secret).at(iteration)
31
+ end
32
+
33
+ def self.time_based_password(secret)
34
+ ROTP::TOTP.new(secret).now
35
+ end
36
+
37
+ def self.valid?(code, secret)
38
+ ROTP::TOTP.new(secret).verify_with_drift(code, DRIFT)
39
+ end
40
+
41
+ def self.generate_secret
42
+ ROTP::Base32.random_base32
43
+ end
44
+ end
@@ -0,0 +1 @@
1
+ require 'google-authenticator-rails/action_controller/rails_adapter'
@@ -0,0 +1,36 @@
1
+ module GoogleAuthenticatorRails
2
+ module ActionController
3
+ class RailsAdapter
4
+ class LoadedTooLateError < StandardError
5
+ def initialize
6
+ super("GoogleAuthenticatorRails is trying to prepend a before_filter in ActionController::Base. Because you've already defined" +
7
+ " ApplicationController, your controllers will not get this before_filter. Please load GoogleAuthenticatorRails before defining" +
8
+ " ApplicationController.")
9
+ end
10
+ end
11
+
12
+ def initialize(controller)
13
+ @controller = controller
14
+ end
15
+
16
+ def cookies
17
+ @controller.send(:cookies)
18
+ end
19
+ end
20
+
21
+ module Integration
22
+ def self.included(klass)
23
+ raise RailsAdapter::LoadedTooLateError.new if defined?(::ApplicationController)
24
+
25
+ klass.prepend_before_filter(:activate_google_authenticator_rails)
26
+ end
27
+
28
+ private
29
+ def activate_google_authenticator_rails
30
+ GoogleAuthenticatorRails::Session::Base.controller = RailsAdapter.new(self)
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ ActionController::Base.send(:include, GoogleAuthenticatorRails::ActionController::Integration)
@@ -1,66 +1,2 @@
1
- require 'google-authenticator-rails/active_record/google_authentication'
2
1
  require 'google-authenticator-rails/active_record/acts_as_google_authenticated'
3
-
4
- class ActiveRecord::Base # :nodoc:
5
-
6
- # This is the single integration point. Monkey patch ActiveRecord::Base
7
- # to include the ActsAsGoogleAuthenticated module, which allows a user
8
- # to call User.acts_as_google_authenticated.
9
- #
10
- # The model being used must have a string column named "google_secret", or an explicitly
11
- # named column.
12
- #
13
- # Example:
14
- #
15
- # class User
16
- # acts_as_google_authenticated
17
- # end
18
- #
19
- # @user = user.new
20
- # @user.set_google_secret! # => true
21
- # @user.google_qr_uri # => http://path.to.google/qr?with=params
22
- # @user.google_authenticate(123456) # => true
23
- #
24
- # Google Labels
25
- # When setting up an account with the GoogleAuthenticator you need to provide
26
- # a label for that account (to distinguish it from other accounts).
27
- #
28
- # GoogleAuthenticatorRails allows you to customize how the record will create
29
- # that label. There are three options:
30
- # - The default just uses the column "email" on the model
31
- # - You can specify a custom column with the :column_name option
32
- # - You can specify a custom method via a symbol or a proc
33
- #
34
- # Examples:
35
- #
36
- # class User
37
- # acts_as_google_authenticated :column => :user_name
38
- # end
39
- #
40
- # @user = User.new(:user_name => "ted")
41
- # @user.google_label # => "ted"
42
- #
43
- # class User
44
- # acts_as_google_authenticated :method => :user_name_with_label
45
- #
46
- # def user_name_with_label
47
- # "#{user_name}@mysweetservice.com"
48
- # end
49
- # end
50
- #
51
- # @user = User.new(:user_name => "ted")
52
- # @user.google_label # => "ted@mysweetservice.com"
53
- #
54
- # class User
55
- # acts_as_google_authenticated :method => Proc.new { |user| user.user_name_with_label.upcase }
56
- #
57
- # def user_name_with_label
58
- # "#{user_name}@mysweetservice.com"
59
- # end
60
- # end
61
- #
62
- # @user = User.new(:user_name => "ted")
63
- # @user.google_label # => "TED@MYSWEETSERVICE.COM"
64
- #
65
- include ActiveRecord::ActsAsGoogleAuthenticated
66
- end
2
+ require 'google-authenticator-rails/active_record/helpers'
@@ -1,37 +1,96 @@
1
- module ActiveRecord # :nodoc:
2
- module ActsAsGoogleAuthenticated # :nodoc:
3
- def self.included(base)
4
- base.extend ClassMethods
5
- end
1
+ module GoogleAuthenticatorRails # :nodoc:
2
+ module ActiveRecord # :nodoc:
3
+ module ActsAsGoogleAuthenticated # :nodoc:
4
+ def self.included(base)
5
+ base.extend ClassMethods
6
+ end
7
+
8
+ # This is the single integration point. Monkey patch ActiveRecord::Base
9
+ # to include the ActsAsGoogleAuthenticated module, which allows a user
10
+ # to call User.acts_as_google_authenticated.
11
+ #
12
+ # The model being used must have a string column named "google_secret", or an explicitly
13
+ # named column.
14
+ #
15
+ # Example:
16
+ #
17
+ # class User
18
+ # acts_as_google_authenticated
19
+ # end
20
+ #
21
+ # @user = user.new
22
+ # @user.set_google_secret # => true
23
+ # @user.google_qr_uri # => http://path.to.google/qr?with=params
24
+ # @user.google_authentic?(123456) # => true
25
+ #
26
+ # Google Labels
27
+ # When setting up an account with the GoogleAuthenticator you need to provide
28
+ # a label for that account (to distinguish it from other accounts).
29
+ #
30
+ # GoogleAuthenticatorRails allows you to customize how the record will create
31
+ # that label. There are three options:
32
+ # - The default just uses the column "email" on the model
33
+ # - You can specify a custom column with the :column_name option
34
+ # - You can specify a custom method via a symbol or a proc
35
+ #
36
+ # Examples:
37
+ #
38
+ # class User
39
+ # acts_as_google_authenticated :column => :user_name
40
+ # end
41
+ #
42
+ # @user = User.new(:user_name => "ted")
43
+ # @user.google_label # => "ted"
44
+ #
45
+ # class User
46
+ # acts_as_google_authenticated :method => :user_name_with_label
47
+ #
48
+ # def user_name_with_label
49
+ # "#{user_name}@example.com"
50
+ # end
51
+ # end
52
+ #
53
+ # @user = User.new(:user_name => "ted")
54
+ # @user.google_label # => "ted@example.com"
55
+ #
56
+ # class User
57
+ # acts_as_google_authenticated :method => Proc.new { |user| user.user_name_with_label.upcase }
58
+ #
59
+ # def user_name_with_label
60
+ # "#{user_name}@example.com"
61
+ # end
62
+ # end
63
+ #
64
+ # @user = User.new(:user_name => "ted")
65
+ # @user.google_label # => "TED@EXAMPLE.COM"
66
+ #
67
+ module ClassMethods # :nodoc
6
68
 
7
- module ClassMethods # :nodoc
69
+ # Initializes the class attributes with the specified options and includes the
70
+ # respective ActiveRecord helper methods
71
+ #
72
+ # Options:
73
+ # [:column_name] the name of the column used to create the google_label
74
+ # [:method] name of the method to call to create the google_label
75
+ # it supercedes :column_name
76
+ # [:google_secret_column] the column the secret will be stored in, defaults
77
+ # to "google_secret"
78
+ def acts_as_google_authenticated(options = {})
79
+ @google_label_column = options[:column_name] || :email
80
+ @google_label_method = options[:method] || :default_google_label_method
81
+ @google_secret_column = options[:google_secret_column] || :google_secret
8
82
 
9
- # Initializes the class attributes with the specified options and includes the
10
- # GoogleAuthentication module
11
- #
12
- # Options:
13
- # [:column_name] the name of the column used to create the google_label
14
- # [:method] name of the method to call to created the google_label
15
- # it supercedes :column_name
16
- # [:google_secret_column] the column the secret will be stored in, defaults
17
- # to "google_secret"
18
- # [:skip_attr_accessible] defaults to false, if set to true will no call
19
- # attr_accessible on the google_secret_column
20
- def acts_as_google_authenticated(options = {})
21
- @google_label_column = options[:column_name] || :email
22
- @google_label_method = options[:method] || :default_google_label_method
23
- @google_secret_column = options[:google_secret_column] || :google_secret
24
-
25
- attr_accessible @google_secret_column unless options[:skip_attr_accessible] == true
83
+ puts ":skip_attr_accessible is no longer required. Called from #{Kernel.caller[0]}}" if options.has_key?(:skip_attr_accessible)
26
84
 
27
- [:google_label_column, :google_label_method, :google_secret_column].each do |cattr|
28
- self.class.__send__(:define_method, cattr) do
29
- instance_variable_get("@#{cattr}")
85
+ [:google_label_column, :google_label_method, :google_secret_column].each do |cattr|
86
+ self.singleton_class.class_eval { attr_reader cattr }
30
87
  end
31
- end
32
88
 
33
- include ActiveRecord::GoogleAuthentication
89
+ include GoogleAuthenticatorRails::ActiveRecord::Helpers
90
+ end
34
91
  end
35
92
  end
36
93
  end
37
- end
94
+ end
95
+
96
+ ActiveRecord::Base.send(:include, GoogleAuthenticatorRails::ActiveRecord::ActsAsGoogleAuthenticated)
@@ -0,0 +1,51 @@
1
+ module GoogleAuthenticatorRails # :nodoc:
2
+ module ActiveRecord # :nodoc:
3
+ module Helpers
4
+ def set_google_secret
5
+ self.__send__("#{self.class.google_secret_column}=", GoogleAuthenticatorRails::generate_secret)
6
+ save
7
+ end
8
+
9
+ # TODO: Remove this method in version 0.0.4
10
+ def set_google_secret!
11
+ put "DEPRECATION WARNING: #set_google_secret! is no longer being used, use #set_google_secret instead. #set_google_secret! will be removed in 0.0.4. Called from #{Kernel.caller[0]}"
12
+ set_google_secret
13
+ end
14
+
15
+ def google_authentic?(code)
16
+ GoogleAuthenticatorRails.valid?(code, google_secret_value)
17
+ end
18
+
19
+ # TODO: Remove this method in version 0.0.4
20
+ def google_authenticate(code)
21
+ put "DEPRECATION WARNING: #google_authenticate is no longer being used, use #google_authentic? instead. #google_authenticate will be removed in 0.0.4. Called from #{Kernel.caller[0]}"
22
+ google_authentic?(code)
23
+ end
24
+
25
+ def google_qr_uri
26
+ GoogleQR.new(:data => ROTP::TOTP.new(google_secret_value).provisioning_uri(google_label), :size => "200x200").to_s
27
+ end
28
+
29
+ def google_label
30
+ method = self.class.google_label_method
31
+ case method
32
+ when Proc
33
+ method.call(self)
34
+ when Symbol, String
35
+ self.__send__(method)
36
+ else
37
+ raise NoMethodError.new("the method used to generate the google_label was never defined")
38
+ end
39
+ end
40
+
41
+ private
42
+ def default_google_label_method
43
+ self.__send__(self.class.google_label_column)
44
+ end
45
+
46
+ def google_secret_value
47
+ self.__send__(self.class.google_secret_column)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,10 @@
1
+ SESSION_GOOGLE_AUTHENTICATOR_RAILS_PATH = GOOGLE_AUTHENTICATOR_RAILS_PATH + "session/"
2
+
3
+ [
4
+ "activation",
5
+ "persistence",
6
+
7
+ "base"
8
+ ].each do |library|
9
+ require SESSION_GOOGLE_AUTHENTICATOR_RAILS_PATH + library
10
+ end
@@ -0,0 +1,51 @@
1
+ module GoogleAuthenticatorRails
2
+ module Session
3
+ module Activation
4
+ class ControllerMissingError < StandardError; end
5
+
6
+ def self.included(klass)
7
+ klass.class_eval do
8
+ extend ClassMethods
9
+ include InstanceMethods
10
+ end
11
+ end
12
+
13
+ end
14
+
15
+ module ClassMethods
16
+ # Every thread in Passenger handles only a single request at a time, but there can be many threads running.
17
+ # This ensures that when setting the current active controller
18
+ # it only gets set for the current active thread (and doesn't mess up any other threads).
19
+ #
20
+ def controller=(controller)
21
+ Thread.current[:google_authenticator_rails_controller] = controller
22
+ end
23
+
24
+ def controller
25
+ Thread.current[:google_authenticator_rails_controller]
26
+ end
27
+
28
+ # If the controller isn't set, we can't use the Sessions. They rely on the session information passed
29
+ # in from ActionController to access the cookies.
30
+ #
31
+ def activated?
32
+ !controller.nil?
33
+ end
34
+ end
35
+
36
+ module InstanceMethods
37
+ attr_reader :record
38
+
39
+ def initialize(record)
40
+ raise Activation::ControllerMissingError unless self.class.activated?
41
+
42
+ @record = record
43
+ end
44
+
45
+ private
46
+ def controller
47
+ self.class.controller
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,32 @@
1
+ module GoogleAuthenticatorRails
2
+ module Session
3
+ # This is where the heart of the session control logic works.
4
+ # GoogleAuthenticatorRails works in the same way as Authlogic. It assumes that you've created a class based on
5
+ # GoogleAuthenticatorRails::Session::Base with the name of the model you want to authenticate + "MfaSession". So if you had
6
+ #
7
+ # class User < ActiveRecord::Base
8
+ # end
9
+ #
10
+ # Your Session management class would look like
11
+ #
12
+ # class UserMfaSession < GoogleAuthenticatorRails::Session::Base
13
+ # end
14
+ #
15
+ # The Session class gets the name of the record to lookup from the name of the class.
16
+ #
17
+ # To create a new session based off our User class, you just call
18
+ #
19
+ # UserMfaSession.create(@user) # => <# UserMfaSession @record="<# User >">
20
+ #
21
+ # Then, in your controller, you can lookup that session by calling
22
+ #
23
+ # UserMfaSession.find
24
+ #
25
+ # You don't have to pass any arguments because only one session can be active at a time.
26
+ #
27
+ class Base
28
+ include Activation
29
+ include Persistence
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,75 @@
1
+ module GoogleAuthenticatorRails
2
+ module Session
3
+ module Persistence
4
+ class TokenNotFound < StandardError; end
5
+
6
+ def self.included(klass)
7
+ klass.class_eval do
8
+ extend ClassMethods
9
+ include InstanceMethods
10
+ end
11
+ end
12
+ end
13
+
14
+ module ClassMethods
15
+ def find
16
+ cookie = controller.cookies[cookie_key]
17
+ if cookie
18
+ token, user_id = parse_cookie(cookie).values_at(:token, :user_id)
19
+ conditions = { :persistence_token => token, :id => user_id }
20
+ record = __send__(finder, conditions).first
21
+ session = new(record)
22
+ session.valid? ? session : nil
23
+ else
24
+ nil
25
+ end
26
+ end
27
+
28
+ def create(user)
29
+ raise GoogleAuthenticatorRails::Session::Persistence::TokenNotFound if !user.respond_to?(:persistence_token) || user.persistence_token.blank?
30
+ controller.cookies[cookie_key] = create_cookie(user.persistence_token, user.id)
31
+ new(user)
32
+ end
33
+
34
+ private
35
+ def finder
36
+ @_finder ||= klass.public_methods.include?(:where) ? :rails_3_finder : :rails_2_finder
37
+ end
38
+
39
+ def rails_3_finder(conditions)
40
+ klass.where(conditions)
41
+ end
42
+
43
+ def rails_2_finder(conditions)
44
+ klass.scoped(:conditions => conditions)
45
+ end
46
+
47
+ def klass
48
+ @_klass ||= "#{self.to_s.sub("MfaSession", "")}".constantize
49
+ end
50
+
51
+ def parse_cookie(cookie)
52
+ token, user_id = cookie.split('::')
53
+ { :token => token, :user_id => user_id }
54
+ end
55
+
56
+ def create_cookie(token, user_id)
57
+ value = [token, user_id].join('::')
58
+ {
59
+ :value => value,
60
+ :expires => 24.hours.from_now
61
+ }
62
+ end
63
+
64
+ def cookie_key
65
+ "#{klass.to_s.downcase}_mfa_credentials"
66
+ end
67
+ end
68
+
69
+ module InstanceMethods
70
+ def valid?
71
+ !record.nil?
72
+ end
73
+ end
74
+ end
75
+ end
@@ -1,7 +1,7 @@
1
1
  module Google
2
2
  module Authenticator
3
3
  module Rails
4
- VERSION = "0.0.2"
4
+ VERSION = "0.0.3"
5
5
  end
6
6
  end
7
7
  end
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+
3
+ describe GoogleAuthenticatorRails::ActionController::Integration do
4
+ describe '::included' do
5
+ context 'ApplicationController already defined' do
6
+ before { class ApplicationController < MockController; end }
7
+ after { Object.send(:remove_const, :ApplicationController) }
8
+ subject { lambda { MockController.send(:include, GoogleAuthenticatorRails::ActionController::Integration) } }
9
+
10
+ it { should raise_error(GoogleAuthenticatorRails::ActionController::RailsAdapter::LoadedTooLateError) }
11
+ end
12
+
13
+ it 'should add the before filter' do
14
+ MockController.should_receive(:prepend_before_filter).with(:activate_google_authenticator_rails)
15
+ MockController.send(:include, GoogleAuthenticatorRails::ActionController::Integration)
16
+ end
17
+ end
18
+
19
+ describe '::activate_google_authenticator_rails' do
20
+ let(:controller) { MockController.new }
21
+
22
+ before do
23
+ MockController.send(:include, GoogleAuthenticatorRails::ActionController::Integration)
24
+ controller.send(:activate_google_authenticator_rails)
25
+ end
26
+
27
+ specify { GoogleAuthenticatorRails::Session::Base.controller.should be_a GoogleAuthenticatorRails::ActionController::RailsAdapter }
28
+ end
29
+ end
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+
3
+ describe GoogleAuthenticatorRails::ActionController::RailsAdapter do
4
+ describe '#cookies' do
5
+ let(:controller) { MockController.new }
6
+ let(:adapter) { GoogleAuthenticatorRails::ActionController::RailsAdapter.new(controller) }
7
+
8
+ after { adapter.cookies }
9
+ specify { controller.should_receive(:cookies) }
10
+ end
11
+ end
@@ -1,94 +1,81 @@
1
1
  require 'spec_helper'
2
2
 
3
- class User < ActiveRecord::Base
4
- attr_accessible :email, :user_name
5
-
6
- acts_as_google_authenticated
7
- end
8
-
9
- class CustomUser < ActiveRecord::Base
10
- attr_accessible :email, :user_name
11
-
12
- acts_as_google_authenticated :google_secret_column => :mfa_secret
13
- end
14
-
15
- describe Google::Authenticator::Rails do
3
+ describe GoogleAuthenticatorRails do
4
+ let(:random32) { "5qlcip7azyjuwm36" }
16
5
  before do
17
- ROTP::Base32.stub!(:random_base32).and_return("5qlcip7azyjuwm36")
18
- end
19
-
20
- it 'implements counter based passwords' do
21
- Google::Authenticator::Rails::generate_password("test", 1).should == 812658
22
- Google::Authenticator::Rails::generate_password("test", 2).should == 73348
6
+ ROTP::Base32.stub!(:random_base32).and_return(random32)
23
7
  end
24
8
 
25
- it 'implements time based password' do
26
- time = Time.parse("2012-08-07 11:11:11 AM +0700")
27
- Time.stub!(:now).and_return(time)
28
- Google::Authenticator::Rails::time_based_password("test").should == 472374
9
+ describe '#generate_password' do
10
+ subject { GoogleAuthenticatorRails::generate_password("test", counter) }
11
+
12
+ context 'counter = 1' do
13
+ let(:counter) { 1 }
14
+ it { should == 812658 }
15
+ end
16
+
17
+ context 'counter = 2' do
18
+ let(:counter) { 2 }
19
+ it { should == 73348 }
20
+ end
29
21
  end
30
22
 
31
- it 'can validate a code' do
32
- time = Time.parse("2012-08-07 11:11:11 AM +0700")
33
- Time.stub!(:now).and_return(time)
34
- Google::Authenticator::Rails::valid?(472374, "test").should be_true
23
+ context 'time-based passwords' do
24
+ let(:time) { Time.parse("2012-08-07 11:11:11 AM +0700") }
25
+ let(:secret) { "test" }
26
+ let(:code) { 472374 }
27
+ before { Time.stub!(:now).and_return(time) }
28
+
29
+ specify { GoogleAuthenticatorRails::time_based_password(secret).should == code }
30
+ specify { GoogleAuthenticatorRails::valid?(code, secret).should be true }
31
+
32
+ specify { GoogleAuthenticatorRails::valid?(code * 2, secret).should be false }
33
+ specify { GoogleAuthenticatorRails::valid?(code, secret * 2).should be false }
35
34
  end
36
35
 
37
36
  it 'can create a secret' do
38
- Google::Authenticator::Rails::generate_secret.should == "5qlcip7azyjuwm36"
37
+ GoogleAuthenticatorRails::generate_secret.should == random32
39
38
  end
40
39
 
41
40
  context 'integration with ActiveRecord' do
42
-
41
+ let(:original_time) { Time.parse("2012-08-07 11:11:00 AM +0700") }
42
+ let(:time) { original_time }
43
43
  before do
44
- time = Time.parse("2012-08-07 11:11:00 AM +0700")
45
44
  Time.stub!(:now).and_return(time)
46
45
  @user = User.create(:email => "test@example.com", :user_name => "test_user")
47
46
  @user.google_secret = "test"
48
47
  end
49
48
 
50
- it 'validates codes' do
51
- @user.google_authenticate(472374).should be_true
52
- end
53
-
54
- it 'validates with 5 seconds of drift' do
55
- time = Time.parse("2012-08-07 11:11:34 AM +0700")
56
- Time.stub!(:now).and_return(time)
57
- @user.google_authenticate(472374).should be_true
58
- end
59
-
60
- it 'does not validate with 6 seconds of drift' do
61
- time = Time.parse("2012-08-07 11:11:36 AM +0700")
62
- Time.stub!(:now).and_return(time)
63
- @user.google_authenticate(472374).should be_false
64
- end
65
-
66
- it 'creates a secret' do
67
- @user.set_google_secret!
68
- @user.google_secret.should == "5qlcip7azyjuwm36"
69
- end
70
-
71
- context 'skip_attr_accessible' do
72
- it 'respects the :skip_attr_accessible flag' do
73
- User.should_not_receive(:attr_accessible).with(:google_secret)
74
- User.acts_as_google_authenticated :skip_attr_accessible => true
49
+ context 'code validation' do
50
+ subject { @user.google_authentic?(472374) }
51
+
52
+ it { should be true }
53
+
54
+ context 'within 5 seconds of drift' do
55
+ let(:time) { original_time + 34.seconds }
56
+ it { should be true }
75
57
  end
76
58
 
77
- it 'respects the default' do
78
- User.should_receive(:attr_accessible).with(:google_secret)
79
- User.acts_as_google_authenticated
59
+ context '6 seconds of drift' do
60
+ let(:time) { original_time + 36.seconds }
61
+ it { should be false }
80
62
  end
81
63
  end
64
+
65
+ it 'creates a secret' do
66
+ @user.set_google_secret
67
+ @user.google_secret.should == random32
68
+ end
82
69
 
83
70
  context 'secret column' do
84
71
  before do
85
- Google::Authenticator::Rails.stub!(:generate_secret).and_return("test")
72
+ GoogleAuthenticatorRails.stub!(:generate_secret).and_return("test")
86
73
  @user = CustomUser.create(:email => "test@example.com", :user_name => "test_user")
87
- @user.set_google_secret!
74
+ @user.set_google_secret
88
75
  end
89
76
 
90
77
  it 'validates code' do
91
- @user.google_authenticate(472374).should be_true
78
+ @user.google_authentic?(472374).should be_true
92
79
  end
93
80
 
94
81
  it 'generates a url for a qr code' do
@@ -96,37 +83,39 @@ describe Google::Authenticator::Rails do
96
83
  end
97
84
  end
98
85
 
86
+ context 'google label' do
87
+ let(:user) { NilMethodUser.create(:email => "test@example.com", :user_name => "test_user") }
88
+ subject { lambda { user.google_label } }
89
+ it { should raise_error(NoMethodError) }
90
+ end
91
+
99
92
  context 'qr codes' do
93
+ let(:options) { { :email => "test@example.com", :user_name => "test_user" } }
94
+ let(:user) { User.create options }
95
+ before { user.set_google_secret }
96
+ subject { user.google_qr_uri }
97
+
98
+ it { should eq "https://chart.googleapis.com/chart?cht=qr&chl=otpauth%3A%2F%2Ftotp%2Ftest%40example.com%3Fsecret%3D5qlcip7azyjuwm36&chs=200x200" }
100
99
 
101
- it 'generates a url for a qr code' do
102
- @user.set_google_secret!
103
- @user.google_qr_uri.should == "https://chart.googleapis.com/chart?cht=qr&chl=otpauth%3A%2F%2Ftotp%2Ftest%40example.com%3Fsecret%3D5qlcip7azyjuwm36&chs=200x200"
104
- end
105
-
106
- it 'can generate off any column' do
107
- @user.class.acts_as_google_authenticated :column_name => :user_name
108
- @user.set_google_secret!
109
- @user.google_qr_uri.should == "https://chart.googleapis.com/chart?cht=qr&chl=otpauth%3A%2F%2Ftotp%2Ftest_user%3Fsecret%3D5qlcip7azyjuwm36&chs=200x200"
110
- end
111
-
112
- it 'can generate with a custom proc' do
113
- @user.class.acts_as_google_authenticated :method => Proc.new { |user| "#{user.user_name}@futureadvisor-admin" }
114
- @user.set_google_secret!
115
- @user.google_qr_uri.should == "https://chart.googleapis.com/chart?cht=qr&chl=otpauth%3A%2F%2Ftotp%2Ftest_user%40futureadvisor-admin%3Fsecret%3D5qlcip7azyjuwm36&chs=200x200"
100
+ context 'custom column name' do
101
+ let(:user) { ColumnNameUser.create options }
102
+ it { should eq "https://chart.googleapis.com/chart?cht=qr&chl=otpauth%3A%2F%2Ftotp%2Ftest_user%3Fsecret%3D5qlcip7azyjuwm36&chs=200x200" }
116
103
  end
117
104
 
118
- it 'can generate with a method symbol' do
119
- @user.class.acts_as_google_authenticated :method => :email
120
- @user.set_google_secret!
121
- @user.google_qr_uri.should == "https://chart.googleapis.com/chart?cht=qr&chl=otpauth%3A%2F%2Ftotp%2Ftest%40example.com%3Fsecret%3D5qlcip7azyjuwm36&chs=200x200"
105
+ context 'custom proc' do
106
+ let(:user) { ProcUser.create options }
107
+ it { should eq "https://chart.googleapis.com/chart?cht=qr&chl=otpauth%3A%2F%2Ftotp%2Ftest_user%40futureadvisor-admin%3Fsecret%3D5qlcip7azyjuwm36&chs=200x200" }
122
108
  end
123
-
124
- it 'can generate with a method string' do
125
- @user.class.acts_as_google_authenticated :method => "email"
126
- @user.set_google_secret!
127
- @user.google_qr_uri.should == "https://chart.googleapis.com/chart?cht=qr&chl=otpauth%3A%2F%2Ftotp%2Ftest%40example.com%3Fsecret%3D5qlcip7azyjuwm36&chs=200x200"
109
+
110
+ context 'method defined by symbol' do
111
+ let(:user) { SymbolUser.create options }
112
+ it { should eq "https://chart.googleapis.com/chart?cht=qr&chl=otpauth%3A%2F%2Ftotp%2Ftest%40example.com%3Fsecret%3D5qlcip7azyjuwm36&chs=200x200" }
128
113
  end
129
-
114
+
115
+ context 'method defined by string' do
116
+ let(:user) { StringUser.create options }
117
+ it { should eq "https://chart.googleapis.com/chart?cht=qr&chl=otpauth%3A%2F%2Ftotp%2Ftest%40example.com%3Fsecret%3D5qlcip7azyjuwm36&chs=200x200" }
118
+ end
130
119
  end
131
120
 
132
121
  end
@@ -0,0 +1,51 @@
1
+ require 'spec_helper'
2
+
3
+ describe GoogleAuthenticatorRails::Session::Base do
4
+ describe 'ClassMethods' do
5
+ context 'thread safety' do
6
+ let(:thread_count) { 100 }
7
+ let(:controllers) { thread_count.times.map { MockController.new } }
8
+ let(:threads) do
9
+ controllers.map do |controller|
10
+ Thread.new do
11
+ GoogleAuthenticatorRails::Session::Base.controller = controller
12
+ Thread.current[:test_case_controller] = GoogleAuthenticatorRails::Session::Base.controller
13
+ end
14
+ end
15
+ end
16
+
17
+ before do
18
+ GoogleAuthenticatorRails::Session::Base.controller = nil
19
+ sleep(0.01) while threads.any?(&:status)
20
+ end
21
+
22
+ specify { GoogleAuthenticatorRails::Session::Base.controller.should be_nil }
23
+ specify { threads.map { |thread| thread[:test_case_controller].object_id }.should eq controllers.map(&:object_id) }
24
+ end
25
+
26
+ describe '::activated?' do
27
+ subject { GoogleAuthenticatorRails::Session::Base.activated? }
28
+ before { GoogleAuthenticatorRails::Session::Base.controller = controller }
29
+
30
+ context 'controller present' do
31
+ let(:controller) { MockController.new }
32
+ it { should be true }
33
+ end
34
+
35
+ context 'controller missing' do
36
+ let(:controller) { nil }
37
+ it { should be false }
38
+ end
39
+ end
40
+ end
41
+
42
+ describe 'InstanceMethods' do
43
+ describe '#initialize' do
44
+ context 'controller missing' do
45
+ before { GoogleAuthenticatorRails::Session::Base.controller = nil }
46
+ subject { lambda { GoogleAuthenticatorRails::Session::Base.new(nil) } }
47
+ it { should raise_error(GoogleAuthenticatorRails::Session::Activation::ControllerMissingError) }
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,58 @@
1
+ require 'spec_helper'
2
+
3
+ describe GoogleAuthenticatorRails::Session::Base do
4
+ let(:controller) { MockController.new }
5
+ let(:user) { User.create(:password => "password", :email => "email@example.com") }
6
+
7
+ # Instantiate the controller so it activates UserSession
8
+ before { controller.send(:activate_google_authenticator_rails) }
9
+
10
+ describe 'ClassMethods' do
11
+ describe '::find' do
12
+ subject { UserMfaSession.find }
13
+
14
+ context 'no session' do
15
+ it { should be nil }
16
+ end
17
+
18
+ context 'session' do
19
+ before { set_cookie_for(user) }
20
+ after { clear_cookie }
21
+
22
+ it { should be_a UserMfaSession }
23
+ its(:record) { should eq user }
24
+ end
25
+ end
26
+
27
+ describe '::create' do
28
+ after { clear_cookie }
29
+ subject { UserMfaSession.create(user) }
30
+
31
+ it { should be_a UserMfaSession }
32
+ its(:record) { should eq user }
33
+
34
+ context 'nil user' do
35
+ let(:user) { nil }
36
+ subject { lambda { UserMfaSession.create(user) } }
37
+ it { should raise_error(GoogleAuthenticatorRails::Session::Persistence::TokenNotFound) }
38
+ end
39
+ end
40
+ end
41
+
42
+ describe 'InstanceMethods' do
43
+ describe '#valid?' do
44
+ subject { UserMfaSession.create(user) }
45
+ context 'user object' do
46
+ it { should be_valid }
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ def set_cookie_for(user)
53
+ controller.cookies[UserMfaSession.__send__(:cookie_key)] = { :value => [user.persistence_token, user.id].join('::'), :expires => nil }
54
+ end
55
+
56
+ def clear_cookie
57
+ controller.cookies[UserMfaSession.__send__(:cookie_key)] = nil
58
+ end
@@ -1,8 +1,46 @@
1
- require 'google-authenticator-rails'
2
1
  require 'time'
3
2
  require 'active_record'
3
+ require 'action_controller'
4
4
  require 'rotp'
5
5
 
6
+ require 'google-authenticator-rails'
7
+
8
+ class MockController
9
+ class << self
10
+ attr_accessor :callbacks
11
+
12
+ def prepend_before_filter(filter)
13
+ self.callbacks ||= []
14
+ self.callbacks = [filter] + self.callbacks
15
+ end
16
+ end
17
+
18
+ include GoogleAuthenticatorRails::ActionController::Integration
19
+
20
+ attr_accessor :cookies
21
+
22
+ def initialize
23
+ @cookies = MockCookieJar.new
24
+ end
25
+ end
26
+
27
+ class MockCookieJar < Hash
28
+ def [](key)
29
+ hash = super
30
+ hash && hash[:value]
31
+ end
32
+
33
+ def cookie_domain
34
+ nil
35
+ end
36
+
37
+ def delete(key, options = {})
38
+ super(key)
39
+ end
40
+ end
41
+
42
+ class UserMfaSession < GoogleAuthenticatorRails::Session::Base; end
43
+
6
44
  ActiveRecord::Base.establish_connection(
7
45
  :adapter => 'sqlite3',
8
46
  :database => ':memory:'
@@ -15,6 +53,9 @@ ActiveRecord::Schema.define do
15
53
  t.string :google_secret
16
54
  t.string :email
17
55
  t.string :user_name
56
+ t.string :password
57
+ t.string :persistence_token
58
+
18
59
  t.timestamps
19
60
  end
20
61
 
@@ -22,6 +63,46 @@ ActiveRecord::Schema.define do
22
63
  t.string :mfa_secret
23
64
  t.string :email
24
65
  t.string :user_name
66
+ t.string :persistence_token
67
+
25
68
  t.timestamps
26
69
  end
27
- end
70
+ end
71
+
72
+ class BaseUser < ActiveRecord::Base
73
+ attr_accessible :email, :user_name
74
+ self.table_name = "users"
75
+
76
+ before_save do |user|
77
+ user.persistence_token ||= "token"
78
+ end
79
+ end
80
+
81
+ class User < BaseUser
82
+ acts_as_google_authenticated
83
+ end
84
+
85
+ class CustomUser < BaseUser
86
+ self.table_name = "custom_users"
87
+ acts_as_google_authenticated :google_secret_column => :mfa_secret
88
+ end
89
+
90
+ class NilMethodUser < BaseUser
91
+ acts_as_google_authenticated :method => true
92
+ end
93
+
94
+ class ColumnNameUser < BaseUser
95
+ acts_as_google_authenticated :column_name => :user_name
96
+ end
97
+
98
+ class ProcUser < BaseUser
99
+ acts_as_google_authenticated :method => Proc.new { |user| "#{user.user_name}@futureadvisor-admin" }
100
+ end
101
+
102
+ class SymbolUser < BaseUser
103
+ acts_as_google_authenticated :method => :email
104
+ end
105
+
106
+ class StringUser < BaseUser
107
+ acts_as_google_authenticated :method => "email"
108
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: google-authenticator-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,10 +9,10 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-08-30 00:00:00.000000000 Z
12
+ date: 2012-09-20 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
- name: activesupport
15
+ name: rotp
16
16
  requirement: !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
@@ -28,7 +28,7 @@ dependencies:
28
28
  - !ruby/object:Gem::Version
29
29
  version: '0'
30
30
  - !ruby/object:Gem::Dependency
31
- name: rotp
31
+ name: activerecord
32
32
  requirement: !ruby/object:Gem::Requirement
33
33
  none: false
34
34
  requirements:
@@ -44,7 +44,7 @@ dependencies:
44
44
  - !ruby/object:Gem::Version
45
45
  version: '0'
46
46
  - !ruby/object:Gem::Dependency
47
- name: activerecord
47
+ name: google-qr
48
48
  requirement: !ruby/object:Gem::Requirement
49
49
  none: false
50
50
  requirements:
@@ -60,7 +60,7 @@ dependencies:
60
60
  - !ruby/object:Gem::Version
61
61
  version: '0'
62
62
  - !ruby/object:Gem::Dependency
63
- name: google-qr
63
+ name: actionpack
64
64
  requirement: !ruby/object:Gem::Requirement
65
65
  none: false
66
66
  requirements:
@@ -117,20 +117,28 @@ files:
117
117
  - .gitignore
118
118
  - .rspec
119
119
  - .travis.yml
120
+ - CONTRIBUTE.rb
120
121
  - Gemfile
121
122
  - LICENSE
122
123
  - README.md
123
124
  - Rakefile
124
125
  - google-authenticator.gemspec
125
126
  - lib/google-authenticator-rails.rb
127
+ - lib/google-authenticator-rails/action_controller.rb
128
+ - lib/google-authenticator-rails/action_controller/rails_adapter.rb
126
129
  - lib/google-authenticator-rails/active_record.rb
127
130
  - lib/google-authenticator-rails/active_record/acts_as_google_authenticated.rb
128
- - lib/google-authenticator-rails/active_record/google_authentication.rb
129
- - lib/google-authenticator-rails/google.rb
130
- - lib/google-authenticator-rails/google/rails.rb
131
- - lib/google-authenticator-rails/google/rails/rotp_integration.rb
131
+ - lib/google-authenticator-rails/active_record/helpers.rb
132
+ - lib/google-authenticator-rails/session.rb
133
+ - lib/google-authenticator-rails/session/activation.rb
134
+ - lib/google-authenticator-rails/session/base.rb
135
+ - lib/google-authenticator-rails/session/persistence.rb
132
136
  - lib/google-authenticator-rails/version.rb
137
+ - spec/action_controller/integration_spec.rb
138
+ - spec/action_controller/rails_adapter_spec.rb
133
139
  - spec/google_authenticator_spec.rb
140
+ - spec/session/activation_spec.rb
141
+ - spec/session/persistance_spec.rb
134
142
  - spec/spec_helper.rb
135
143
  homepage: http://github.com/jaredonline/google-authenticator
136
144
  licenses: []
@@ -157,5 +165,9 @@ signing_key:
157
165
  specification_version: 3
158
166
  summary: Add the ability to use the Google Authenticator with ActiveRecord.
159
167
  test_files:
168
+ - spec/action_controller/integration_spec.rb
169
+ - spec/action_controller/rails_adapter_spec.rb
160
170
  - spec/google_authenticator_spec.rb
171
+ - spec/session/activation_spec.rb
172
+ - spec/session/persistance_spec.rb
161
173
  - spec/spec_helper.rb
@@ -1,34 +0,0 @@
1
- module ActiveRecord # :nodoc:
2
- module GoogleAuthentication # :nodoc:
3
- def set_google_secret!
4
- update_attributes("#{self.class.google_secret_column}" => Google::Authenticator::Rails::generate_secret)
5
- end
6
-
7
- def google_authenticate(code)
8
- Google::Authenticator::Rails.valid?(code, google_secret_value)
9
- end
10
-
11
- def google_qr_uri
12
- GoogleQR.new(:data => ROTP::TOTP.new(google_secret_value).provisioning_uri(google_label), :size => "200x200").to_s
13
- end
14
-
15
- def google_label
16
- method = self.class.google_label_method
17
- case method
18
- when Proc
19
- method.call(self)
20
- when Symbol, String
21
- self.__send__(method)
22
- end
23
- end
24
-
25
- private
26
- def default_google_label_method
27
- self.__send__(self.class.google_label_column)
28
- end
29
-
30
- def google_secret_value
31
- self.__send__(self.class.google_secret_column)
32
- end
33
- end
34
- end
@@ -1 +0,0 @@
1
- require 'google-authenticator-rails/google/rails'
@@ -1 +0,0 @@
1
- require 'google-authenticator-rails/google/rails/rotp_integration'
@@ -1,26 +0,0 @@
1
- # Sets up some basic accessors for use with the ROTP module
2
- #
3
- module Google
4
- module Authenticator # :nodoc:
5
- module Rails # :nodoc:
6
- # Drift is set to 6 because ROTP drift is not inclusive. This allows a drift of 5 seconds.
7
- DRIFT = 6
8
-
9
- def self.generate_password(secret, iteration)
10
- ROTP::HOTP.new(secret).at(iteration)
11
- end
12
-
13
- def self.time_based_password(secret)
14
- ROTP::TOTP.new(secret).now
15
- end
16
-
17
- def self.valid?(code, secret)
18
- ROTP::TOTP.new(secret).verify_with_drift(code, DRIFT)
19
- end
20
-
21
- def self.generate_secret
22
- ROTP::Base32.random_base32
23
- end
24
- end
25
- end
26
- end