devise_gauth 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (27) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +144 -0
  4. data/app/controllers/devise/checkga_controller.rb +57 -0
  5. data/app/controllers/devise/displayqr_controller.rb +82 -0
  6. data/app/views/devise/checkga/show.html.erb +7 -0
  7. data/app/views/devise/displayqr/show.html.erb +20 -0
  8. data/config/locales/en.yml +21 -0
  9. data/lib/devise_google_authenticatable/controllers/helpers.rb +72 -0
  10. data/lib/devise_google_authenticatable/hooks/totp_authenticatable.rb +17 -0
  11. data/lib/devise_google_authenticatable/models/google_authenticatable.rb +102 -0
  12. data/lib/devise_google_authenticatable/orm/active_record.rb +23 -0
  13. data/lib/devise_google_authenticatable/patches/display_qr.rb +43 -0
  14. data/lib/devise_google_authenticatable/patches.rb +13 -0
  15. data/lib/devise_google_authenticatable/rails.rb +21 -0
  16. data/lib/devise_google_authenticatable/routes.rb +18 -0
  17. data/lib/devise_google_authenticatable/schema.rb +39 -0
  18. data/lib/devise_google_authenticatable/version.rb +5 -0
  19. data/lib/devise_google_authenticatable/views/helpers.rb +52 -0
  20. data/lib/devise_google_authenticator.rb +44 -0
  21. data/lib/generators/active_record/devise_google_authenticator_generator.rb +15 -0
  22. data/lib/generators/active_record/templates/migration.rb +19 -0
  23. data/lib/generators/devise_google_authenticator/devise_google_authenticator_generator.rb +37 -0
  24. data/lib/generators/devise_google_authenticator/install_generator.rb +34 -0
  25. data/lib/generators/devise_google_authenticator/views_generator.rb +21 -0
  26. data/lib/generators/mongoid/devise_google_authenticator_generator.rb +28 -0
  27. metadata +155 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e9a3c30a2e628f5e3fe99a0c4dedaf30f0dcbeced3c6a52a199fa150dd393b4b
