devise-otp 0.8.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 95e8a170c1942c78dae2cb24c06a818c6e91854268acf78df61b83e22fd18726
4
- data.tar.gz: 409a75794455459fa1c88892216d556b138cf1a40f4bfe0a06f3ce65a47a528e
3
+ metadata.gz: 7a2884c76d255b2c2bb3b40859db7018c5668d49fcb6897141dacd8d7df0bc62
4
+ data.tar.gz: 3c7ee30dd96a9711b4b4194cb2b94cea6abb06113dda2ef58b70c9a96c998ec4
5
5
  SHA512:
6
- metadata.gz: 45d13d374f2a2504fcee11d81f8771712ae706d138725e3bb777470b099b106cb09ffdb4e2132b6ac5c837b00bb341ff9b5fc56ce5981ff144b358d1e8ed4338
7
- data.tar.gz: b946e7a0551ba464285e324559f287a9765657258159e508fe08f4b22068548a4be6602d3173a6fac46638130e5795c69bdbdd4b28031d48cfaa592b87eef9da
6
+ metadata.gz: b099c20c5c6d76fc2e1226511615bb43f2b081620f01bbf1bcc939cec66e286aa3536f8a654495907607e42d23e7f3efd9bb3377ed3a1a8a6ef522ec5e9568e1
7
+ data.tar.gz: 731d1dc34877d0fe91bb092b33ce4ee7c302c4cb75183daa3ff67a0df7f6d242bc86b3ab4e0923683b8408c8f1e6bab8e8a1d1c5fa10b9b0e104617a9b87ded7
@@ -16,6 +16,17 @@ jobs:
16
16
  - '3.2'
17
17
  - '3.1'
18
18
  - 'head'
19
+ rails:
20
+ - rails_8.0
21
+ - rails_7.2
22
+ - rails_7.1
23
+ - rails_7.0
24
+ exclude:
25
+ - ruby: '3.1'
26
+ rails: 'rails_8.0'
27
+
28
+ env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
29
+ BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.rails }}.gemfile
19
30
 
20
31
  steps:
21
32
  - name: Checkout
