authentication-zero 2.16.20 → 2.16.22

Sign up to get free protection for your applications and to get access to all the features.
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 +11 -0
  31. data/lib/generators/authentication/templates/migrations/create_users_migration.rb.tt +4 -6
  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: 92a96efa7edf8438760960d32bc93e431f786889e95fd9a92343c96a19617edb
4
+ data.tar.gz: 8ae66c2b5b556df1281e8c49dd1c6879bbe118af1617838725f3dd7d251457fc
5
5
  SHA512:
6
- metadata.gz: 4c2f8259a291ab4fa9fdb91521b3e8cf6ed03d678aa72ce148302f940575d7107132e7c71aea4aacf1f195b22ac500d528237add10a1db90853c8bd1b643f8f0
7
- data.tar.gz: 01f856238d22656fcac3c71708ca36c69e8b5d44356934b4e021a84409f49dd48a432fcf3f3d58e383d3466be001b9f9d3d8558c368dc1d59bee28fccf1e54b0
6
+ metadata.gz: 02f153adacde895b01bf833726d3cfa4339cbe9665a60cc06ca0140b0fe64832c087f7c2b6ec139040cf6751def4ff0d2d5537576412adb02146fdd5c234e4ca
7
+ data.tar.gz: 87c0513ffa13f295c78fd054c12ee8d3e79f17464def8174394b0751915ae3f0777e3132b2cd3a0feb3c028118843b7473425513bb0534f726a1c9ab2ca51a51
@@ -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.22)
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.22"
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,11 @@
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, index: { unique: true }
7
+
8
+ t.timestamps
9
+ end
10
+ end
11
+ end
@@ -1,13 +1,16 @@
1
1
  class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
2
  def change
3
3
  create_table :users do |t|
4
- t.string :email, null: false
4
+ t.string :email, null: false, index: { unique: true }
5
5
  t.string :password_digest, null: false
6
6
 
7
7
  t.boolean :verified, null: false, default: false
8
8
  <%- if two_factor? %>
9
9
  t.string :otp_secret
10
10
  <%- end -%>
11
+ <%- if webauthn? %>
12
+ t.string :webauthn_id
13
+ <%- end -%>
11
14
  <%- if omniauthable? %>
12
15
  t.string :provider
13
16
  t.string :uid
@@ -15,10 +18,5 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi
15
18
 
16
19
  t.timestamps
17
20
  end
18
-
19
- add_index :users, :email, unique: true
20
- <%- if omniauthable? -%>
21
- add_index :users, [:provider, :uid], unique: true
22
- <%- end -%>
23
21
  end
24
22
  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 if: :otp_secret_changed?, on: :update 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.22
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 %>