authentication-zero 2.16.20 → 2.16.21

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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/CI.yml +6 -6
  3. data/CHANGELOG.md +5 -0
  4. data/Gemfile.lock +1 -1
  5. data/README.md +3 -2
  6. data/lib/authentication_zero/version.rb +1 -1
  7. data/lib/generators/authentication/authentication_generator.rb +58 -5
  8. data/lib/generators/authentication/templates/config/initializers/webauthn.rb +4 -0
  9. data/lib/generators/authentication/templates/controllers/html/application_controller.rb.tt +1 -1
  10. data/lib/generators/authentication/templates/controllers/html/sessions_controller.rb.tt +1 -1
  11. data/lib/generators/authentication/templates/controllers/html/two_factor_authentication/challenge/recovery_codes_controller.rb.tt +30 -0
  12. data/lib/generators/authentication/templates/controllers/html/two_factor_authentication/challenge/security_keys_controller.rb.tt +46 -0
  13. data/lib/generators/authentication/templates/controllers/html/two_factor_authentication/challenge/totps_controller.rb.tt +32 -0
  14. data/lib/generators/authentication/templates/controllers/html/two_factor_authentication/{recovery_codes_controller.rb.tt → profile/recovery_codes_controller.rb.tt} +2 -2
  15. data/lib/generators/authentication/templates/controllers/html/two_factor_authentication/profile/security_keys_controller.rb.tt +62 -0
  16. data/lib/generators/authentication/templates/controllers/html/two_factor_authentication/{totps_controller.rb.tt → profile/totps_controller.rb.tt} +3 -3
  17. data/lib/generators/authentication/templates/erb/home/index.html.erb.tt +5 -2
  18. data/lib/generators/authentication/templates/erb/two_factor_authentication/{challenges/_recovery_code_form.html.erb.tt → challenge/recovery_codes/new.html.erb.tt} +3 -3
  19. data/lib/generators/authentication/templates/erb/two_factor_authentication/challenge/security_keys/new.html.erb.tt +12 -0
  20. data/lib/generators/authentication/templates/erb/two_factor_authentication/challenge/totps/new.html.erb.tt +26 -0
  21. data/lib/generators/authentication/templates/erb/two_factor_authentication/{recovery_codes → profile/recovery_codes}/index.html.erb.tt +1 -1
  22. data/lib/generators/authentication/templates/erb/two_factor_authentication/profile/security_keys/_form_confirm.html.erb.tt +13 -0
  23. data/lib/generators/authentication/templates/erb/two_factor_authentication/profile/security_keys/_form_edit.html.erb.tt +17 -0
  24. data/lib/generators/authentication/templates/erb/two_factor_authentication/profile/security_keys/_security_key.html.erb.tt +1 -0
  25. data/lib/generators/authentication/templates/erb/two_factor_authentication/profile/security_keys/edit.html.erb.tt +3 -0
  26. data/lib/generators/authentication/templates/erb/two_factor_authentication/profile/security_keys/index.html.erb.tt +10 -0
  27. data/lib/generators/authentication/templates/erb/two_factor_authentication/profile/security_keys/new.html.erb.tt +12 -0
  28. data/lib/generators/authentication/templates/erb/two_factor_authentication/{totps → profile/totps}/new.html.erb.tt +1 -1
  29. data/lib/generators/authentication/templates/javascript/controllers/application.js.tt +15 -0
  30. data/lib/generators/authentication/templates/migrations/create_security_keys_migration.rb.tt +13 -0
  31. data/lib/generators/authentication/templates/migrations/create_users_migration.rb.tt +3 -0
  32. data/lib/generators/authentication/templates/models/security_key.rb.tt +3 -0
  33. data/lib/generators/authentication/templates/models/user.rb.tt +8 -0
  34. metadata +24 -11
  35. data/lib/generators/authentication/templates/controllers/html/two_factor_authentication/challenges_controller.rb.tt +0 -52
  36. data/lib/generators/authentication/templates/erb/two_factor_authentication/challenges/_totp_form.html.erb.tt +0 -19
  37. data/lib/generators/authentication/templates/erb/two_factor_authentication/challenges/new.html.erb.tt +0 -7
  38. /data/lib/generators/authentication/templates/erb/two_factor_authentication/{recovery_codes → profile/recovery_codes}/_recovery_code.html.erb.tt +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9e14fa9b399611bdbb9d3a01b3f81408fa45983d06b989d92da3669a8b777476
4
- data.tar.gz: 95396bf303a6454d7d6605cbe53e41423ba2113bf9635ebbf2e177ca0f17c1f4
3
+ metadata.gz: c87cbd4d780d1170998c49949d6f28dbe0305df16880f012a7fd6a9cd19ce698
4
+ data.tar.gz: bb6dbd332c8d4fc221d4de3e8264204d0c999f1707f7529b3d547cb2eb4f5fe4
5
5
  SHA512:
6
- metadata.gz: 4c2f8259a291ab4fa9fdb91521b3e8cf6ed03d678aa72ce148302f940575d7107132e7c71aea4aacf1f195b22ac500d528237add10a1db90853c8bd1b643f8f0
7
- data.tar.gz: 01f856238d22656fcac3c71708ca36c69e8b5d44356934b4e021a84409f49dd48a432fcf3f3d58e383d3466be001b9f9d3d8558c368dc1d59bee28fccf1e54b0
6
+ metadata.gz: dc453d93036ca66a4df78da04c2fb1da2a21e862a409bad8608ec1661b315c9670e126b5de14ccd1b0a6b4fedb0445f258b0a472cb6f81c90ef588a178484cfc
7
+ data.tar.gz: c1d014b975a078302d919ba3ec52b4b2b4f0c94017cb04178785ae28bb5e5c8ff6e5d5e2fb84a8dc551cf3b5dc757c392af3061718ecf4e3a64c176292faaad2
@@ -26,6 +26,9 @@ jobs:
26
26
  - name: Install Rubocop