4
+ data.tar.gz: 3259e009ed15a53e1e154451ad95963e862726f10f3966d46a9f910dfab80fa8
5
+ SHA512:
6
+ metadata.gz: bc4bacfe402e8e98667326ec1c73cca6f2c05c545a7e5b450dc3c50d917ef90565aeb1942de381addd3782f83747d324f796148800f1770bc537f2b6af84ce82
7
+ data.tar.gz: aba6c26a7a3e7d667425a9aac361ae1a1dd7e60e76600f6b2ebc55f58fd904f98a654caa80528382d507305eae4cf54775bf210361d7e35654f3c719c0a5539d
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Pharmony SA
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,144 @@
1
+ # Devise Google Authenticator
2
+
3
+ This is a fork of the [devise](https://github.com/plataformatec/devise) extension to allow your app to utilise [Google Authenticator](http://code.google.com/p/google-authenticator/) for Time-based One Time Passwords (TOTP).
4
+
5
+ The current version of this gem support Rails 4.x, 5.x and 6.x.
6
+
7
+ ## Installation
8
+
9
+ Add the gem to your Gemfile (don't forget devise too):
10
+
11
+ ```ruby
12
+ gem 'devise_gauth', '0.3.16'
13
+ ```
14
+
15
+ Don't forget to "bundle install"
16
+
17
+ Before you can setup Devise Google Authenticator you need to [setup Devise first](https://github.com/heartcombo/devise#getting-started).
18
+
19
+ ### Automatic Installation (Lets assume this is a bare bones app)
20
+
21
+ Run the following generator to add the necessary configuration options to Devise's config file:
22
+
23
+ ```bash
24
+ rails g devise_google_authenticator:install
25
+ ```
26
+
27
+ After you've created your Devise user models (which is usually done with a "rails g devise MODEL"), set up your Google Authenticator additions:
28
+
29
+ ```bash
30
+ rails g devise_google_authenticator MODEL
31
+ ```
32
+
33
+ Don't forget to migrate if you're NOT using Mongoid as your database ORM, Mongoid installations will have appropriate fields added to the model after the command above:
34
+
35
+ ```bash
36
+ rake db:migrate
37
+ ```
38
+
39
+ ### Installation With Existing Users
40
+
41
+ After the above steps have been performed, you'll need to generate secrets for each user:
42
+
43
+ ```ruby
44
+ User.where(gauth_secret: nil).find_each do |user|
45
+ user.send(:assign_auth_secret)
46
+ user.save!
47
+ end
48
+ ```
49
+
50
+ By default, users won't need to perform two-factor authentication (gauth_enabled='0'). By visiting /MODEL/displayqr (eg: /users/displayqr)
51
+ and submitting the form, two-factor authentication will then be turned on (gauth_enabled=1) and required for subsequent logins.
52
+
53
+ ## Configuration Options
54
+
55
+ The install generator adds some options to the end of your Devise config file (config/initializers/devise.rb)
56
+
57
+ * `config.ga_timeout` - how long should the user be able to authenticate with their Google Authenticator token
58
+ * `config.ga_timedrift` - a multiplier which provides for drift between a user's clock (and therefore their OTP) and the system clock. This should be fine at 3.
59
+ * `config.ga_remembertime` - how long to remember the token for before requiring another. By default this is 1 month. To disable this setting change it to nil.
60
+ * `config.ga_appname` - If you want to set a custom application name instead of using the name of the Rails app.
61
+ * `config.ga_bypass_signup` - If you don't want to immediately forward newly registered or signed-up users to the Display QR page. If this is enabled, users will have to visit the /displayqr page to enable Google Authenticator.
62
+
63
+ ## Custom Views
64
+
65
+ If you want to customise your views (which you likely will want to, as they're pretty ugly right now), you can use the generator:
66
+
67
+ ```bash
68
+ rails g devise_google_authenticator:views
69
+ ```
70
+
71
+ ## Usage
72
+
73
+ With this extension enabled, the following is expected behaviour:
74
+
75
+ * When a user registers, they are forwarded onto the Display QR page (unless ga_bypass_signup is set to true). This allows them to add their new "token" to their mobile device, and enable, or disable, the functionality. To enable/disable the functionality, the user has to enter the current token.
76
+ * If users can't self-register, they're still able to visit this page by visiting /MODEL/displayqr (eg: /users/displayqr).
77
+ * If the function is enabled (for that user), when they sign in, they'll be prompted for their password (as per normal), but then redirected into the Check QR page. They have to enter their token (from their device) to then successfully authenticate.
78
+ * If configured (by default to 1 month), the user will only be prompted for the token every 1 month.
79
+
80
+ ## I18n
81
+
82
+ The install generator also installs an english copy of a Devise Google Authenticator i18n file. This can be modified (or used to create other language versions) and is located at: config/locales/devise.google_authenticator.en.yml
83
+
84
+ ## Testing
85
+
86
+ You can use [Docker](https://www.docker.com/) to build & run tests. To make it
87
+ even more easier this project uses [Earthly](https://earthly.dev/) so that you
88
+ can build the project's docker image with:
89
+
90
+ ```
91
+ earthly +dev
92
+ ```
93
+
94
+ Then you can run the tests using [Docker Compose](https://docs.docker.com/compose/):
95
+
96
+ ```
97
+ docker compose run --rm gem
98
+ ```
99
+
100
+ Or you can run the tests like on the CI using Earthly
101
+ (This reduces random failures between the CI and you environment):
102
+
103
+ ```
104
+ earthly --allow-privileged +test
105
+ ```
106
+
107
+ And in the case you'd like to test a different version of Ruby/Rails/Devise:
108
+
109
+ ```
110
+ earthly --allow-privileged +test --EARTHLY_RUBY_VERSION=2.5 --EARTHLY_RAILS_VERSION=5.2.8.1 --EARTHLY_DEVISE_VERSION=4.8.1
111
+ ```
112
+
113
+ ## Thanks (and unknown contributors)
114
+
115
+ This extension would not exist without the following other projects and associated authors (Whom I have turned to for inspiration and definitely have helped contributing by providing awesome Devise extensions. A lot of this code has been refactored from various sources, in particular these - in particular Sergio and Devise_invitable for his excellent unit test code):
116
+
117
+ * Devise (José Valim, Carlos Antônio da Silva, Rodrigo Flores) https://github.com/plataformatec/devise
118
+ * Devise_invitable (Sergio Cambra) https://github.com/scambra/devise_invitable
119
+ * Devise_openid_authenticatable (Nat Budin) https://github.com/nbudin/devise_openid_authenticatable
120
+ * Devise_security_extension (Team Phatworx, Marco Scholl, Alexander Dreher) https://github.com/phatworx/devise_security_extension
121
+ * Ronald Arias https://github.com/ronald05arias
122
+ * Sunny Ng https://github.com/blahblahblah-
123
+ * Michael Guymon https://github.com/mguymon
124
+ * Mikkel Garcia https://github.com/mikkel
125
+ * Ricky Reusser https://github.com/rreusser
126
+ * Felipe Lima https://github.com/felipecsl
127
+ * Sylvain Utard https://github.com/redox
128
+
129
+
130
+ ## Contributing to devise_gauth
131
+
132
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
133
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
134
+ * Fork the project
135
+ * Start a feature/bugfix branch
136
+ * Commit and push until you are happy with your contribution
137
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
138
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
139
+
140
+ ## Copyright
141
+
142
+ Copyright (c) 2014 Christian Frichot. See LICENSE.txt for
143
+ further details.
144
+
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Devise::CheckgaController < Devise::SessionsController
4
+ if Rails.version >= '4'
5
+ prepend_before_action :devise_resource, only: [:show]
6
+ prepend_before_action :require_no_authentication, only: %i[show update]
7
+ else
8
+ prepend_before_filter :devise_resource, only: [:show]
9
+ prepend_before_filter :require_no_authentication, only: %i[show update]
10
+ end
11
+
12
+ include Devise::Controllers::Helpers
13
+
14
+ def show
15
+ @tmpid = params[:id]
16
+ if @tmpid.nil?
17
+ redirect_to :root
18
+ else
19
+ render :show
20
+ end
21
+ end
22
+
23
+ def update
24
+ resource = resource_class.find_by_gauth_tmp(params[resource_name]['tmpid'])
25
+
26
+ if resource
27
+
28
+ if resource.validate_token(params[resource_name]['gauth_token'].to_i)
29
+ set_flash_message(:notice, :signed_in) if is_navigational_format?
30
+ sign_in(resource_name, resource)
31
+ warden.manager._run_callbacks(:after_set_user, resource, warden, { event: :authentication })
32
+ respond_with resource, location: after_sign_in_path_for(resource)
33
+
34
+ if resource.class.ga_remembertime
35
+ cookies.signed[:gauth] = {
36
+ value: resource.email << ',' << Time.now.to_i.to_s,
37
+ secure: !(Rails.env.test? || Rails.env.development?),
38
+ expires: (resource.class.ga_remembertime + 1.days).from_now
39
+ }
40
+ end
41
+ else
42
+ set_flash_message(:error, :error)
43
+ redirect_to :root
44
+ end
45
+
46
+ else
47
+ set_flash_message(:error, :error)
48
+ redirect_to :root
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def devise_resource
55
+ self.resource = resource_class.new
56
+ end
57
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'devise/version'
4
+
5
+ class Devise::DisplayqrController < DeviseController
6
+ if Rails.version >= '4'
7
+ prepend_before_action :authenticate_scope!, only: %i[show update refresh]
8
+ else
9
+ prepend_before_filter :authenticate_scope!, only: %i[show update refresh]
10
+ end
11
+
12
+ include Devise::Controllers::Helpers
13
+
14
+ # GET /resource/displayqr
15
+ def show
16
+ if resource.nil? || resource.gauth_secret.nil?
17
+ sign_in resource_class.new, resource
18
+ redirect_to stored_location_for(scope) || :root
19
+ else
20
+ @tmpid = resource.assign_tmp
21
+ render :show
22
+ end
23
+ end
24
+
25
+ def update
26
+ if resource.gauth_tmp != params[resource_name]['tmpid'] || !resource.validate_token(params[resource_name]['gauth_token'].to_i)
27
+ set_flash_message(:error, :invalid_token)
28
+ render :show
29
+ return
30
+ end
31
+
32
+ if resource.set_gauth_enabled(params[resource_name]['gauth_enabled'])
33
+ set_flash_message :notice, (resource.gauth_enabled? ? :enabled : :disabled)
34
+ if Gem::Version.new(Devise::VERSION) < Gem::Version.new('4.2.0')
35
+ sign_in scope, resource, bypass: true
36
+ else
37
+ bypass_sign_in resource, scope: scope
38
+ end
39
+
40
+ respond_with resource, location: after_sign_in_path_for(resource)
41
+ else
42
+ render :show
43
+ end
44
+ end
45
+
46
+ def refresh
47
+ unless resource.nil?
48
+ resource.send(:assign_auth_secret)
49
+ resource.save
50
+ set_flash_message :notice, :newtoken
51
+ sign_in scope, resource, bypass: true
52
+ redirect_to [resource_name, :displayqr]
53
+ else
54
+ redirect_to :root
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def scope
61
+ resource_name.to_sym
62
+ end
63
+
64
+ def authenticate_scope!
65
+ # https://github.com/AsteriskLabs/devise_google_authenticator/issues/29
66
+ send(:"authenticate_#{resource_name}!", force: true)
67
+ self.resource = send("current_#{resource_name}")
68
+ end
69
+
70
+ # 7/2/15 - Unsure if this is used anymore - @xntrik
71
+ def resource_params
72
+ if strong_parameters_enabled?
73
+ return params.require(resource_name.to_sym).permit(:gauth_enabled)
74
+ end
75
+
76
+ params
77
+ end
78
+
79
+ def strong_parameters_enabled?
80
+ defined?(ActionController::StrongParameters)
81
+ end
82
+ end
@@ -0,0 +1,7 @@
1
+ <h2><%= I18n.t('submit_token_title', { scope: 'devise' }) %></h2>
2
+
3
+ <%= form_for(resource, as: resource_name, url: [resource_name, :checkga], html: { method: :put }) do |f| %>
4
+ <%= f.hidden_field :tmpid, { value: @tmpid } %>
5
+ <%= f.text_field :gauth_token, autocomplete: :off%>
6
+ <p><%= f.submit I18n.t('submit_token', { scope: 'devise' }) %></p>
7
+ <% end %>
@@ -0,0 +1,20 @@
1
+ <h2><%= I18n.t('title', { scope: 'devise.registration' }) %></h2>
2
+
3
+ <%= google_authenticator_qrcode(resource) %>
4
+
5
+ <%= form_for(resource, as: resource_name, url: [:refresh, resource_name, :displayqr], html: { method: :post }) do |f|%>
6
+ <p><%= f.submit I18n.t('newtoken', { scope: 'devise.registration' }) %></p>
7
+ <% end %>
8
+
9
+ <%= form_for(resource, as: resource_name, url: [resource_name, :displayqr], html: { method: :put }) do |f| %>
10
+ <%= devise_error_messages! %>
11
+ <h3><%= I18n.t('nice_request', { scope: 'devise.registration' }) %></h3>
12
+ <p><%= f.label :gauth_enabled, I18n.t('qrstatus', { scope: 'devise.registration' }) %><br />
13
+ <%= f.check_box :gauth_enabled %></p>
14
+ <%= f.hidden_field :tmpid, value: @tmpid %>
15
+ <p><%= f.label :gauth_token, I18n.t('enter_token', { scope: 'devise.registration' }) %><br />
16
+ <%= f.number_field :gauth_token, autocomplete: :off %>
17
+
18
+ <p><%= f.submit I18n.t('submit', { scope: 'devise.registration' }) %></p>
19
+ <% end %>
20
+
@@ -0,0 +1,21 @@
1
+ en:
2
+ devise:
3
+ submit_token: "Check Token"
4
+ submit_token_title: "Please enter your Google Authenticator token:"
5
+ registration:
6
+ title: "Your QR Code:"
7
+ nice_request: "Would you like to enable Google Authenticator?"
8
+ qrstatus: "Google Authenticator Status:"
9
+ enter_token: "Please enter your token number to continue"
10
+ submit: "Continue..."
11
+ newtoken: "Generate new token"
12
+ checkga:
13
+ user:
14
+ signed_in: "Signed in successfully from token."
15
+ error: "Sign in failed"
16
+ displayqr:
17
+ user:
18
+ status: "User status updated!"
19
+ newtoken: "You have updated to a new token - make sure you update your Google Authenticator application before continuing!!"
20
+ disabled: "2FA is currently disabled"
21
+ enabled: "2FA is currently enabled"
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeviseGoogleAuthenticator
4
+ module Controllers
5
+ module Helpers
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ if Rails.version >= '4'
10
+ before_action :check_request_and_redirect_to_check_totp,
11
+ if: :user_signing_in?
12
+ else
13
+ before_filter :check_request_and_redirect_to_check_totp,
14
+ if: :user_signing_in?
15
+ end
16
+
17
+ define_method :checkga_resource_path_name do |resource, id|
18
+ name = resource.class.name.singularize.underscore
19
+ name = name.split('/').last
20
+ "#{name}_checkga_path(id:'#{id}')"
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def devise_sessions_controller?
27
+ instance_of?(Devise::SessionsController) ||
28
+ self.class.ancestors.include?(Devise::SessionsController)
29
+ end
30
+
31
+ def user_signing_in?
32
+ if devise_controller? && signed_in?(resource_name) &&
33
+ devise_sessions_controller? &&
34
+ action_name == 'create'
35
+ return true
36
+ end
37
+
38
+ false
39
+ end
40
+
41
+ def check_request_and_redirect_to_check_totp
42
+ # User successfully signed in AND has enabled 2FA
43
+ if signed_in?(resource_name) &&
44
+ warden.session(resource_name)[:with_totp_authentication]
45
+
46
+ resource = warden.authenticate!(auth_options)
47
+
48
+ # The user has 2FA and token has expired
49
+ if resource.respond_to?(:get_qr) && resource.require_token?(cookies.signed[:gauth])
50
+
51
+ tmpid = resource.assign_tmp # Assign a temporary key and fetch it
52
+ warden.logout # log the user out
53
+
54
+ # We head back into the checkga controller with the temporary id
55
+ # Because the model used for google auth may not always be the same,
56
+ # and may be a sub-model, the eval will evaluate the appropriate path
57
+ # name
58
+ # This change addresses https://github.com/AsteriskLabs/devise_google_authenticator/issues/7
59
+ respond_with resource,
60
+ location: eval(checkga_resource_path_name(resource,
61
+ tmpid))
62
+ # User is not using, OR not enabled for Google 2FA, OR is remembering
63
+ # token and therefore not asking for the moment
64
+ # carry on, nothing to see here.
65
+ else
66
+ respond_with resource, location: after_sign_in_path_for(resource)
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ Warden::Manager.after_authentication do |user, auth, _|
4
+ gauth_enabled = true
5
+ gauth_enabled = user.gauth_enabled? if user.respond_to?(:gauth_enabled?)
6
+
7
+ if gauth_enabled && user.respond_to?(:with_totp_authentication?)
8
+ with_totp_authentication = user.with_totp_authentication?
9
+ # Build Warden scope from user class name
10
+ user_scope = user.class.name.underscore.to_sym
11
+ # Ensure Warden knows about it
12
+ if auth.config[:default_strategies].keys.include?(user_scope)
13
+ scope = auth.session(user_scope)
14
+ scope[:with_totp_authentication] = with_totp_authentication
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rotp'
4
+ require 'devise_google_authenticatable/hooks/totp_authenticatable'
5
+
6
+ module Devise
7
+ module Models
8
+ module GoogleAuthenticatable
9
+ def self.included(base)
10
+ base.extend ClassMethods
11
+
12
+ base.class_eval do
13
+ before_validation :assign_auth_secret, on: :create
14
+ include InstanceMethods
15
+
16
+ # Allow base model to override the gauth_enabled? method
17
+ unless base.method_defined?(:gauth_enabled?)
18
+ include GauthEnabledInstanceMethod
19
+ end
20
+ end
21
+ end
22
+
23
+ module InstanceMethods
24
+ # Is the TOTP Authentication enabled
25
+ def with_totp_authentication?
26
+ gauth_enabled.to_i != 0
27
+ end
28
+
29
+ def get_qr
30
+ gauth_secret
31
+ end
32
+
33
+ def set_gauth_enabled(param)
34
+ update(gauth_enabled: param)
35
+ end
36
+
37
+ def assign_tmp
38
+ update(gauth_tmp: ROTP::Base32.random_base32(32),
39
+ gauth_tmp_datetime: Time.now)
40
+ gauth_tmp
41
+ end
42
+
43
+ def validate_token(token)
44
+ return false if gauth_tmp_datetime.nil?
45
+
46
+ return false if gauth_tmp_datetime < self.class.ga_timeout.ago
47
+
48
+ valid_vals = []
49
+ valid_vals << ROTP::TOTP.new(get_qr).at(Time.now)
50
+ (1..self.class.ga_timedrift).each do |cc|
51
+ valid_vals << ROTP::TOTP.new(get_qr).at(Time.now.ago(30 * cc))
52
+ valid_vals << ROTP::TOTP.new(get_qr).at(Time.now.in(30 * cc))
53
+ end
54
+
55
+ valid_vals.include?(token.to_i)
56
+ end
57
+
58
+ def require_token?(cookie)
59
+ return true if self.class.ga_remembertime.nil? || cookie.blank?
60
+
61
+ array = cookie.to_s.split ','
62
+ return true if array.count != 2
63
+
64
+ last_logged_in_email = array[0]
65
+ last_logged_in_time = array[1].to_i
66
+
67
+ last_logged_in_email != email ||
68
+ (Time.now.to_i - last_logged_in_time) > self.class.ga_remembertime
69
+ .to_i
70
+ end
71
+
72
+ private
73
+
74
+ def assign_auth_secret
75
+ self.gauth_secret = ROTP::Base32.random_base32(64)
76
+ end
77
+ end
78
+
79
+ module GauthEnabledInstanceMethod
80
+ def gauth_enabled?
81
+ # Active_record seems to handle determining the status better this way
82
+ if gauth_enabled.respond_to?('to_i')
83
+ gauth_enabled.to_i != 0
84
+ # Mongoid does NOT have a .to_i for the Boolean return value, hence,
85
+ # we can just return it
86
+ else
87
+ gauth_enabled
88
+ end
89
+ end
90
+ end
91
+
92
+ module ClassMethods
93
+ def find_by_gauth_tmp(gauth_tmp)
94
+ where(gauth_tmp: gauth_tmp).first
95
+ end
96
+ ::Devise::Models.config(self, :ga_timeout, :ga_timedrift,
97
+ :ga_remembertime, :ga_appname,
98
+ :ga_bypass_signup)
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeviseGoogleAuthenticator
4
+ module Orm
5
+ # This module contains handle schema (migrations):
6
+ #
7
+ # create_table :accounts do |t|
8
+ # t.gauth_secret
9
+ # t.gauth_enabled
10
+ # end
11
+ #
12
+
13
+ module ActiveRecord
14
+ module Schema
15
+ include DeviseGoogleAuthenticator::Schema
16
+ end
17
+ end
18
+
19
+ end
20
+ end
21
+
22
+ ActiveRecord::ConnectionAdapters::Table.send :include, DeviseGoogleAuthenticator::Orm::ActiveRecord::Schema
23
+ ActiveRecord::ConnectionAdapters::TableDefinition.send :include, DeviseGoogleAuthenticator::Orm::ActiveRecord::Schema
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeviseGoogleAuthenticator::Patches
4
+ # patch Registrations controller to display the QR code
5
+ module DisplayQR
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ # arrr be the patch
10
+ alias_method :create_original, :create
11
+
12
+ define_method :create do
13
+ build_resource(sign_up_params)
14
+
15
+ if resource.save
16
+ yield resource if block_given?
17
+ if resource.active_for_authentication?
18
+ set_flash_message :notice, :signed_up if is_flashing_format?
19
+ sign_in(resource_name, resource)
20
+
21
+ if resource.respond_to? :gauth_enabled?
22
+ if resource.class.ga_bypass_signup
23
+ respond_with resource, location: after_sign_up_path_for(resource)
24
+ else
25
+ respond_with resource, location: { controller: 'displayqr', action: 'show' }
26
+ end
27
+ else
28
+ respond_with resource, location: after_sign_up_path_for(resource)
29
+ end
30
+
31
+ else
32
+ set_flash_message :notice, :"signed_up_but_#{resource.inactive_message}" if is_flashing_format?
33
+ expire_data_after_sign_in!
34
+ respond_with resource, location: after_inactive_sign_up_path_for(resource)
35
+ end
36
+ else
37
+ clean_up_passwords resource
38
+ respond_with resource
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeviseGoogleAuthenticator
4
+ module Patches
5
+ autoload :DisplayQR, 'devise_google_authenticatable/patches/display_qr'
6
+
7
+ class << self
8
+ def apply
9
+ Devise::RegistrationsController.send(:include, Patches::DisplayQR)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'devise_google_authenticatable/controllers/helpers'
4
+
5
+ module DeviseGoogleAuthenticator
6
+ class Engine < ::Rails::Engine
7
+ if Rails.version > '5'
8
+ ActiveSupport::Reloader.to_prepare do
9
+ DeviseGoogleAuthenticator::Patches.apply
10
+ end
11
+ else
12
+ ActionDispatch::Callbacks.to_prepare do
13
+ DeviseGoogleAuthenticator::Patches.apply
14
+ end
15
+ end
16
+
17
+ ActiveSupport.on_load(:action_controller) do
18
+ include DeviseGoogleAuthenticator::Controllers::Helpers
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionDispatch
4
+ module Routing
5
+ class Mapper
6
+ protected
7
+
8
+ # route for handle expired passwords
9
+ def devise_displayqr(mapping, controllers)
10
+ resource :displayqr, only: %i[show update], path: mapping.path_names[:displayqr], controller: controllers[:displayqr] do
11
+ post :refresh, path: mapping.path_names[:refresh], as: :refresh
12
+ end
13
+
14
+ resource :checkga, only: %i[show update], path: mapping.path_names[:checkga], controller: controllers[:checkga]
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeviseGoogleAuthenticator
4
+ # add schema helper for migrations
5
+ module Schema
6
+ # Add gauth_secret columns in the resource's database tables
7
+ #
8
+ # Examples
9
+ #
10
+ # # For a new resource migration:
11
+ # create_table :the_resources do |t|
12
+ # t.gauth_secret
13
+ # t.gauth_enabled
14
+ # ...
15
+ # end
16
+ #
17
+ # # or if the resource's table already exists, define a migration and put this in:
18
+ # change_table :the_resources do |t|
19
+ # t.string :gauth_secret
20
+ # t.boolean :gauth_enabled
21
+ # end
22
+ #
23
+ def gauth_secret
24
+ apply_devise_schema :gauth_secret, String
25
+ end
26
+
27
+ def gauth_enabled
28
+ apply_devise_schema :gauth_enabled, Integer, { default: 0 }
29
+ end
30
+
31
+ def gauth_tmp
32
+ apply_devise_schema :gauth_tmp, String
33
+ end
34
+
35
+ def gauth_tmp_datetime
36
+ apply_devise_schema :gauth_tmp_datetime, Datetime
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeviseGoogleAuthenticator
4
+ VERSION = '0.4.0'
5
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rqrcode'
4
+ require 'base64'
5
+
6
+ module DeviseGoogleAuthenticator
7
+ module Views
8
+ module Helpers
9
+ def build_qrcode_data_from(username, app, gauth_secret, qualifier = nil, issuer = nil)
10
+ data = "otpauth://totp/#{otpauth_user(username, app, qualifier)}"
11
+ data += "?secret=#{gauth_secret}"
12
+ data += "&issuer=#{issuer}" unless issuer.nil?
13
+ data
14
+ end
15
+
16
+ def build_qrcode_from(data)
17
+ qrcode = RQRCode::QRCode.new(data, level: :m, mode: :byte_8bit)
18
+ png = qrcode.as_png(fill: 'white', color: 'black', border_modules: 1, module_px_size: 4)
19
+ "data:image/png;base64,#{Base64.encode64(png.to_s).strip}"
20
+ end
21
+
22
+ def application_name_from(user)
23
+ user.class.ga_appname || if Rails.version < '6'
24
+ Rails.application.class.parent_name
25
+ else
26
+ Rails.application.class.module_parent_name
27
+ end
28
+ end
29
+
30
+ def google_authenticator_qrcode(user, qualifier = nil, issuer = nil)
31
+ data = build_qrcode_data_from(
32
+ username_from_email(user.email),
33
+ application_name_from(user),
34
+ user.gauth_secret,
35
+ qualifier,
36
+ issuer
37
+ )
38
+
39
+ # data-uri is easier, so...
40
+ image_tag(build_qrcode_from(data), alt: 'Google Authenticator QRCode')
41
+ end
42
+
43
+ def otpauth_user(username, app, qualifier = nil)
44
+ "#{username}@#{app}#{qualifier}"
45
+ end
46
+
47
+ def username_from_email(email)
48
+ /^(.*)@/.match(email)[1]
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record/connection_adapters/abstract/schema_definitions'
4
+ require 'active_support/core_ext/integer'
5
+ require 'active_support/core_ext/string'
6
+ require 'active_support/ordered_hash'
7
+ require 'active_support/concern'
8
+ require 'devise'
9
+
10
+ module Devise # :nodoc:
11
+ mattr_accessor :ga_timeout
12
+ @@ga_timeout = 3.minutes
13
+
14
+ mattr_accessor :ga_timedrift
15
+ @@ga_timedrift = 3
16
+
17
+ mattr_accessor :ga_remembertime
18
+ @@ga_remembertime = 1.month
19
+
20
+ mattr_accessor :ga_appname
21
+ @@ga_appname = if Rails.version < '6'
22
+ Rails.application.class.parent_name
23
+ else
24
+ Rails.application.class.module_parent_name
25
+ end
26
+
27
+ mattr_accessor :ga_bypass_signup
28
+ @@ga_bypass_signup = false
29
+ end
30
+
31
+ # a security extension for devise
32
+ module DeviseGoogleAuthenticator
33
+ autoload :Schema, 'devise_google_authenticatable/schema'
34
+ autoload :Patches, 'devise_google_authenticatable/patches'
35
+ end
36
+
37
+ require 'devise_google_authenticatable/routes'
38
+ require 'devise_google_authenticatable/rails'
39
+ require 'devise_google_authenticatable/orm/active_record'
40
+ require 'devise_google_authenticatable/controllers/helpers'
41
+ require 'devise_google_authenticatable/views/helpers'
42
+ ActionView::Base.send :include, DeviseGoogleAuthenticator::Views::Helpers
43
+
44
+ Devise.add_module :google_authenticatable, controller: :google_authenticatable, model: 'devise_google_authenticatable/models/google_authenticatable', route: :displayqr
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators/active_record'
4
+
5
+ module ActiveRecord
6
+ module Generators
7
+ class DeviseGoogleAuthenticatorGenerator < ActiveRecord::Generators::Base
8
+ source_root File.expand_path("../templates", __FILE__)
9
+
10
+ def copy_devise_migration
11
+ migration_template "migration.rb", "db/migrate/devise_google_authenticator_add_to_#{table_name}.rb"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DeviseGoogleAuthenticatorAddTo<%= table_name.camelize %> < ActiveRecord::Migration
4
+ def self.up
5
+ change_table :<%= table_name %> do |t|
6
+ t.string :gauth_secret
7
+ t.string :gauth_enabled, default: '0'
8
+ t.string :gauth_tmp
9
+ t.datetime :gauth_tmp_datetime
10
+ end
11
+
12
+ end
13
+
14
+ def self.down
15
+ change_table :<%= table_name %> do |t|
16
+ t.remove :gauth_secret, :gauth_enabled, :gauth_tmp, :gauth_tmp_datetime
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeviseGoogleAuthenticator
4
+ module Generators
5
+ class DeviseGoogleAuthenticatorGenerator < Rails::Generators::NamedBase
6
+ namespace 'devise_google_authenticator'
7
+
8
+ desc 'Add :google_authenticatable directive in the given model, plus accessors. Also generate migration for ActiveRecord'
9
+
10
+ def inject_devise_google_authenticator_content
11
+ path = File.join('app', 'models', "#{file_path}.rb")
12
+
13
+ return unless File.exist?(path)
14
+
15
+ inject_into_file(path, 'google_authenticatable, :', after: 'devise :')
16
+ inject_into_file(path, 'gauth_enabled, :gauth_tmp, :gauth_tmp_datetime, :', after: 'attr_accessible :') if needs_attr_accessible?
17
+ inject_into_class(path, class_name, "\tattr_accessor :gauth_token\n")
18
+ end
19
+
20
+ hook_for :orm
21
+
22
+ private
23
+
24
+ def needs_attr_accessible?
25
+ rails_3? && !strong_parameters_enabled?
26
+ end
27
+
28
+ def rails_3?
29
+ Rails::VERSION::MAJOR == 3
30
+ end
31
+
32
+ def strong_parameters_enabled?
33
+ defined?(ActionController::StrongParameters)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeviseGoogleAuthenticator
4
+ module Generators
5
+ # Install Generator
6
+ class InstallGenerator < Rails::Generators::Base
7
+ source_root File.expand_path('../../templates', __FILE__)
8
+
9
+ desc 'Install the devise google authenticator extension'
10
+
11
+ def add_configs
12
+ inject_into_file 'config/initializers/devise.rb', "\n # ==> Devise Google Authenticator Extension\n # Configure extension for devise\n\n" +
13
+ " # How long should the user have to enter their token. To change the default, uncomment and change the below:\n" +
14
+ " # config.ga_timeout = 3.minutes\n\n" +
15
+ " # Change time drift settings for valid token values. To change the default, uncomment and change the below:\n" +
16
+ " # config.ga_timedrift = 3\n\n" +
17
+ " # Change setting to how long to remember device before requiring another token. Change to nil to turn feature off.\n" +
18
+ " # To change the default, uncomment and change the below:\n" +
19
+ " # config.ga_remembertime = 1.month\n\n" +
20
+ " # Change setting to assign the application name used by code generator. Defaults to Rails.application.class.parent_name.\n" +
21
+ " # To change the default, uncomment and change the below:\n" +
22
+ " # config.ga_appname = 'example.com'\n\n" +
23
+ " # Change setting to bypass the Display QR page immediately after a user sign's up\n" +
24
+ " # To change the default, uncomment and change the below. Defaults to false:\n" +
25
+ " # config.ga_bypass_signup = true\n\n" +
26
+ "\n", before: /end[ |\n|]+\Z/
27
+ end
28
+
29
+ def copy_locale
30
+ copy_file '../../../config/locales/en.yml', 'config/locales/devise.google_authenticator.en.yml'
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'generators/devise/views_generator'
4
+
5
+ module DeviseGoogleAuthenticator
6
+ module Generators
7
+ class ViewsGenerator < Rails::Generators::Base
8
+ desc 'Copies all Devise Google Authenticator views to your application.'
9
+
10
+ argument :scope, required: false, default: nil,
11
+ desc: 'The scope to copy views to'
12
+
13
+ include ::Devise::Generators::ViewPathTemplates
14
+ source_root File.expand_path('../../../../app/views/devise', __FILE__)
15
+ def copy_views
16
+ view_directory :checkga
17
+ view_directory :displayqr
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators/named_base'
4
+ require 'generators/devise/orm_helpers'
5
+
6
+ module Mongoid
7
+ module Generators
8
+ class DeviseGoogleAuthenticatorGenerator < Rails::Generators::NamedBase
9
+ include Devise::Generators::OrmHelpers
10
+
11
+ def inject_field_types
12
+ inject_into_file model_path, migration_data, after: "include Mongoid::Document\n" if model_exists?
13
+ end
14
+
15
+ def migration_data
16
+ <<RUBY
17
+ # Google Authenticator
18
+ field :gauth_secret, type: String
19
+ field :gauth_enabled, type: Boolean, default: 'f'
20
+ field :gauth_tmp, type: String
21
+ field :gauth_tmp_datetime, type: DateTime
22
+
23
+ RUBY
24
+ end
25
+ end
26
+ end
27
+ end
28
+
metadata ADDED
@@ -0,0 +1,155 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: devise_gauth
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.0
5
+ platform: ruby
6
+ authors:
7
+ - Pharmony SA
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-01-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: actionmailer
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 5.2.8.1
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '6'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: 5.2.8.1
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '6'
33
+ - !ruby/object:Gem::Dependency
34
+ name: devise
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: 4.9.3
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: 4.9.3
47
+ - !ruby/object:Gem::Dependency
48
+ name: railties
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: 5.2.8.1
54
+ - - "<"
55
+ - !ruby/object:Gem::Version
56
+ version: '6'
57
+ type: :runtime
58
+ prerelease: false
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - "~>"
62
+ - !ruby/object:Gem::Version
63
+ version: 5.2.8.1
64
+ - - "<"
65
+ - !ruby/object:Gem::Version
66
+ version: '6'
67
+ - !ruby/object:Gem::Dependency
68
+ name: rotp
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '1.6'
74
+ type: :runtime
75
+ prerelease: false
76
+ version_requirements: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: '1.6'
81
+ - !ruby/object:Gem::Dependency
82
+ name: rqrcode
83
+ requirement: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - "~>"
86
+ - !ruby/object:Gem::Version
87
+ version: 2.1.2
88
+ type: :runtime
89
+ prerelease: false
90
+ version_requirements: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - "~>"
93
+ - !ruby/object:Gem::Version
94
+ version: 2.1.2
95
+ description: Devise Google Authenticator Extension, for adding Google's OTP to your
96
+ Rails apps!
97
+ email:
98
+ - dev@pharmony.eu
99
+ executables: []
100
+ extensions: []
101
+ extra_rdoc_files: []
102
+ files:
103
+ - LICENSE.txt
104
+ - README.md
105
+ - app/controllers/devise/checkga_controller.rb
106
+ - app/controllers/devise/displayqr_controller.rb
107
+ - app/views/devise/checkga/show.html.erb
108
+ - app/views/devise/displayqr/show.html.erb
109
+ - config/locales/en.yml
110
+ - lib/devise_google_authenticatable/controllers/helpers.rb
111
+ - lib/devise_google_authenticatable/hooks/totp_authenticatable.rb
112
+ - lib/devise_google_authenticatable/models/google_authenticatable.rb
113
+ - lib/devise_google_authenticatable/orm/active_record.rb
114
+ - lib/devise_google_authenticatable/patches.rb
115
+ - lib/devise_google_authenticatable/patches/display_qr.rb
116
+ - lib/devise_google_authenticatable/rails.rb
117
+ - lib/devise_google_authenticatable/routes.rb
118
+ - lib/devise_google_authenticatable/schema.rb
119
+ - lib/devise_google_authenticatable/version.rb
120
+ - lib/devise_google_authenticatable/views/helpers.rb
121
+ - lib/devise_google_authenticator.rb
122
+ - lib/generators/active_record/devise_google_authenticator_generator.rb
123
+ - lib/generators/active_record/templates/migration.rb
124
+ - lib/generators/devise_google_authenticator/devise_google_authenticator_generator.rb
125
+ - lib/generators/devise_google_authenticator/install_generator.rb
126
+ - lib/generators/devise_google_authenticator/views_generator.rb
127
+ - lib/generators/mongoid/devise_google_authenticator_generator.rb
128
+ homepage: http://github.com/pharmony/devise_gauth
129
+ licenses:
130
+ - MIT
131
+ metadata:
132
+ allowed_push_host: https://rubygems.org
133
+ homepage_uri: http://github.com/pharmony/devise_gauth
134
+ source_code_uri: http://github.com/pharmony/devise_gauth
135
+ changelog_uri: http://github.com/pharmony/devise_gauth/blob/master/CHANGELOG.md
136
+ post_install_message:
137
+ rdoc_options: []
138
+ require_paths:
139
+ - lib
140
+ required_ruby_version: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '2.7'
145
+ required_rubygems_version: !ruby/object:Gem::Requirement
146
+ requirements:
147
+ - - ">="
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ requirements: []
151
+ rubygems_version: 3.1.6
152
+ signing_key:
153
+ specification_version: 4
154
+ summary: Devise Google Authenticator Extension
155
+ test_files: []