devise-otp 0.8.0 → 1.0.0

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.
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