27
27
  run: gem install rubocop rubocop-performance rubocop-minitest rubocop-packaging rubocop-minitest rubocop-rails
28
28
 
29
+ - name: Install Brakeman
30
+ run: gem install brakeman
31
+
29
32
  - name: Create fresh Rails app and run generator
30
33
  run: |
31
34
  rails new test-app
@@ -39,9 +42,6 @@ jobs:
39
42
  - name: Rubocop
40
43
  run: cd test-app && rubocop
41
44
 
42
- - name: Install Brakeman
43
- run: gem install brakeman
44
-
45
45
  - name: Brakeman
46
46
  run: cd test-app && brakeman
47
47
 
@@ -69,6 +69,9 @@ jobs:
69
69
  - name: Install Rubocop
70
70
  run: gem install rubocop rubocop-performance rubocop-minitest rubocop-packaging rubocop-minitest rubocop-rails
71
71
 
72
+ - name: Install Brakeman
73
+ run: gem install brakeman
74
+
72
75
  - name: Create fresh Rails app and run generator
73
76
  run: |
74
77
  rails new test-app
@@ -82,9 +85,6 @@ jobs:
82
85
  - name: Rubocop
83
86
  run: cd test-app && rubocop
84
87
 
85
- - name: Install Brakeman
86
- run: gem install brakeman
87
-
88
88
  - name: Brakeman
89
89
  run: cd test-app && brakeman
90
90
 
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## Authentication Zero 2.16.21 ##
2
+
3
+ * Add two factor authentication using a hardware security key (--webauthn)
4
+ * Move two factor authentication to new namespaces
5
+
1
6
  ## Authentication Zero 2.16.18 ##
2
7
 
3
8
  * Use session to store the token for the 2fa challenge
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- authentication-zero (2.16.20)
4
+ authentication-zero (2.16.21)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -20,7 +20,7 @@ Since Authentication Zero generates this code into your application instead of b
20
20
 
21
21
  ## Features
22
22
 
23
- - **Simplest code ever (~200 lines of code)**
23
+ - **Simple code**
24
24
  - **Inspired by hey.com**
25
25
  - Sign up
26
26
  - Email and password validations
@@ -29,6 +29,7 @@ Since Authentication Zero generates this code into your application instead of b
29
29
  - Authentication by token (--api)
30
30
  - Passwordless authentication (--passwordless)
31
31
  - Two factor authentication + recovery codes (--two-factor)
32
+ - Two factor authentication using a hardware security key (--webauthn)
32
33
  - Social Login with OmniAuth (--omniauthable)
33
34
  - Send invitations (--invitable)
34
35
  - Sign-in as button functionallity (--masqueradable)
@@ -36,7 +37,7 @@ Since Authentication Zero generates this code into your application instead of b
36
37
  - Ask password before sensitive data changes, aka: sudo (--sudoable)
37
38
  - Reset the user password and send reset instructions
38
39
  - Reset the user password only from verified emails
39
- - Lock mechanism to prevent spamming (--lockable)
40
+ - Lock mechanism to prevent email bombing (--lockable)
40
41
  - Rate limiting for your app, 1000 reqs/minute (--ratelimit)
41
42
  - Send e-mail confirmation when your email has been changed
42
43
  - Manage multiple sessions & devices
@@ -1,3 +1,3 @@
1
1
  module AuthenticationZero
2
- VERSION = "2.16.20"
2
+ VERSION = "2.16.21"
3
3
  end
@@ -12,6 +12,7 @@ class AuthenticationGenerator < Rails::Generators::Base
12
12
  class_option :omniauthable, type: :boolean, desc: "Add social login support"
13
13
  class_option :trackable, type: :boolean, desc: "Add activity log support"
14
14
  class_option :two_factor, type: :boolean, desc: "Add two factor authentication"
15
+ class_option :webauthn, type: :boolean, desc: "Add two factor authentication using a hardware security key"
15
16
  class_option :invitable, type: :boolean, desc: "Add sending invitations"
16
17
  class_option :masqueradable, type: :boolean, desc: "Add sign-in as button functionallity"
17
18
 
@@ -42,6 +43,10 @@ class AuthenticationGenerator < Rails::Generators::Base
42
43
  gem "rotp", comment: "Use rotp for generating and validating one time passwords [https://github.com/mdp/rotp]"
43
44
  gem "rqrcode", comment: "Use rqrcode for creating and rendering QR codes into various formats [https://github.com/whomwah/rqrcode]"
44
45
  end
46
+
47
+ if webauthn?
48
+ gem "webauthn", comment: "Use webauthn for making rails become a conformant web authn relying party [https://github.com/cedarcode/webauthn-ruby]"
49
+ end
45
50
  end
46
51
 
47
52
  def add_environment_configurations
@@ -53,6 +58,7 @@ class AuthenticationGenerator < Rails::Generators::Base
53
58
  def create_configuration_files
54
59
  copy_file "config/redis/shared.yml", "config/redis/shared.yml" if redis?
55
60
  copy_file "config/initializers/omniauth.rb", "config/initializers/omniauth.rb" if omniauthable?
61
+ copy_file "config/initializers/webauthn.rb", "config/initializers/webauthn.rb" if webauthn?
56
62
  end
57
63
 
58
64
  def create_migrations
@@ -63,6 +69,7 @@ class AuthenticationGenerator < Rails::Generators::Base
63
69
  migration_template "migrations/create_sign_in_tokens_migration.rb", "#{db_migrate_path}/create_sign_in_tokens_migration.rb" if passwordless?
64
70
  migration_template "migrations/create_events_migration.rb", "#{db_migrate_path}/create_events.rb" if options.trackable?
65
71
  migration_template "migrations/create_recovery_codes_migration.rb", "#{db_migrate_path}/create_recovery_codes.rb" if two_factor?
72
+ migration_template "migrations/create_security_keys_migration.rb", "#{db_migrate_path}/create_security_keys.rb" if webauthn?
66
73
  end
67
74
 
68
75
  def create_models
