google-authenticator-rails 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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