data/.gitignore CHANGED
@@ -38,7 +38,9 @@ test/dummy/db/*.sqlite3
38
38
  test/dummy/db/*.sqlite3-shm
39
39
  test/dummy/db/*.sqlite3-wal
40
40
 
41
+ # Ignore Gemfile.lock
41
42
  Gemfile.lock
43
+ gemfiles/*.lock
42
44
 
43
45
  # Generated test files
44
46
  tmp/*
data/Appraisals ADDED
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ appraise 'rails_7.0' do
4
+ gem 'rails', '~> 7.0.0'
5
+ gem 'sqlite3', '~> 1.5.0'
6
+
7
+ # Fix: LoadError: cannot load such file -- base64
8
+ install_if '-> { Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.3.0") }' do
9
+ gem 'base64'
10
+ gem 'bigdecimal'
11
+ gem 'mutex_m'
12
+ gem 'drb'
13
+ gem 'logger'
14
+ end
15
+ end
16
+
17
+ appraise 'rails_7.1' do
18
+ gem 'rails', '~> 7.1.0'
19
+ gem 'sqlite3', '~> 1.5.0'
20
+
21
+ # Fix:
22
+ # warning: logger was loaded from the standard library, but will no longer be part of the default gems since Ruby 3.5.0.
23
+ # Add logger to your Gemfile or gemspec.
24
+ install_if '-> { Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.4.0") }' do
25
+ gem 'logger'
26
+ end
27
+ end
28
+
29
+ appraise 'rails_7.2' do
30
+ gem 'rails', '~> 7.2.0'
31
+ gem 'sqlite3', '~> 1.5.0'
32
+ end
33
+
34
+ appraise 'rails_8.0' do
35
+ gem 'rails', '~> 8.0.0'
36
+ end
data/Gemfile CHANGED
@@ -3,6 +3,8 @@ source "https://rubygems.org"
3
3
  # Specify your gem's dependencies in devise-otp.gemspec
4
4
  gemspec
5
5
 
6
+ gem "appraisal", git: "https://github.com/thoughtbot/appraisal.git"
7
+
6
8
  gem "capybara"
7
9
  gem "minitest-reporters", ">= 0.5.0"
8
10
  gem "puma"
@@ -10,5 +12,5 @@ gem "rake"
10
12
  gem "rdoc"
11
13
  gem "shoulda"
12
14
  gem "sprockets-rails"
13
- gem "sqlite3", "~> 1.4"
15
+ gem "sqlite3", "~> 2.1"
14
16
  gem "standardrb"
data/README.md CHANGED
@@ -13,7 +13,7 @@ Some of the compatible token devices are:
13
13
  * [Google Authenticator](https://code.google.com/p/google-authenticator/)
14
14
  * [FreeOTP](https://fedorahosted.org/freeotp/)
15
15
 
16
- Device OTP was recently updated to work with Rails 7 and Turbo.
16
+ Device OTP was recently updated to work with Rails 7+ and Turbo.
17
17
 
18
18
  ## Sponsor
19
19
 
@@ -58,10 +58,13 @@ Don't forget to migrate:
58
58
 
59
59
  rake db:migrate
60
60
 
61
- Add the gem's JavaScript to you `application.js`:
61
+ ### Default CSS
62
62
 
63
- //= require devise-otp
63
+ To use the default CSS for devise-otp, just require the devise-otp.css file as usual in your application.css file (or equivalent):
64
64
 
65
+ *= require devise-otp
66
+
67
+ It might be even easier to just copy the styles to your project.
65
68
 
66
69
  ### Custom views
67
70
 
@@ -77,9 +80,7 @@ The install generator also installs an english copy of a Devise OTP i18n file. T
77
80
 
78
81
  ### QR codes
79
82
 
80
- By default, Devise OTP assumes that you use [Sprockets](https://github.com/rails/sprockets) to render assets and so will use the ([qrcode.js](/app/assets/javascripts/qrcode.js)) embeded library to render the QR code.
81
-
82
- If you need something more, have a look at [QR codes](/docs/QR_CODES.md) documentation file.
83
+ Devise OTP generates QR Codes directly as SVG's via the [rqrcode](https://github.com/whomwah/rqrcode), so there are no JavaScript (or Sprockets) dependencies.
83
84
 
84
85
  ## Configuration
85
86
 
@@ -102,7 +103,7 @@ Enforcing mandatory OTP requires adding the ensure\_mandatory\_{scope}\_otp! met
102
103
 
103
104
  ## Authors
104
105
 
105
- The project was originally started by Lele Forzani by forking [devise_google_authenticator](https://github.com/AsteriskLabs/devise_google_authenticator) and still contains some devise_google_authenticator code. It's now maintained by [Josef Strzibny](https://github.com/strzibny/).
106
+ The project was originally started by Lele Forzani by forking [devise_google_authenticator](https://github.com/AsteriskLabs/devise_google_authenticator) and still contains some devise_google_authenticator code. It's now maintained by [Josef Strzibny](https://github.com/strzibny/) and [Laney Stroup](https://github.com/strouptl).
106
107
 
107
108
  Contributions are welcome!
108
109
 
@@ -0,0 +1,4 @@
1
+ .qrcode-container {
2
+ max-width: 300px;
3
+ margin: 0 auto;
4
+ }
@@ -26,17 +26,15 @@ module DeviseOtp
26
26
  # signs the resource in, if the OTP token is valid and the user has a valid challenge
27
27
  #
28
28
  def update
29
- if @token.blank?
30
- otp_set_flash_message(:alert, :token_blank)
31
- redirect_to otp_credential_path_for(resource_name, challenge: @challenge, recovery: @recovery)
32
- elsif resource.otp_challenge_valid? && resource.validate_otp_token(@token, @recovery)
29
+ if resource.otp_challenge_valid? && resource.validate_otp_token(@token, @recovery)
33
30
  sign_in(resource_name, resource)
34
31
 
35
32
  otp_set_trusted_device_for(resource) if params[:enable_persistence] == "true"
36
33
  otp_refresh_credentials_for(resource)
37
34
  respond_with resource, location: after_sign_in_path_for(resource)
38
35
  else
39
- otp_set_flash_message :alert, :token_invalid
36
+ kind = (@token.blank? ? :token_blank : :token_invalid)
37
+ otp_set_flash_message :alert, kind, :now => true
40
38
  render :show
41
39
  end
42
40
  end
@@ -103,7 +101,7 @@ module DeviseOtp
103
101
  end
104
102
 
105
103
  def failed_refresh
106
- otp_set_flash_message :alert, :invalid_refresh
104
+ otp_set_flash_message :alert, :invalid_refresh, :now => true
107
105
  render :refresh
108
106
  end
109
107
 
@@ -35,7 +35,7 @@ module DeviseOtp
35
35
  otp_set_flash_message :success, :successfully_updated
36
36
  redirect_to otp_token_path_for(resource)
37
37
  else
38
- otp_set_flash_message :danger, :could_not_confirm
38
+ otp_set_flash_message :danger, :could_not_confirm, :now => true
39
39
  render :edit
40
40
  end
41
41
  end
@@ -109,7 +109,6 @@ module DeviseOtp
109
109
  ensure_resource!
110
110
 
111
111
  if needs_credentials_refresh?(resource)
112
- otp_set_flash_message :notice, :need_to_refresh_credentials
113
112
  redirect_to refresh_otp_credential_path_for(resource)
114
113
  end
115
114
  end
@@ -14,7 +14,6 @@ en:
14
14
  otp_session_invalid: Session invalid. Please start again.
15
15
  token_invalid: 'The token you provided was invalid.'
16
16
  token_blank: 'You need to type in the token you generated with your device.'
17
- need_to_refresh_credentials: 'We need to check your credentials before you can change these settings.'
18
17
  valid_refresh: 'Thank you, your credentials were accepted.'
19
18
  invalid_refresh: 'Sorry, you provided the wrong credentials.'
20
19
  credentials_refresh:
@@ -41,7 +40,6 @@ en:
41
40
  successfully_set_persistence: 'Your device is now trusted.'
42
41
  successfully_cleared_persistence: 'Your device has been removed from the list of trusted devices.'
43
42
  successfully_reset_persistence: 'Your list of trusted devices has been cleared.'
44
- need_to_refresh_credentials: 'We need to check your credentials before you can change these settings.'
45
43
  recovery:
46
44
  title: 'Your Emergency Recovery Codes'
47
45
  explain: 'Take note or print these recovery codes. The will allow you to log back in in case your token device is lost, stolen, or unavailable.'
data/devise-otp.gemspec CHANGED
@@ -5,16 +5,17 @@ require_relative "lib/devise-otp/version"
5
5
  Gem::Specification.new do |gem|
6
6
  gem.name = "devise-otp"
7
7
  gem.version = Devise::OTP::VERSION
8
- gem.authors = ["Lele Forzani", "Josef Strzibny"]
9
- gem.email = ["lele@windmill.it", "strzibny@strzibny.name"]
10
- gem.description = "Time Based OTP/rfc6238 compatible authentication for Devise"
8
+ gem.authors = ["Lele Forzani", "Josef Strzibny", "Laney Stroup"]
9
+ gem.email = ["lele@windmill.it", "strzibny@strzibny.name", "laney@stroupsolutions.com"]
10
+ gem.description = "OTP authentication for Devise"
11
11
  gem.summary = "Time Based OTP/rfc6238 compatible authentication for Devise"
12
- gem.homepage = "http://git.windmill.it/wm/devise-otp"
12
+ gem.homepage = "https://github.com/wmlele/devise-otp"
13
13
 
14
14
  gem.files = `git ls-files`.split($/)
15
15
  gem.require_paths = ["lib"]
16
16
 
17
- gem.add_dependency "rails", ">= 7.0", "< 8.0"
17
+ gem.add_dependency "rails", ">= 7.0"
18
18
  gem.add_dependency "devise", ">= 4.8.0", "< 5.0"
19
19
  gem.add_dependency "rotp", ">= 2.0.0"
20
+ gem.add_dependency "rqrcode", "~> 2.0"
20
21
  end
@@ -0,0 +1,25 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal", git: "https://github.com/thoughtbot/appraisal.git"
6
+ gem "capybara"
7
+ gem "minitest-reporters", ">= 0.5.0"
8
+ gem "puma"
9
+ gem "rake"
10
+ gem "rdoc"
11
+ gem "shoulda"
12
+ gem "sprockets-rails"
13
+ gem "sqlite3", "~> 1.5.0"
14
+ gem "standardrb"
15
+ gem "rails", "~> 7.0.0"
16
+
17
+ install_if -> { Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.3.0") } do
18
+ gem "base64"
19
+ gem "bigdecimal"
20
+ gem "mutex_m"
21
+ gem "drb"
22
+ gem "logger"
23
+ end
24
+
25
+ gemspec path: "../"
@@ -0,0 +1,21 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal", git: "https://github.com/thoughtbot/appraisal.git"
6
+ gem "capybara"
7
+ gem "minitest-reporters", ">= 0.5.0"
8
+ gem "puma"
9
+ gem "rake"
10
+ gem "rdoc"
11
+ gem "shoulda"
12
+ gem "sprockets-rails"
13
+ gem "sqlite3", "~> 1.5.0"
14
+ gem "standardrb"
15
+ gem "rails", "~> 7.1.0"
16
+
17
+ install_if -> { Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.4.0") } do
18
+ gem "logger"
19
+ end
20
+
21
+ gemspec path: "../"
@@ -0,0 +1,17 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal", git: "https://github.com/thoughtbot/appraisal.git"
6
+ gem "capybara"
7
+ gem "minitest-reporters", ">= 0.5.0"
8
+ gem "puma"
9
+ gem "rake"
10
+ gem "rdoc"
11
+ gem "shoulda"
12
+ gem "sprockets-rails"
13
+ gem "sqlite3", "~> 1.5.0"
14
+ gem "standardrb"
15
+ gem "rails", "~> 7.2.0"
16
+
17
+ gemspec path: "../"
@@ -0,0 +1,17 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal", git: "https://github.com/thoughtbot/appraisal.git"
6
+ gem "capybara"
7
+ gem "minitest-reporters", ">= 0.5.0"
8
+ gem "puma"
9
+ gem "rake"
10
+ gem "rdoc"
11
+ gem "shoulda"
12
+ gem "sprockets-rails"
13
+ gem "sqlite3", "~> 2.1"
14
+ gem "standardrb"
15
+ gem "rails", "~> 8.0.0"
16
+
17
+ gemspec path: "../"
@@ -1,5 +1,5 @@
1
1
  module Devise
2
2
  module OTP
3
- VERSION = "0.8.0"
3
+ VERSION = "1.0.0"
4
4
  end
5
5
  end
@@ -1,3 +1,5 @@
1
+ require "rqrcode"
2
+
1
3
  module DeviseOtpAuthenticatable
2
4
  module Controllers
3
5
  module Helpers
@@ -12,11 +14,8 @@ module DeviseOtpAuthenticatable
12
14
  #
13
15
  def otp_set_flash_message(key, kind, options = {})
14
16
  options[:scope] ||= "devise.otp.#{controller_name}"
15
- options[:default] = Array(options[:default]).unshift(kind.to_sym)
16
- options[:resource_name] = resource_name
17
- options = devise_i18n_options(options) if respond_to?(:devise_i18n_options, true)
18
- message = I18n.t("#{options[:resource_name]}.#{kind}", **options)
19
- flash[key] = message if message.present?
17
+
18
+ set_flash_message(key, kind, options)
20
19
  end
21
20
 
22
21
  def otp_t
@@ -122,33 +121,11 @@ module DeviseOtpAuthenticatable
122
121
  # returns the URL for the QR Code to initialize the Authenticator device
123
122
  #
124
123
  def otp_authenticator_token_image(resource)
125
- otp_authenticator_token_image_js(resource.otp_provisioning_uri)
126
- end
127
-
128
- private
129
-
130
- def otp_authenticator_token_image_js(otp_url)
131
124
  content_tag(:div, class: "qrcode-container") do
132
- content_tag(:div, id: "qrcode", class: "qrcode") do
133
- javascript_tag(%[
134
- new QRCode("qrcode", {
135
- text: "#{otp_url}",
136
- width: 256,
137
- height: 256,
138
- colorDark : "#000000",
139
- colorLight : "#ffffff",
140
- correctLevel : QRCode.CorrectLevel.H
141
- });
142
- ])
143
- end
125
+ raw RQRCode::QRCode.new(resource.otp_provisioning_uri).as_svg(:module_size => 5, :viewbox => true, :use_path => true)
144
126
  end
145
127
  end
146
128
 
147
- def otp_authenticator_token_image_google(otp_url)
148
- otp_url = Rack::Utils.escape(otp_url)
149
- url = "https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl=#{otp_url}"
150
- image_tag(url, alt: "OTP Url QRCode")
151
- end
152
129
  end
153
130
  end
154
131
  end
@@ -11,4 +11,3 @@
11
11
  // GO AFTER THE REQUIRES BELOW.
12
12
  //
13
13
  //= require_tree .
14
- //= require devise-otp
@@ -8,6 +8,7 @@
8
8
  * You're free to add application-wide styles to this file and they'll appear at the top of the
9
9
  * compiled file, but it's generally better to create a new file per style scope.
10
10
  *
11
+ *= require devise-otp
11
12
  *= require_self
12
13
  *= require_tree .
13
14
  */