@@ -74,6 +81,7 @@ class AuthenticationGenerator < Rails::Generators::Base
74
81
  template "models/current.rb", "app/models/current.rb"
75
82
  template "models/event.rb", "app/models/event.rb" if options.trackable?
76
83
  template "models/recovery_code.rb", "app/models/recovery_code.rb" if two_factor?
84
+ template "models/security_key.rb", "app/models/security_key.rb" if webauthn?
77
85
  end
78
86
 
79
87
  def create_fixture_file
@@ -84,7 +92,17 @@ class AuthenticationGenerator < Rails::Generators::Base
84
92
  template "controllers/#{format_folder}/application_controller.rb", "app/controllers/application_controller.rb", force: true
85
93
 
86
94
  directory "controllers/#{format_folder}/identity", "app/controllers/identity"
87
- directory "controllers/#{format_folder}/two_factor_authentication", "app/controllers/two_factor_authentication" if two_factor?
95
+
96
+ if two_factor?
97
+ template "controllers/html/two_factor_authentication/profile/totps_controller.rb", "app/controllers/two_factor_authentication/profile/totps_controller.rb"
98
+ template "controllers/html/two_factor_authentication/profile/recovery_codes_controller.rb", "app/controllers/two_factor_authentication/profile/recovery_codes_controller.rb"
99
+ template "controllers/html/two_factor_authentication/profile/security_keys_controller.rb", "app/controllers/two_factor_authentication/profile/security_keys_controller.rb" if webauthn?
100
+
101
+ template "controllers/html/two_factor_authentication/challenge/totps_controller.rb", "app/controllers/two_factor_authentication/challenge/totps_controller.rb"
102
+ template "controllers/html/two_factor_authentication/challenge/recovery_codes_controller.rb", "app/controllers/two_factor_authentication/challenge/recovery_codes_controller.rb"
103
+ template "controllers/html/two_factor_authentication/challenge/security_keys_controller.rb", "app/controllers/two_factor_authentication/challenge/security_keys_controller.rb" if webauthn?
104
+ end
105
+
88
106
  template "controllers/#{format_folder}/sessions_controller.rb", "app/controllers/sessions_controller.rb"
89
107
  template "controllers/#{format_folder}/passwords_controller.rb", "app/controllers/passwords_controller.rb"
90
108
  template "controllers/#{format_folder}/invitations_controller.rb", "app/controllers/invitations_controller.rb" if invitable?
@@ -97,6 +115,16 @@ class AuthenticationGenerator < Rails::Generators::Base
97
115
  template "controllers/#{format_folder}/authentications/events_controller.rb", "app/controllers/authentications/events_controller.rb" if options.trackable?
98
116
  end
99
117
 
118
+ def install_javascript_dependencies
119
+ return if options.api?
120
+ template "javascript/controllers/application.js", "app/javascript/controllers/application.js"
121
+
122
+ if webauthn?
123
+ run "bin/importmap pin stimulus-web-authn" if importmaps?
124
+ run "yarn add stimulus-web-authn" if node?
125
+ end
126
+ end
127
+
100
128
  def create_views
101
129
  if options.api?
102
130
  template "erb/user_mailer/email_verification.html.erb", "app/views/user_mailer/email_verification.html.erb"
@@ -122,8 +150,17 @@ class AuthenticationGenerator < Rails::Generators::Base
122
150
 
123
151
  directory "erb/sessions/passwordlesses", "app/views/sessions/passwordlesses" if passwordless?
124
152
 
125
- directory "erb/two_factor_authentication", "app/views/two_factor_authentication" if two_factor?
126
153
  directory "erb/authentications/events", "app/views/authentications/events" if options.trackable?
154
+
155
+ if two_factor?
156
+ directory "erb/two_factor_authentication/profile/totps", "app/views/two_factor_authentication/profile/totps"
157
+ directory "erb/two_factor_authentication/profile/recovery_codes", "app/views/two_factor_authentication/profile/recovery_codes"
158
+ directory "erb/two_factor_authentication/profile/security_keys", "app/views/two_factor_authentication/profile/security_keys" if webauthn?
159
+
160
+ directory "erb/two_factor_authentication/challenge/totps", "app/views/two_factor_authentication/challenge/totps"
161
+ directory "erb/two_factor_authentication/challenge/recovery_codes", "app/views/two_factor_authentication/challenge/recovery_codes"
162
+ directory "erb/two_factor_authentication/challenge/security_keys", "app/views/two_factor_authentication/challenge/security_keys" if webauthn?
163
+ end
127
164
  end
128
165
  end
129
166
 
@@ -157,9 +194,13 @@ class AuthenticationGenerator < Rails::Generators::Base
157
194
  end
158
195
 
159
196
  if two_factor?
160
- route "resources :recovery_codes, only: [:index, :create]", namespace: :two_factor_authentication
161
- route "resource :totp, only: [:new, :create]", namespace: :two_factor_authentication
162
- route "resource :challenge, only: [:new, :create]", namespace: :two_factor_authentication
197
+ route "resources :recovery_codes, only: [:index, :create]", namespace: [:two_factor_authentication, :profile]
198
+ route "resource :totp, only: [:new, :create]", namespace: [:two_factor_authentication, :profile]
199
+ route "resources :security_keys", namespace: [:two_factor_authentication, :profile] if webauthn?
200
+
201
+ route "resource :recovery_codes, only: [:new, :create]", namespace: [:two_factor_authentication, :challenge]
202
+ route "resource :totp, only: [:new, :create]", namespace: [:two_factor_authentication, :challenge]
203
+ route "resource :security_keys, only: [:new, :create]", namespace: [:two_factor_authentication, :challenge] if webauthn?
163
204
  end
164
205
 
165
206
  if options.trackable?
@@ -212,6 +253,10 @@ class AuthenticationGenerator < Rails::Generators::Base
212
253
  options.two_factor? && !options.api?
213
254
  end
214
255
 
256
+ def webauthn?
257
+ options.webauthn? && two_factor?
258
+ end
259
+
215
260
  def invitable?
216
261
  options.invitable? && !options.api?
217
262
  end
@@ -227,4 +272,12 @@ class AuthenticationGenerator < Rails::Generators::Base
227
272
  def redis?
228
273
  options.lockable? || options.ratelimit? || sudoable?
229
274
  end
275
+
276
+ def importmaps?
277
+ Rails.root.join("config/importmap.rb").exist?
278
+ end
279
+
280
+ def node?
281
+ Rails.root.join("package.json").exist?
282
+ end
230
283
  end
@@ -0,0 +1,4 @@
1
+ WebAuthn.configure do |config|
2
+ config.origin = "http://localhost:3000"
3
+ config.rp_name = "Example Inc."
4
+ end
@@ -28,7 +28,7 @@ class ApplicationController < ActionController::Base
28
28
  <%- if sudoable? %>
29
29
  def require_sudo
30
30
  unless Current.session.sudo?
31
- redirect_to new_sessions_sudo_path(proceed_to_url: request.url)
31
+ redirect_to new_sessions_sudo_path(proceed_to_url: request.original_url)
32
32
  end
33
33
  end
34
34
  <%- end -%>
@@ -18,7 +18,7 @@ class SessionsController < ApplicationController
18
18
  <%- if two_factor? -%>
19
19
  if user.otp_secret.present?
20
20
  session[:challenge_token] = user.signed_id(purpose: :authentication_challenge, expires_in: 20.minutes)
21
- redirect_to new_two_factor_authentication_challenge_path
21
+ redirect_to new_two_factor_authentication_challenge_totp_path
22
22
  else
23
23
  @session = user.sessions.create!
24
24
  cookies.signed.permanent[:session_token] = { value: @session.id, httponly: true }
@@ -0,0 +1,30 @@
1
+ class TwoFactorAuthentication::Challenge::RecoveryCodesController < ApplicationController
2
+ skip_before_action :authenticate
3
+
4
+ before_action :set_user
5
+
6
+ def new
7
+ end
8
+
9
+ def create
10
+ if recover_code = @user.recovery_codes.find_by(code: params[:code], used: false)
11
+ recover_code.update!(used: true); sign_in_and_redirect_to_root
12
+ else
13
+ redirect_to new_two_factor_authentication_challenge_recovery_codes_path, alert: "That code didn't work. Please try again"
14
+ end
15
+ end
16
+
17
+ private
18
+ def set_user
19
+ @user = User.find_signed!(session[:challenge_token], purpose: :authentication_challenge)
20
+ rescue StandardError
21
+ redirect_to sign_in_path, alert: "That's taking too long. Please re-enter your password and try again"
22
+ end
23
+
24
+ def sign_in_and_redirect_to_root
25
+ session = @user.sessions.create!
26
+ cookies.signed.permanent[:session_token] = { value: session.id, httponly: true }
27
+
28
+ redirect_to root_path, notice: "Signed in successfully"
29
+ end
30
+ end
@@ -0,0 +1,46 @@
1
+ class TwoFactorAuthentication::Challenge::SecurityKeysController < ApplicationController
2
+ skip_before_action :authenticate
3
+
4
+ before_action :set_user
5
+
6
+ def new
7
+ respond_to do |format|
8
+ format.html
9
+ format.json { render json: options_for_get }
10
+ end
11
+ end
12
+
13
+ def create
14
+ if @user.security_keys.exists?(external_id: credential.id)
15
+ sign_in_and_redirect_to_root
16
+ else
17
+ render json: { error: "Verification failed: #{e.message}" }, status: :unprocessable_entity
18
+ end
19
+ end
20
+
21
+ private
22
+ def set_user
23
+ @user = User.find_signed!(session[:challenge_token], purpose: :authentication_challenge)
24
+ rescue StandardError
25
+ redirect_to sign_in_path, alert: "That's taking too long. Please re-enter your password and try again"
26
+ end
27
+
28
+ def sign_in_and_redirect_to_root
29
+ session = @user.sessions.create!
30
+ cookies.signed.permanent[:session_token] = { value: session.id, httponly: true }
31
+
32
+ render json: { status: "ok", location: root_url }, status: :created
33
+ end
34
+
35
+ def options_for_get
36
+ WebAuthn::Credential.options_for_get(allow: external_ids)
37
+ end
38
+
39
+ def external_ids
40
+ @user.security_keys.pluck(:external_id)
41
+ end
42
+
43
+ def credential
44
+ @credential ||= WebAuthn::Credential.from_get(params.require(:credential))
45
+ end
46
+ end
@@ -0,0 +1,32 @@
1
+ class TwoFactorAuthentication::Challenge::TotpsController < ApplicationController
2
+ skip_before_action :authenticate
3
+
4
+ before_action :set_user
5
+
6
+ def new
7
+ end
8
+
9
+ def create
10
+ @totp = ROTP::TOTP.new(@user.otp_secret, issuer: "YourAppName")
11
+
12
+ if @totp.verify(params[:code], drift_behind: 15)
13
+ sign_in_and_redirect_to_root
14
+ else
15
+ redirect_to new_two_factor_authentication_challenge_totp_path, alert: "That code didn't work. Please try again"
16
+ end
17
+ end
18
+
19
+ private
20
+ def set_user
21
+ @user = User.find_signed!(session[:challenge_token], purpose: :authentication_challenge)
22
+ rescue StandardError
23
+ redirect_to sign_in_path, alert: "That's taking too long. Please re-enter your password and try again"
24
+ end
25
+
26
+ def sign_in_and_redirect_to_root
27
+ session = @user.sessions.create!
28
+ cookies.signed.permanent[:session_token] = { value: session.id, httponly: true }
29
+
30
+ redirect_to root_path, notice: "Signed in successfully"
31
+ end
32
+ end
@@ -1,4 +1,4 @@
1
- class TwoFactorAuthentication::RecoveryCodesController < ApplicationController
1
+ class TwoFactorAuthentication::Profile::RecoveryCodesController < ApplicationController
2
2
  before_action :set_user