@@ -8,7 +8,13 @@
8
8
  </head>
9
9
  <body>
10
10
 
11
- <%= yield %>
11
+ <div id="alerts">
12
+ <% flash.keys.each do |key| %>
13
+ <%= content_tag :p, flash[key], :id => key %>
14
+ <% end %>
15
+ </div>
16
+
17
+ <%= yield %>
12
18
 
13
19
  </body>
14
20
  </html>
@@ -1,4 +1,4 @@
1
- class CreateAdmins < ActiveRecord::Migration[7.1]
1
+ class CreateAdmins < ActiveRecord::Migration[5.0]
2
2
  def change
3
3
  create_table :admins do |t|
4
4
  t.string :name
@@ -23,6 +23,9 @@ class DisableTokenTest < ActionDispatch::IntegrationTest
23
23
  disable_otp
24
24
 
25
25
  assert page.has_content? "Disabled"
26
+ within "#alerts" do
27
+ assert page.has_content? 'Two-Factor Authentication has been disabled.'
28
+ end
26
29
 
27
30
  # logout
28
31
  sign_out
@@ -20,6 +20,10 @@ class EnableOtpFormTest < ActionDispatch::IntegrationTest
20
20
  assert_equal user_otp_token_path, current_path
21
21
  assert page.has_content?("Enabled")