3
3
 
4
4
  def index
@@ -13,7 +13,7 @@ class TwoFactorAuthentication::RecoveryCodesController < ApplicationController
13
13
  @user.recovery_codes.delete_all
14
14
  @user.recovery_codes.create!(new_recovery_codes)
15
15
 
16
- redirect_to two_factor_authentication_recovery_codes_path, notice: "Your new recovery codes have been generated"
16
+ redirect_to two_factor_authentication_profile_recovery_codes_path, notice: "Your new recovery codes have been generated"
17
17
  end
18
18
 
19
19
  private
@@ -0,0 +1,62 @@
1
+ class TwoFactorAuthentication::Profile::SecurityKeysController < ApplicationController
2
+ before_action :set_user
3
+ before_action :set_security_key, only: %i[ edit update destroy ]
4
+
5
+ def index
6
+ @security_keys = @user.security_keys
7
+ end
8
+
9
+ def new
10
+ respond_to do |format|
11
+ format.html
12
+ format.json { render json: options_for_create }
13
+ end
14
+ end
15
+
16
+ def edit
17
+ end
18
+
19
+ def create
20
+ @security_key = @user.security_keys.create!(credential_params)
21
+ render json: { status: "ok", location: edit_two_factor_authentication_profile_security_key_url(@security_key, confirmation: true) }, status: :created
22
+ end
23
+
24
+ def update
25
+ @security_key.update! name: params[:name]
26
+ redirect_to two_factor_authentication_profile_security_keys_path, notice: "Your changes have been saved"
27
+ end
28
+
29
+ def destroy
30
+ @security_key.destroy
31
+ redirect_to two_factor_authentication_profile_security_keys_path, notice: "#{@security_key.name} has been removed"
32
+ end
33
+
34
+ private
35
+ def set_user
36
+ @user = Current.user
37
+ end
38
+
39
+ def set_security_key
40
+ @security_key = @user.security_keys.find(params[:id])
41
+ end
42
+
43
+ def options_for_create
44
+ WebAuthn::Credential.options_for_create(user: user_info, exclude: external_ids)
45
+ end
46
+
47
+ def user_info
48
+ { id: @user.webauthn_id, name: @user.email }
49
+ end
50
+
51
+ def external_ids
52
+ @user.security_keys.pluck(:external_id)
53
+ end
54
+
55
+ def credential_params
56
+ { external_id: credential.id, name: "security key" }
57
+ end
58
+
59
+ def credential
60
+ @credential ||= WebAuthn::Credential.from_create(params.require(:credential))
61
+ end
62
+ end
@@ -1,4 +1,4 @@
1
- class TwoFactorAuthentication::TotpsController < ApplicationController
1
+ class TwoFactorAuthentication::Profile::TotpsController < ApplicationController
2
2
  before_action :set_user
3
3
  before_action :set_totp
4
4
 
@@ -9,9 +9,9 @@ class TwoFactorAuthentication::TotpsController < ApplicationController
9
9
  def create
10
10
  if @totp.verify(params[:code], drift_behind: 15)
11
11
  @user.update! otp_secret: params[:secret]
12
- redirect_to two_factor_authentication_recovery_codes_path
12
+ redirect_to two_factor_authentication_profile_recovery_codes_path
13
13
  else
14
- redirect_to new_two_factor_authentication_totp_path, alert: "That code didn't work. Please try again"
14
+ redirect_to new_two_factor_authentication_profile_totp_path, alert: "That code didn't work. Please try again"
15
15
  end
16
16
  end
17
17
 
@@ -13,11 +13,14 @@
13
13
  </div>
14
14
  <%- if two_factor? %>
15
15
  <div>
16
- <%%= link_to "Two-Factor Authentication", new_two_factor_authentication_totp_path %>
16
+ <%%= link_to "Two-Factor Authentication", new_two_factor_authentication_profile_totp_path %>
17
17
  </div>
18
18
 
19
19
  <%% if Current.user.otp_secret.present? %>
20
- <div><%%= link_to "Recovery Codes", two_factor_authentication_recovery_codes_path %></div>
20
+ <div><%%= link_to "Recovery Codes", two_factor_authentication_profile_recovery_codes_path %></div>
21
+ <%- if webauthn? -%>
22
+ <div><%%= link_to "Security keys", two_factor_authentication_profile_security_keys_path %></div>
23
+ <%- end -%>
21
24
  <%% end %>
22
25
  <%- end -%>
23
26
  <%- if invitable? %>
@@ -1,6 +1,6 @@
1
- <%%= form_with(url: two_factor_authentication_challenge_path) do |form| %>
2
- <%%= form.hidden_field :scheme_type, value: "recovery_codes" %>
1
+ <p style="color: red"><%%= alert %></p>
3
2
 
3
+ <%%= form_with(url: two_factor_authentication_challenge_recovery_codes_path) do |form| %>
4
4
  <div>
5
5
  <%%= form.label :code do %>
6
6
  <h1>OK, enter one of your recovery codes below:</h1>
@@ -14,5 +14,5 @@
14
14
  <%% end %>
15
15
 
16
16
  <div>
17
- <p>To access your account, enter one of the recovery codes (e.g., XXXXX-XXXXX) you saved when you set up your two-factor authentication device.</p>
17
+ <p>To access your account, enter one of the recovery codes (e.g., xxxxxxxxxx) you saved when you set up your two-factor authentication device.</p>
18
18
  </div>
@@ -0,0 +1,12 @@
1
+ <div data-controller="web-authn"
2
+ data-web-authn-loading-class="web-authn-loading"
3
+ data-web-authn-fallback-url-value="<%%= new_two_factor_authentication_challenge_totp_path %>"
4
+ data-web-authn-challenge-url-value="<%%= new_two_factor_authentication_challenge_security_keys_url %>"
5
+ data-web-authn-verification-url-value="<%%= two_factor_authentication_challenge_security_keys_url %>">
6
+
7
+ <p data-web-authn-target="error" style="color: red" hidden></p>
8
+
9
+ <h1>Verify with your security key.</h1>
10
+ <%%= button_tag "Use security key", type: :button, data: { web_authn_target: "button", action: "web-authn#getCredential" } %>
11
+ <p>Have your security key ready. If it's the USB kind insert it now and then, if it has one, press the activation button when asked.</p>
12
+ <div>
@@ -0,0 +1,26 @@
1
+ <p style="color: red"><%%= alert %></p>
2
+
3
+ <%%= form_with(url: two_factor_authentication_challenge_totp_path) do |form| %>
4
+ <div>
5
+ <%%= form.label :code do %>
6
+ <h1>Next, open the 2FA authenticator app on your phone and type the six digit code below:</h1>
7
+ <%% end %>
8
+ <%%= form.text_field :code, autofocus: true, required: true, autocomplete: :off %>
9
+ </div>
10
+
11
+ <div>
12
+ <%%= form.submit "Verify" %>
13
+ </div>
14
+ <%% end %>
15
+
16
+ <div>
17
+ <p><strong>Don't have your phone?</strong></p>
18
+ <div>
19
+ <%%= link_to "Use a recovery code to access your account.", new_two_factor_authentication_challenge_recovery_codes_path %>
20
+ </div>
21
+ <%- if webauthn? %>
22
+ <%% if @user.security_keys.exists? %>
23
+ <div><%%= link_to "Use a security key to access your account.", new_two_factor_authentication_challenge_security_keys_path %></div>
24
+ <%% end %>
25
+ <%- end -%>
26
+ </div>
@@ -13,4 +13,4 @@
13
13
 
14
14
  <p>If you think your codes have fallen into the wrong hands, you can get a new set. Be sure to save the new ones because the old codes will stop working.</p>
15
15
 
16
- <%%= button_to "Generate new recovery codes", two_factor_authentication_recovery_codes_path %>
16
+ <%%= button_to "Generate new recovery codes", two_factor_authentication_profile_recovery_codes_path %>
@@ -0,0 +1,13 @@
1
+ <%%= form_with(url: two_factor_authentication_profile_security_key_path, method: :patch) do |form| %>
2
+ <p>One more step. Please give this security key a nickname to help you remember it.</p>
3
+
4
+ <div>
5
+ <%%= form.label :name, style: "display: block" %>
6
+ <%%= form.text_field :name, autofocus: true, required: true %>
7
+ <div>e.g., Macbook Touch ID</div>
8
+ </div>
9
+
10
+ <div>
11
+ <%%= form.submit "Save" %>
12
+ </div>
13
+ <%% end %>
@@ -0,0 +1,17 @@
1
+ <%%= form_with(url: two_factor_authentication_profile_security_key_path, method: :patch) do |form| %>
2
+ <div>
3
+ <%%= form.label :name, style: "display: block" %>
4
+ <%%= form.text_field :name, value: @security_key.name, autofocus: true, required: true %>
5
+ <div>e.g., Macbook Touch ID</div>
6
+ </div>
7
+
8
+ <div>
9
+ <%%= form.submit "Save changes" %>
10
+ </div>
11
+ <%% end %>
12
+
13
+ <br>
14
+
15
+ <div>
16
+ <%%= button_to "Remove this security key...", two_factor_authentication_profile_security_key_path(@security_key), method: :delete %>
17
+ </div>
@@ -0,0 +1 @@
1
+ <li><%%= link_to security_key.name, edit_two_factor_authentication_profile_security_key_path(security_key) %></li>
@@ -0,0 +1,3 @@
1
+ <h1>Edit security key</h1>
2
+
3
+ <%%= render params[:confirmation] ? "form_confirm" : "form_edit" %>
@@ -0,0 +1,10 @@
1
+ <p style="color: green"><%%= notice %></p>
2
+
3
+ <h1>Security keys</h1>
4
+ <p>A security key is a hardware device used to verify your identity. For example, a built-in fingerprint reader, a plug-in USB key, or a login system like Windows Hello.</p>
5
+
6
+ <ul><%%= render @security_keys %></ul>
7
+
8
+ <br>
9
+
10
+ <%%= link_to "Add security key", new_two_factor_authentication_profile_security_key_path %>
@@ -0,0 +1,12 @@
1
+ <div data-controller="web-authn"
2
+ data-web-authn-loading-class="web-authn-loading"
3
+ data-web-authn-challenge-url-value="<%%= new_two_factor_authentication_profile_security_key_url %>"
4
+ data-web-authn-verification-url-value="<%%= two_factor_authentication_profile_security_keys_url %>">
5
+
6
+ <p data-web-authn-target="error" style="color: red" hidden></p>
7
+
8
+ <h1>Add a security key</h1>
9
+ <p>Have your security key ready. If it's the USB kind insert it now and then, if it has one, press the activation button when asked.</p>
10
+
11
+ <%%= button_tag "I'm ready, let's go", type: :button, data: { web_authn_target: "button", action: "web-authn#createCredential" } %>
12
+ </div>
@@ -14,7 +14,7 @@
14
14
  <figcaption>Point your camera here</figcaption>
15
15
  </figure>
16
16
 
17
- <%%= form_with(url: two_factor_authentication_totp_path) do |form| %>
17
+ <%%= form_with(url: two_factor_authentication_profile_totp_path) do |form| %>
18
18
  <%%= form.hidden_field :secret, value: @totp.secret %>
19
19
 
20
20
  <div>