22
22
 
23
+ within "#alerts" do
24
+ assert page.has_content? 'Your Two-Factor Authentication settings have been updated.'
25
+ end
26
+
23
27
  user.reload
24
28
  assert user.otp_enabled?
25
29
  end
@@ -37,6 +41,15 @@ class EnableOtpFormTest < ActionDispatch::IntegrationTest
37
41
 
38
42
  user.reload
39
43
  assert_not user.otp_enabled?
44
+
45
+ within "#alerts" do
46
+ assert page.has_content? 'The Confirmation Code you entered did not match the QR code shown below.'
47
+ end
48
+
49
+ visit "/"
50
+ within "#alerts" do
51
+ assert !page.has_content?('The Confirmation Code you entered did not match the QR code shown below.')
52
+ end
40
53
  end
41
54
 
42
55
  test "a user should not be able enable their OTP authentication with a blank confirmation code" do
@@ -50,6 +63,10 @@ class EnableOtpFormTest < ActionDispatch::IntegrationTest
50
63
 
51
64
  assert page.has_content?("To Enable Two-Factor Authentication")
52
65
 
66
+ within "#alerts" do
67
+ assert page.has_content? 'The Confirmation Code you entered did not match the QR code shown below.'
68
+ end
69
+
53
70
  user.reload