@@ -0,0 +1,15 @@
1
+ import { Application } from "@hotwired/stimulus"
2
+ <%- if webauthn? -%>
3
+ import WebAuthnController from "stimulus-web-authn"
4
+ <%- end -%>
5
+
6
+ const application = Application.start()
7
+ <%- if webauthn? -%>
8
+ application.register("web-authn", WebAuthnController)
9
+ <%- end -%>
10
+
11
+ // Configure Stimulus development experience
12
+ application.debug = false
13
+ window.Stimulus = application
14
+
15
+ export { application }
@@ -0,0 +1,13 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ def change
3
+ create_table :security_keys do |t|
4
+ t.references :user, null: false, foreign_key: true
5
+ t.string :name, null: false
6
+ t.string :external_id, null: false
7
+
8
+ t.timestamps
9
+ end
10
+
11
+ add_index :security_keys, :external_id, unique: true
12
+ end
13
+ end
@@ -12,6 +12,9 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi
12
12
  t.string :provider
13
13
  t.string :uid
14
14
  <%- end -%>
15
+ <%- if webauthn? %>
16
+ t.string :webauthn_id
17
+ <%- end -%>
15
18
 
16
19
  t.timestamps
17
20
  end
@@ -0,0 +1,3 @@
1
+ class SecurityKey < ApplicationRecord
2
+ belongs_to :user
3
+ end
@@ -7,6 +7,9 @@ class User < ApplicationRecord
7
7
  <%- if two_factor? -%>
8
8
  has_many :recovery_codes, dependent: :destroy
9
9
  <%- end -%>
10
+ <%- if webauthn? -%>
11
+ has_many :security_keys, dependent: :destroy
12
+ <%- end -%>
10
13
  <%- if passwordless? -%>
11
14
  has_many :sign_in_tokens, dependent: :destroy
12
15
  <%- end -%>
@@ -27,6 +30,11 @@ class User < ApplicationRecord
27
30
  before_validation if: :email_changed?, on: :update do
28
31
  self.verified = false
29
32
  end
33
+ <%- if webauthn? %>
34
+ before_validation on: :create do
35
+ self.webauthn_id = WebAuthn.generate_user_id
36
+ end
37
+ <%- end -%>
30
38
 
31
39
  after_update if: :password_digest_previously_changed? do
32
40
  sessions.where.not(id: Current.session).delete_all
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: authentication-zero
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.16.20
4
+ version: 2.16.21
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nixon
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-04-09 00:00:00.000000000 Z
11
+ date: 2023-04-12 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -36,6 +36,7 @@ files:
36
36
  - lib/generators/authentication/USAGE
37
37
  - lib/generators/authentication/authentication_generator.rb
38
38
  - lib/generators/authentication/templates/config/initializers/omniauth.rb
39
+ - lib/generators/authentication/templates/config/initializers/webauthn.rb
39
40
  - lib/generators/authentication/templates/config/redis/shared.yml
40
41
  - lib/generators/authentication/templates/controllers/api/application_controller.rb.tt
41
42
  - lib/generators/authentication/templates/controllers/api/authentications/events_controller.rb.tt
@@ -59,9 +60,12 @@ files:
59
60
  - lib/generators/authentication/templates/controllers/html/sessions/passwordlesses_controller.rb.tt
60
61
  - lib/generators/authentication/templates/controllers/html/sessions/sudos_controller.rb.tt
61
62
  - lib/generators/authentication/templates/controllers/html/sessions_controller.rb.tt
62
- - lib/generators/authentication/templates/controllers/html/two_factor_authentication/challenges_controller.rb.tt
63
- - lib/generators/authentication/templates/controllers/html/two_factor_authentication/recovery_codes_controller.rb.tt
64
- - lib/generators/authentication/templates/controllers/html/two_factor_authentication/totps_controller.rb.tt
63
+ - lib/generators/authentication/templates/controllers/html/two_factor_authentication/challenge/recovery_codes_controller.rb.tt
64
+ - lib/generators/authentication/templates/controllers/html/two_factor_authentication/challenge/security_keys_controller.rb.tt
65
+ - lib/generators/authentication/templates/controllers/html/two_factor_authentication/challenge/totps_controller.rb.tt
66
+ - lib/generators/authentication/templates/controllers/html/two_factor_authentication/profile/recovery_codes_controller.rb.tt
67
+ - lib/generators/authentication/templates/controllers/html/two_factor_authentication/profile/security_keys_controller.rb.tt
68
+ - lib/generators/authentication/templates/controllers/html/two_factor_authentication/profile/totps_controller.rb.tt
65
69
  - lib/generators/authentication/templates/erb/authentications/events/index.html.erb.tt
66
70
  - lib/generators/authentication/templates/erb/home/index.html.erb.tt
67
71
  - lib/generators/authentication/templates/erb/identity/emails/edit.html.erb.tt
@@ -74,21 +78,29 @@ files:
74
78
  - lib/generators/authentication/templates/erb/sessions/new.html.erb.tt
75
79
  - lib/generators/authentication/templates/erb/sessions/passwordlesses/new.html.erb.tt
76
80
  - lib/generators/authentication/templates/erb/sessions/sudos/new.html.erb.tt
77
- - lib/generators/authentication/templates/erb/two_factor_authentication/challenges/_recovery_code_form.html.erb.tt
78
- - lib/generators/authentication/templates/erb/two_factor_authentication/challenges/_totp_form.html.erb.tt
79
- - lib/generators/authentication/templates/erb/two_factor_authentication/challenges/new.html.erb.tt
80
- - lib/generators/authentication/templates/erb/two_factor_authentication/recovery_codes/_recovery_code.html.erb.tt
81
- - lib/generators/authentication/templates/erb/two_factor_authentication/recovery_codes/index.html.erb.tt
82
- - lib/generators/authentication/templates/erb/two_factor_authentication/totps/new.html.erb.tt
81
+ - lib/generators/authentication/templates/erb/two_factor_authentication/challenge/recovery_codes/new.html.erb.tt
82
+ - lib/generators/authentication/templates/erb/two_factor_authentication/challenge/security_keys/new.html.erb.tt
83
+ - lib/generators/authentication/templates/erb/two_factor_authentication/challenge/totps/new.html.erb.tt
84
+ - lib/generators/authentication/templates/erb/two_factor_authentication/profile/recovery_codes/_recovery_code.html.erb.tt
85
+ - lib/generators/authentication/templates/erb/two_factor_authentication/profile/recovery_codes/index.html.erb.tt
86
+ - lib/generators/authentication/templates/erb/two_factor_authentication/profile/security_keys/_form_confirm.html.erb.tt
87
+ - lib/generators/authentication/templates/erb/two_factor_authentication/profile/security_keys/_form_edit.html.erb.tt
88
+ - lib/generators/authentication/templates/erb/two_factor_authentication/profile/security_keys/_security_key.html.erb.tt
89
+ - lib/generators/authentication/templates/erb/two_factor_authentication/profile/security_keys/edit.html.erb.tt
90
+ - lib/generators/authentication/templates/erb/two_factor_authentication/profile/security_keys/index.html.erb.tt
91
+ - lib/generators/authentication/templates/erb/two_factor_authentication/profile/security_keys/new.html.erb.tt
92
+ - lib/generators/authentication/templates/erb/two_factor_authentication/profile/totps/new.html.erb.tt
83
93
  - lib/generators/authentication/templates/erb/user_mailer/email_verification.html.erb.tt
84
94
  - lib/generators/authentication/templates/erb/user_mailer/invitation_instructions.html.erb.tt
85
95
  - lib/generators/authentication/templates/erb/user_mailer/password_reset.html.erb.tt
86
96
  - lib/generators/authentication/templates/erb/user_mailer/passwordless.html.erb.tt
97
+ - lib/generators/authentication/templates/javascript/controllers/application.js.tt
87
98
  - lib/generators/authentication/templates/mailers/user_mailer.rb.tt
88
99
  - lib/generators/authentication/templates/migrations/create_email_verification_tokens_migration.rb.tt
89
100
  - lib/generators/authentication/templates/migrations/create_events_migration.rb.tt
90
101
  - lib/generators/authentication/templates/migrations/create_password_reset_tokens_migration.rb.tt
91
102
  - lib/generators/authentication/templates/migrations/create_recovery_codes_migration.rb.tt
103
+ - lib/generators/authentication/templates/migrations/create_security_keys_migration.rb.tt
92
104
  - lib/generators/authentication/templates/migrations/create_sessions_migration.rb.tt
93
105
  - lib/generators/authentication/templates/migrations/create_sign_in_tokens_migration.rb.tt
94
106
  - lib/generators/authentication/templates/migrations/create_users_migration.rb.tt
@@ -97,6 +109,7 @@ files:
97
109
  - lib/generators/authentication/templates/models/event.rb.tt
98
110
  - lib/generators/authentication/templates/models/password_reset_token.rb.tt
99
111
  - lib/generators/authentication/templates/models/recovery_code.rb.tt
112
+ - lib/generators/authentication/templates/models/security_key.rb.tt
100
113
  - lib/generators/authentication/templates/models/session.rb.tt
101
114
  - lib/generators/authentication/templates/models/sign_in_token.rb.tt
102
115
  - lib/generators/authentication/templates/models/user.rb.tt
@@ -1,52 +0,0 @@
1
- class TwoFactorAuthentication::ChallengesController < ApplicationController
2
- skip_before_action :authenticate
3
-
4
- before_action :set_user
5
-
6
- def new
7
- end
8
-
9
- def create
10
- if params[:scheme_type] == "recovery_codes"
11
- verify_recovery_code
12
- else
13
- verify_time_based_one_time_password
14
- end
15
- end
16
-
17
- private
18
- def set_user
19
- @user = User.find_signed!(session[:challenge_token], purpose: :authentication_challenge)
20
- rescue StandardError
21
- redirect_to sign_in_path, alert: "That's taking too long. Please re-enter your password and try again"
22
- end
23
-
24
- def verify_recovery_code
25
- if recover_code = @user.recovery_codes.find_by(code: params[:code], used: false)
26
- recover_code.update!(used: true); sign_in_and_redirect_to_root
27
- else
28
- redirect_to_authentication_challenge
29
- end
30
- end
31
-
32
- def verify_time_based_one_time_password
33
- @totp = ROTP::TOTP.new(@user.otp_secret, issuer: "YourAppName")
34
-
35
- if @totp.verify(params[:code], drift_behind: 15)
36
- sign_in_and_redirect_to_root
37
- else
38
- redirect_to_authentication_challenge
39
- end
40
- end
41
-
42
- def sign_in_and_redirect_to_root
43
- session = @user.sessions.create!
44
- cookies.signed.permanent[:session_token] = { value: session.id, httponly: true }
45
-
46
- redirect_to root_path, notice: "Signed in successfully"
47
- end
48
-
49
- def redirect_to_authentication_challenge
50
- redirect_to new_two_factor_authentication_challenge_path(scheme_type: params[:scheme_type]), alert: "That code didn't work. Please try again"
51
- end
52
- end
@@ -1,19 +0,0 @@
1
- <%%= form_with(url: two_factor_authentication_challenge_path) do |form| %>
2
- <%%= form.hidden_field :scheme_type, value: "totp" %>
3
-
4
- <div>
5
- <%%= form.label :code do %>
6
- <h1>Next, open the 2FA authenticator app on your phone and type the six digit code below:</h1>
7
- <%% end %>
8
- <%%= form.text_field :code, autofocus: true, required: true, autocomplete: :off %>
9
- </div>
10
-
11
- <div>
12
- <%%= form.submit "Verify" %>
13
- </div>
14
- <%% end %>
15
-
16
- <div>
17
- <p><strong>Don't have your phone?</strong></p>
18
- <%%= link_to "Use a recovery code to access your account.", new_two_factor_authentication_challenge_path(scheme_type: "recovery_codes") %>
19
- </div>
@@ -1,7 +0,0 @@
1
- <p style="color: red"><%%= alert %></p>
2
-
3
- <%% if params[:scheme_type] == "recovery_codes" %>
4
- <%%= render "recovery_code_form" %>
5
- <%% else %>
6
- <%%= render "totp_form" %>
7
- <%% end %>