54
71
  assert_not user.otp_enabled?
55
72
  end
@@ -36,6 +36,9 @@ class PersistenceTest < ActionDispatch::IntegrationTest
36
36
 
37
37
  click_link("Trust this browser")
38
38
  assert_text "Your browser is trusted."
39
+ within "#alerts" do
40
+ assert page.has_content? 'Your device is now trusted.'
41
+ end
39
42
  sign_out
40
43
 
41
44
  sign_user_in
@@ -60,6 +60,15 @@ class RefreshTest < ActionDispatch::IntegrationTest
60
60
  fill_in "user_refresh_password", with: "12345670"
61
61
  click_button "Continue..."
62
62
  assert_equal refresh_user_otp_credential_path, current_path
63
+
64
+ within "#alerts" do
65
+ assert page.has_content? 'Sorry, you provided the wrong credentials.'
66
+ end
67
+
68
+ visit "/"
69
+ within "#alerts" do
70
+ assert !page.has_content?('Sorry, you provided the wrong credentials.')
71
+ end
63
72
  end
64
73
 
65
74
  test "user should be finally be able to access their settings, and just password is enough" do
@@ -23,6 +23,9 @@ class ResetTokenTest < ActionDispatch::IntegrationTest
23
23
  reset_otp
24
24
 
25
25
  assert_equal "/users/otp/token/edit", current_path
26
+ within "#alerts" do
27
+ assert page.has_content? 'Your token secret has been reset. Please confirm your new token secret below.'
28
+ end
26
29
  end
27
30
 
28
31
  test "generates new token secrets" do
@@ -43,6 +43,7 @@ class SignInTest < ActionDispatch::IntegrationTest
43
43
  click_button "Submit Token"
44
44
 
45
45
  assert_equal user_otp_credential_path, current_path
46
+ assert page.has_content? "The token you provided was invalid."
46
47
  end
47
48
 
48
49
  test "fail blank token authentication" do
@@ -53,6 +54,7 @@ class SignInTest < ActionDispatch::IntegrationTest
53
54
  click_button "Submit Token"
54
55
 
55
56
  assert_equal user_otp_credential_path, current_path
57
+ assert page.has_content? "You need to type in the token you generated with your device."
56
58
  end
57
59
 
58
60
  test "successful token authentication" do
@@ -78,4 +80,32 @@ class SignInTest < ActionDispatch::IntegrationTest
78
80
  User.otp_authentication_timeout = old_timeout
79
81
  assert_equal new_user_session_path, current_path
80
82
  end
83
+
84
+ test "blank token flash message does not persist to successful authentication redirect." do
85
+ user = enable_otp_and_sign_in
86
+
87
+ fill_in "token", with: "123456"
88
+ click_button "Submit Token"
89
+
90
+ assert page.has_content?("The token you provided was invalid.")
91
+
92
+ fill_in "token", with: ROTP::TOTP.new(user.otp_auth_secret).at(Time.now)
93
+ click_button "Submit Token"
94
+
95
+ assert !page.has_content?("The token you provided was invalid.")
96
+ end
97
+
98
+ test "invalid token flash message does not persist to successful authentication redirect." do
99
+ user = enable_otp_and_sign_in
100
+
101
+ fill_in "token", with: ""
102
+ click_button "Submit Token"
103
+
104
+ assert page.has_content?("You need to type in the token you generated with your device.")
105
+
106
+ fill_in "token", with: ROTP::TOTP.new(user.otp_auth_secret).at(Time.now)
107
+ click_button "Submit Token"
108
+
109
+ assert !page.has_content?("You need to type in the token you generated with your device.")
110
+ end
81
111
  end