rodauth 2.28.0 → 2.30.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 15ed571757453e13ded3557bd2736779546b2c759230f8ee9070976a0207899e
4
- data.tar.gz: 9aa5adf648fa1449a75a03d62ae4907f71d41adc49e81fde8da52097042c986c
3
+ metadata.gz: 8270d10e6c0fbe554fc322958893f2c8069363af6455c0d17b0a7d3aafab11bd
4
+ data.tar.gz: 6eae8a9487764a9b189b27b8f1588e1516d86a6953fdabe0cff6adfd84facb5a
5
5
  SHA512:
6
- metadata.gz: bdae4bbe3d9c471f967a8b24741504494a4003edce0c265dba9026694d64c08f61bd74cdbf80bded79e3b2c163fe7d273fcf72741f986711891fea5e481c501f
7
- data.tar.gz: 5c6352c91b52012c6869a149e61887a1a6e0ace76557f2c4922eb699dc4da7f08a1265c5548bff25a3f617fa0a3b565b6d00f3472822d32812ae6261a4bcc95c
6
+ metadata.gz: d4dca9a0819842a478fac05f138b07452a054c7e498821eee7e61ab49640eccc46974d38c3aea1f71bf226eb7f76b03888a6734d7ae2139bcc74aca9a24bad6c
7
+ data.tar.gz: 5075118b0c6a8b27df251a2e6bfcd45e3dacc7ba1402bf5bacfc38a50b93b424a0ccdb479b1f5541d589f5687a8424c7319a194c0b01a80c762b725065190013
data/CHANGELOG CHANGED
@@ -1,3 +1,21 @@
1
+ === 2.30.0 (2023-05-22)
2
+
3
+ * Make load_memory in the remember feature not raise NoMethodError if logged in when the account no longer exists (jeremyevans) (#331)
4
+
5
+ * Add webauthn_autofill feature, for supporting autofill of webauthn information on the login form (janko) (#328)
6
+
7
+ === 2.29.0 (2023-03-22)
8
+
9
+ * Support :render=>false plugin options (davekaro) (#319)
10
+
11
+ * Add remove_active_session method for removing the active session for a given session id (janko) (#317)
12
+
13
+ * Remove current active session when adding new active session (janko) (#314)
14
+
15
+ * Extend the remember cookie deadline once an hour by default while logged in (janko, jeremyevans) (#313)
16
+
17
+ * Add account! method for returning associated account or loading account based on the session value (janko) (#309)
18
+
1
19
  === 2.28.0 (2023-02-22)
2
20
 
3
21
  * Skip rendering reset password request form on invalid internal request logins (janko) (#303)
data/README.rdoc CHANGED
@@ -37,6 +37,7 @@ HTML and JSON API for all supported features.
37
37
  * WebAuthn (Multifactor authentication via WebAuthn)
38
38
  * WebAuthn Login (Passwordless login via WebAuthn)
39
39
  * WebAuthn Verify Account (Passwordless WebAuthn Setup)
40
+ * WebAuthn Autofill (Autofill WebAuthn credentials on login)
40
41
  * OTP (Multifactor authentication via TOTP)
41
42
  * Recovery Codes (Multifactor authentication via backup codes)
42
43
  * SMS Codes (Multifactor authentication via SMS)
@@ -79,7 +80,8 @@ There are some dependencies that Rodauth uses depending on the
79
80
  features in use. These are development dependencies instead of
80
81
  runtime dependencies in the gem as it is possible to run without them:
81
82
 
82
- tilt :: Used by all features unless in JSON API only mode.
83
+ tilt :: Used by all features unless in JSON API only mode or using
84
+ :render=>false plugin option.
83
85
  rack_csrf :: Used for CSRF support if the <tt>csrf: :rack_csrf</tt> plugin
84
86
  option is given (the default is to use Roda's route_csrf
85
87
  plugin, as that allows for more secure request-specific
@@ -317,6 +319,16 @@ bad idea), you don't need to use the PostgreSQL citext extension. Just
317
319
  remember to modify the migration below to use +String+ instead of +citext+
318
320
  for the email in that case.
319
321
 
322
+ === Grant schema rights (PostgreSQL 15+)
323
+
324
+ PostgreSQL 15 changed default database security so that only the database
325
+ owner has writable access to the public schema. Rodauth expects the
326
+ +ph+ account to have writable access to the public schema when setting
327
+ things up. Temporarily grant that access (it will be revoked after the
328
+ migation has run)
329
+
330
+ psql -U postgres -c "GRANT CREATE ON SCHEMA public TO ${DATABASE_NAME}_password" ${DATABASE_NAME}
331
+
320
332
  === Using non-default schema
321
333
 
322
334
  PostgreSQL sets up new tables in the public schema by default.
@@ -738,6 +750,13 @@ One thing to notice in the above migrations is that Rodauth uses additional
738
750
  tables for additional features, instead of additional columns in a single
739
751
  table.
740
752
 
753
+ === Revoking schema rights (PostgreSQL 15+)
754
+
755
+ If you explicit granted access to the public schema before running the
756
+ migration, revoke it afterward:
757
+
758
+ psql -U postgres -c "REVOKE CREATE ON SCHEMA public FROM ${DATABASE_NAME}_password" ${DATABASE_NAME}
759
+
741
760
  === Locking Down (PostgreSQL only)
742
761
 
743
762
  After running the migrations, you can increase security slightly by making
@@ -852,6 +871,8 @@ which configures which dependent plugins should be loaded. Options:
852
871
  :csrf :: Set to +false+ to not load a csrf plugin. Set to +:rack_csrf+
853
872
  to use the csrf plugin instead of the route_csrf plugin.
854
873
  :flash :: Set to +false+ to not load the flash plugin
874
+ :render :: Set to +false+ to not load the render plugin. This is useful
875
+ to avoid the dependency on tilt when using alternative view libaries.
855
876
  :json :: Set to +true+ to load the json and json_parser plugins. Set
856
877
  to +:only+ to only load those plugins and not any other plugins.
857
878
  Note that if you are enabling features that send email, you
@@ -912,6 +933,7 @@ view the appropriate file in the doc directory.
912
933
  * {Verify Account Grace Period}[rdoc-ref:doc/verify_account_grace_period.rdoc]
913
934
  * {Verify Login Change}[rdoc-ref:doc/verify_login_change.rdoc]
914
935
  * {WebAuthn}[rdoc-ref:doc/webauthn.rdoc]
936
+ * {WebAuthn Autofill}[rdoc-ref:doc/webauthn_autofill.rdoc]
915
937
  * {WebAuthn Login}[rdoc-ref:doc/webauthn_login.rdoc]
916
938
  * {WebAuthn Verify Account}[rdoc-ref:doc/webauthn_verify_account.rdoc]
917
939
 
@@ -1000,6 +1022,8 @@ logged_in? :: Whether the session has been logged in.
1000
1022
  authenticated? :: Similar to +logged_in?+, but if the account has setup two
1001
1023
  factor authentication, whether the session has authenticated
1002
1024
  via two factors.
1025
+ account! :: Returns the current account record if it has already been loaded,
1026
+ otherwise retrieves the account from session if logged in.
1003
1027
  authenticated_by :: An array of strings for successful authentication methods for
1004
1028
  the current session (e.g. password/remember/webauthn).
1005
1029
  possible_authentication_methods :: An array of strings for possible authentication
@@ -48,6 +48,7 @@ add_active_session :: Create a session id for the session and populate the sessi
48
48
  currently_active_session? :: Whether the session is currently active, by checking the database table.
49
49
  handle_duplicate_active_session_id(exception) :: How to handle the case where a duplicate session id for the account is inserted into the table. Does nothing by default. This should only be called if the random number generator is broken.
50
50
  no_longer_active_session :: What action to take if +rodauth.check_active_session+ is called and the session is no longer active.
51
+ remove_active_session(session_id) :: Removes the active session matching the given session ID from the database. Useful for implementing session revoking.
51
52
  remove_all_active_sessions :: Remove all active session from the database, used for global logouts and when closing accounts.
52
53
  remove_current_session :: Remove current session from the database, used for regular logouts.
53
54
  remove_inactive_sessions :: Remove inactive sessions from the database, run before checking for whether the current session is active.
data/doc/json.rdoc CHANGED
@@ -15,7 +15,7 @@ an array containing the field name and the error message for that field.
15
15
  Successful requests by default store a +success+ entry with a success
16
16
  message, though that can be disabled.
17
17
 
18
- The JSON response can be modified at any point by modifying the `json_response`
18
+ The JSON response can be modified at any point by modifying the +json_response+
19
19
  hash. The following example adds an {error reason}[rdoc-ref:doc/error_reasons.rdoc]
20
20
  to the JSON response:
21
21
 
@@ -0,0 +1,27 @@
1
+ = New Features
2
+
3
+ * When using the remember feature, by default, the remember deadline
4
+ is extended while logged in, if it hasn't been extended in the last
5
+ hour
6
+
7
+ * An account! method has been added, which will return the hash for
8
+ the account if already retrieved, or attempt to retrieve the
9
+ account hash using the currently logged in session if not.
10
+ Because of the ambiguity in the provenance of the returned account
11
+ hash, callers should be careful when using this method.
12
+
13
+ * A remove_active_session method has been added. You can call this
14
+ method with a specific session id, and it will remove the related
15
+ active session.
16
+
17
+ * A render: false plugin option is now support, which will disable
18
+ the automatic loading of the render plugin. This should only be
19
+ used if you are completely replacing Rodauth's view rendering with
20
+ your own.
21
+
22
+ = Other Improvements
23
+
24
+ * When logging in when using the active_sessions feature, if there is
25
+ a current active session, it is removed before a new active session
26
+ is created. This prevents some stale active sessions from remaining
27
+ in the database (which would eventually be cleaned up later).
@@ -0,0 +1,15 @@
1
+ = New Features
2
+
3
+ * A webauthn_autofill feature has been added to allow autofilling
4
+ webauthn credentials during login (also known as conditional
5
+ mediation). This allows for easier login using passkeys.
6
+ This requires a supported browser and operating system on the
7
+ client side to work.
8
+
9
+ = Other Improvements
10
+
11
+ * The load_memory method in the remember feature no longer raises
12
+ a NoMethodError if the there is a remember cookie, the session is
13
+ already logged in, and the account no longer exists. The
14
+ load_memory method now removes the remember cookie and clears the
15
+ session in that case.
data/doc/remember.rdoc CHANGED
@@ -30,13 +30,15 @@ for sessions autologged in via a remember token:
30
30
 
31
31
  == Auth Value Methods
32
32
 
33
- extend_remember_deadline? :: Whether to extend the remember token deadline when the user is autologged in via remember token.
33
+ extend_remember_deadline? :: Whether to extend the remember token deadline when the user is autologged in via remember token and every +extend_remember_deadline_period+ seconds while logged in.
34
+ extend_remember_deadline_period :: The amount of seconds to wait before extending remember token deadline when +extend_remember_deadline?+ is true (3600 by default).
34
35
  raw_remember_token_deadline :: A deadline before which to allow a raw remember token to be used. Allows for graceful transition for when +hmac_secret+ is first set.
35
36
  remember_additional_form_tags :: HTML fragment containing additional form tags to use on the change remember setting form.
36
37
  remember_button :: The text to use for the change remember settings button.
37
38
  remember_cookie_key :: The cookie name to use for the remember token.
38
39
  remember_cookie_options :: Any options to set for the remember cookie. By default, the `:path` cookie option is set to `/` and `:httponly` is set to `true`. Also, `:secure` is set to `true` by default if the current request is an HTTPS request.
39
40
  remember_deadline_column :: The column name in the +remember_table+ storing the deadline after which the token will be ignored.
41
+ remember_deadline_extended_session_key :: The session key set if the remember deadline token is being extended.
40
42
  remember_deadline_interval :: The amount of time for which to remember accounts, 14 days by default. Only used if +set_deadline_values?+ is true.
41
43
  remember_disable_label :: The label for disabling remembering.
42
44
  remember_disable_param_value :: The parameter value for disabling remembering.
@@ -0,0 +1,14 @@
1
+ = Documentation for WebAuthn Autofill Feature
2
+
3
+ The webauthn_autofill feature enables autofill UI (aka "conditional mediation")
4
+ for WebAuthn credentials, logging the user in on selection. It depends on the
5
+ webauthn_login feature.
6
+
7
+ == Auth Value Methods
8
+
9
+ webauthn_autofill_js :: The javascript code to execute on the login page to enable autofill UI.
10
+ webauthn_autofill_js_route :: The route to the webauthn autofill javascript file.
11
+
12
+ == Auth Methods
13
+
14
+ before_webauthn_autofill_js_route :: Run arbitrary code before handling a webauthn autofill javascript route.
@@ -1,6 +1,6 @@
1
1
  = Documentation for WebAuthn Login Feature
2
2
 
3
- The webauthn feature implements passwordless authentication via
3
+ The webauthn_login feature implements passwordless authentication via
4
4
  WebAuthn. It depends on the login and webauthn features.
5
5
 
6
6
  == Auth Value Methods
@@ -1,6 +1,6 @@
1
1
  = Documentation for WebAuthn Verify Account Feature
2
2
 
3
- The webauthn feature implements setting up an WebAuthn authenticator
3
+ The webauthn_verify_account feature implements setting up an WebAuthn authenticator
4
4
  during the account verification process, and making such setup
5
5
  a requirement for account verification. By default, it disables
6
6
  asking for a password during account creation and verification,
@@ -0,0 +1,38 @@
1
+ (function() {
2
+ var pack = function(v) { return btoa(String.fromCharCode.apply(null, new Uint8Array(v))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); };
3
+ var unpack = function(v) { return Uint8Array.from(atob(v.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0)); };
4
+ var element = document.getElementById('webauthn-login-form');
5
+
6
+ if (!window.PublicKeyCredential || !PublicKeyCredential.isConditionalMediationAvailable) return;
7
+
8
+ PublicKeyCredential.isConditionalMediationAvailable().then(function(available) {
9
+ if (!available) return;
10
+
11
+ var opts = JSON.parse(element.getAttribute("data-credential-options"));
12
+ opts.challenge = unpack(opts.challenge);
13
+ opts.allowCredentials.forEach(function(cred) { cred.id = unpack(cred.id); });
14
+
15
+ navigator.credentials.get({mediation: "conditional", publicKey: opts}).then(function(cred) {
16
+ var rawId = pack(cred.rawId);
17
+ var authValue = {
18
+ type: cred.type,
19
+ id: rawId,
20
+ rawId: rawId,
21
+ response: {
22
+ authenticatorData: pack(cred.response.authenticatorData),
23
+ clientDataJSON: pack(cred.response.clientDataJSON),
24
+ signature: pack(cred.response.signature)
25
+ }
26
+ };
27
+
28
+ if (cred.response.userHandle) {
29
+ authValue.response.userHandle = pack(cred.response.userHandle);
30
+ }
31
+
32
+ document.getElementById('webauthn-auth').value = JSON.stringify(authValue);
33
+
34
+ element.submit();
35
+ });
36
+ });
37
+ })();
38
+
@@ -29,6 +29,7 @@ module Rodauth
29
29
  :currently_active_session?,
30
30
  :handle_duplicate_active_session_id,
31
31
  :no_longer_active_session,
32
+ :remove_active_session,
32
33
  :remove_all_active_sessions,
33
34
  :remove_current_session,
34
35
  :remove_inactive_sessions,
@@ -82,10 +83,14 @@ module Rodauth
82
83
 
83
84
  def remove_current_session
84
85
  if session_id = session[session_id_session_key]
85
- active_sessions_ds.where(active_sessions_session_id_column=>compute_hmac(session_id)).delete
86
+ remove_active_session(compute_hmac(session_id))
86
87
  end
87
88
  end
88
89
 
90
+ def remove_active_session(session_id)
91
+ active_sessions_ds.where(active_sessions_session_id_column=>session_id).delete
92
+ end
93
+
89
94
  def remove_all_active_sessions
90
95
  active_sessions_ds.delete
91
96
  end
@@ -101,6 +106,7 @@ module Rodauth
101
106
  end
102
107
 
103
108
  def update_session
109
+ remove_current_session
104
110
  super
105
111
  add_active_session
106
112
  end
@@ -269,6 +269,10 @@ module Rodauth
269
269
  Sequel::DATABASES.first or raise "Sequel database connection is missing"
270
270
  end
271
271
 
272
+ def login_field_autocomplete_value
273
+ login_uses_email? ? "email" : "on"
274
+ end
275
+
272
276
  def password_field_autocomplete_value
273
277
  @password_field_autocomplete_value || 'current-password'
274
278
  end
@@ -355,6 +359,10 @@ module Rodauth
355
359
  account_open_status_value
356
360
  end
357
361
 
362
+ def account!
363
+ account || (session_value && account_from_session)
364
+ end
365
+
358
366
  def account_from_session
359
367
  @account = _account_from_session
360
368
  end
@@ -680,7 +688,7 @@ module Rodauth
680
688
  # note that only the salt is returned.
681
689
  def get_password_hash
682
690
  if account_password_hash_column
683
- (account || account_from_session)[account_password_hash_column]
691
+ account![account_password_hash_column]
684
692
  elsif use_database_authentication_functions?
685
693
  db.get(Sequel.function(function_name(:rodauth_get_salt), account ? account_id : session_value))
686
694
  else
@@ -116,7 +116,7 @@ module Rodauth
116
116
 
117
117
  def before_webauthn_login_route
118
118
  super if defined?(super)
119
- if use_json? && !param_or_nil(webauthn_auth_param) && account_from_login(param(login_param))
119
+ if use_json? && !param_or_nil(webauthn_auth_param) && webauthn_login_options?
120
120
  cred = webauthn_credential_options_for_get
121
121
  json_response[webauthn_auth_param] = cred.as_json
122
122
  json_response[webauthn_auth_challenge_param] = cred.challenge
@@ -17,6 +17,7 @@ module Rodauth
17
17
  auth_value_method :raw_remember_token_deadline, nil
18
18
  auth_value_method :remember_cookie_options, {}.freeze
19
19
  auth_value_method :extend_remember_deadline?, false
20
+ auth_value_method :extend_remember_deadline_period, 3600
20
21
  auth_value_method :remember_period, {:days=>14}.freeze
21
22
  auth_value_method :remember_deadline_interval, {:days=>14}.freeze
22
23
  auth_value_method :remember_id_column, :id
@@ -28,6 +29,7 @@ module Rodauth
28
29
  auth_value_method :remember_remember_param_value, 'remember'
29
30
  auth_value_method :remember_forget_param_value, 'forget'
30
31
  auth_value_method :remember_disable_param_value, 'disable'
32
+ session_key :remember_deadline_extended_session_key, :remember_deadline_extended_at
31
33
  translatable_method :remember_remember_label, 'Remember Me'
32
34
  translatable_method :remember_forget_label, 'Forget Me'
33
35
  translatable_method :remember_disable_label, 'Disable Remember Me'
@@ -110,43 +112,27 @@ module Rodauth
110
112
  end
111
113
 
112
114
  def load_memory
113
- return if session[session_key]
114
-
115
- unless id = remembered_session_id
116
- # Only set expired cookie if there is already a cookie set.
117
- forget_login if _get_remember_cookie
118
- return
119
- end
120
-
121
- set_session_value(session_key, id)
122
- account = account_from_session
123
- remove_session_value(session_key)
124
-
125
- unless account
126
- remove_remember_key(id)
127
- forget_login
128
- return
129
- end
130
-
131
- before_load_memory
132
- login_session('remember')
133
-
134
- if extend_remember_deadline?
135
- active_remember_key_ds(id).update(remember_deadline_column=>Sequel.date_add(Sequel::CURRENT_TIMESTAMP, remember_period))
136
- remember_login
115
+ if logged_in?
116
+ if extend_remember_deadline_while_logged_in?
117
+ if account_from_session
118
+ extend_remember_deadline
119
+ else
120
+ forget_login
121
+ clear_session
122
+ end
123
+ end
124
+ elsif account_from_remember_cookie
125
+ before_load_memory
126
+ login_session('remember')
127
+ extend_remember_deadline if extend_remember_deadline?
128
+ after_load_memory
137
129
  end
138
- after_load_memory
139
130
  end
140
131
 
141
132
  def remember_login
142
133
  get_remember_key
143
- opts = Hash[remember_cookie_options]
144
- opts[:value] = "#{account_id}_#{convert_token_key(remember_key_value)}"
145
- opts[:expires] = convert_timestamp(active_remember_key_ds.get(remember_deadline_column))
146
- opts[:path] = "/" unless opts.key?(:path)
147
- opts[:httponly] = true unless opts.key?(:httponly) || opts.key?(:http_only)
148
- opts[:secure] = true unless opts.key?(:secure) || !request.ssl?
149
- ::Rack::Utils.set_cookie_header!(response.headers, remember_cookie_key, opts)
134
+ set_remember_cookie
135
+ set_session_value(remember_deadline_extended_session_key, Time.now.to_i) if extend_remember_deadline?
150
136
  end
151
137
 
152
138
  def forget_login
@@ -191,6 +177,53 @@ module Rodauth
191
177
 
192
178
  private
193
179
 
180
+ def set_remember_cookie
181
+ opts = Hash[remember_cookie_options]
182
+ opts[:value] = "#{account_id}_#{convert_token_key(remember_key_value)}"
183
+ opts[:expires] = convert_timestamp(active_remember_key_ds.get(remember_deadline_column))
184
+ opts[:path] = "/" unless opts.key?(:path)
185
+ opts[:httponly] = true unless opts.key?(:httponly) || opts.key?(:http_only)
186
+ opts[:secure] = true unless opts.key?(:secure) || !request.ssl?
187
+ ::Rack::Utils.set_cookie_header!(response.headers, remember_cookie_key, opts)
188
+ end
189
+
190
+ def extend_remember_deadline_while_logged_in?
191
+ return false unless extend_remember_deadline?
192
+
193
+ if extended_at = session[remember_deadline_extended_session_key]
194
+ extended_at + extend_remember_deadline_period < Time.now.to_i
195
+ elsif logged_in_via_remember_key?
196
+ # Handle existing sessions before the change to extend remember deadline
197
+ # while logged in.
198
+ true
199
+ end
200
+ end
201
+
202
+ def extend_remember_deadline
203
+ active_remember_key_ds.update(remember_deadline_column=>Sequel.date_add(Sequel::CURRENT_TIMESTAMP, remember_period))
204
+ remember_login
205
+ end
206
+
207
+ def account_from_remember_cookie
208
+ unless id = remembered_session_id
209
+ # Only set expired cookie if there is already a cookie set.
210
+ forget_login if _get_remember_cookie
211
+ return
212
+ end
213
+
214
+ set_session_value(session_key, id)
215
+ account_from_session
216
+ remove_session_value(session_key)
217
+
218
+ unless account
219
+ remove_remember_key(id)
220
+ forget_login
221
+ return
222
+ end
223
+
224
+ account
225
+ end
226
+
194
227
  def _get_remember_cookie
195
228
  request.cookies[remember_cookie_key]
196
229
  end
@@ -83,7 +83,7 @@ module Rodauth
83
83
  end
84
84
 
85
85
  def account_in_unverified_grace_period?
86
- return false unless account || (session_value && account_from_session)
86
+ return false unless account!
87
87
  account[account_status_column] == account_unverified_status_value &&
88
88
  verify_account_grace_period &&
89
89
  !verify_account_ds.where(Sequel.date_add(verification_requested_at_column, :seconds=>verify_account_grace_period) > Sequel::CURRENT_TIMESTAMP).empty?
@@ -320,7 +320,7 @@ module Rodauth
320
320
 
321
321
  def webauthn_credential_options_for_get
322
322
  WebAuthn::Credential.options_for_get(
323
- :allow => account_webauthn_ids,
323
+ :allow => webauthn_allow,
324
324
  :timeout => webauthn_auth_timeout,
325
325
  :rp_id => webauthn_rp_id,
326
326
  :user_verification => webauthn_user_verification,
@@ -329,13 +329,17 @@ module Rodauth
329
329
  end
330
330
 
331
331
  def webauthn_user_name
332
- (account || account_from_session)[login_column]
332
+ account![login_column]
333
333
  end
334
334
 
335
335
  def webauthn_origin
336
336
  base_url
337
337
  end
338
338
 
339
+ def webauthn_allow
340
+ account_webauthn_ids
341
+ end
342
+
339
343
  def webauthn_rp_id
340
344
  webauthn_origin.sub(/\Ahttps?:\/\//, '').sub(/:\d+\z/, '')
341
345
  end
@@ -453,21 +457,8 @@ module Rodauth
453
457
  end
454
458
 
455
459
  def webauthn_auth_credential_from_form_submission
456
- case auth_data = raw_param(webauthn_auth_param)
457
- when String
458
- begin
459
- auth_data = JSON.parse(auth_data)
460
- rescue
461
- throw_error_reason(:invalid_webauthn_auth_param, invalid_field_error_status, webauthn_auth_param, webauthn_invalid_auth_param_message)
462
- end
463
- when Hash
464
- # nothing
465
- else
466
- throw_error_reason(:invalid_webauthn_auth_param, invalid_field_error_status, webauthn_auth_param, webauthn_invalid_auth_param_message)
467
- end
468
-
469
460
  begin
470
- webauthn_credential = WebAuthn::Credential.from_get(auth_data)
461
+ webauthn_credential = WebAuthn::Credential.from_get(webauthn_auth_data)
471
462
  unless valid_webauthn_credential_auth?(webauthn_credential)
472
463
  throw_error_reason(:invalid_webauthn_auth_param, invalid_key_error_status, webauthn_auth_param, webauthn_invalid_auth_param_message)
473
464
  end
@@ -480,26 +471,28 @@ module Rodauth
480
471
  webauthn_credential
481
472
  end
482
473
 
483
- def webauthn_setup_credential_from_form_submission
484
- case setup_data = raw_param(webauthn_setup_param)
474
+ def webauthn_auth_data
475
+ case auth_data = raw_param(webauthn_auth_param)
485
476
  when String
486
477
  begin
487
- setup_data = JSON.parse(setup_data)
478
+ JSON.parse(auth_data)
488
479
  rescue
489
- throw_error_reason(:invalid_webauthn_setup_param, invalid_field_error_status, webauthn_setup_param, webauthn_invalid_setup_param_message)
480
+ throw_error_reason(:invalid_webauthn_auth_param, invalid_field_error_status, webauthn_auth_param, webauthn_invalid_auth_param_message)
490
481
  end
491
482
  when Hash
492
- # nothing
483
+ auth_data
493
484
  else
494
- throw_error_reason(:invalid_webauthn_setup_param, invalid_field_error_status, webauthn_setup_param, webauthn_invalid_setup_param_message)
485
+ throw_error_reason(:invalid_webauthn_auth_param, invalid_field_error_status, webauthn_auth_param, webauthn_invalid_auth_param_message)
495
486
  end
487
+ end
496
488
 
489
+ def webauthn_setup_credential_from_form_submission
497
490
  unless two_factor_password_match?(param(password_param))
498
491
  throw_error_reason(:invalid_password, invalid_password_error_status, password_param, invalid_password_message)
499
492
  end
500
493
 
501
494
  begin
502
- webauthn_credential = WebAuthn::Credential.from_create(setup_data)
495
+ webauthn_credential = WebAuthn::Credential.from_create(webauthn_setup_data)
503
496
  unless valid_new_webauthn_credential?(webauthn_credential)
504
497
  throw_error_reason(:invalid_webauthn_setup_param, invalid_field_error_status, webauthn_setup_param, webauthn_invalid_setup_param_message)
505
498
  end
@@ -509,5 +502,20 @@ module Rodauth
509
502
 
510
503
  webauthn_credential
511
504
  end
505
+
506
+ def webauthn_setup_data
507
+ case setup_data = raw_param(webauthn_setup_param)
508
+ when String
509
+ begin
510
+ JSON.parse(setup_data)
511
+ rescue
512
+ throw_error_reason(:invalid_webauthn_setup_param, invalid_field_error_status, webauthn_setup_param, webauthn_invalid_setup_param_message)
513
+ end
514
+ when Hash
515
+ setup_data
516
+ else
517
+ throw_error_reason(:invalid_webauthn_setup_param, invalid_field_error_status, webauthn_setup_param, webauthn_invalid_setup_param_message)
518
+ end
519
+ end
512
520
  end
513
521
  end
@@ -0,0 +1,58 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Rodauth
4
+ Feature.define(:webauthn_autofill, :WebauthnAutofill) do
5
+ depends :webauthn_login
6
+
7
+ auth_value_method :webauthn_autofill_js, File.binread(File.expand_path('../../../../javascript/webauthn_autofill.js', __FILE__)).freeze
8
+
9
+ route(:webauthn_autofill_js) do |r|
10
+ before_webauthn_autofill_js_route
11
+ r.get do
12
+ response['Content-Type'] = 'text/javascript'
13
+ webauthn_autofill_js
14
+ end
15
+ end
16
+
17
+ def webauthn_allow
18
+ return [] unless logged_in? || account
19
+ super
20
+ end
21
+
22
+ def webauthn_user_verification
23
+ 'preferred'
24
+ end
25
+
26
+ def webauthn_authenticator_selection
27
+ super.merge({ 'residentKey' => 'required', 'requireResidentKey' => true })
28
+ end
29
+
30
+ def login_field_autocomplete_value
31
+ request.path_info == login_path ? "#{super} webauthn" : super
32
+ end
33
+
34
+ private
35
+
36
+ def _login_form_footer
37
+ footer = super
38
+ footer += render("webauthn-autofill") unless valid_login_entered?
39
+ footer
40
+ end
41
+
42
+ def account_from_webauthn_login
43
+ return super if param_or_nil(login_param)
44
+
45
+ credential_id = webauthn_auth_data["id"]
46
+ account_id = db[webauthn_keys_table]
47
+ .where(webauthn_keys_webauthn_id_column => credential_id)
48
+ .get(webauthn_keys_account_id_column)
49
+
50
+ @account = account_ds(account_id).first if account_id
51
+ end
52
+
53
+ def webauthn_login_options?
54
+ return true unless param_or_nil(login_param)
55
+ super
56
+ end
57
+ end
58
+ end
@@ -16,7 +16,7 @@ module Rodauth
16
16
 
17
17
  r.post do
18
18
  catch_error do
19
- unless account_from_login(param(login_param)) && open_account?
19
+ unless account_from_webauthn_login && open_account?
20
20
  throw_error_reason(:no_matching_login, no_matching_login_error_status, login_param, no_matching_login_message)
21
21
  end
22
22
 
@@ -54,6 +54,14 @@ module Rodauth
54
54
 
55
55
  private
56
56
 
57
+ def account_from_webauthn_login
58
+ account_from_login(param(login_param))
59
+ end
60
+
61
+ def webauthn_login_options?
62
+ !!account_from_webauthn_login
63
+ end
64
+
57
65
  def _multi_phase_login_forms
58
66
  forms = super
59
67
  if valid_login_entered? && webauthn_setup?
@@ -6,7 +6,7 @@ module Rodauth
6
6
  MAJOR = 2
7
7
 
8
8
  # The minor version of Rodauth, updated for new feature releases of Rodauth.
9
- MINOR = 28
9
+ MINOR = 30
10
10
 
11
11
  # The patch version of Rodauth, updated only for bug fixes from the last
12
12
  # feature release.
data/lib/rodauth.rb CHANGED
@@ -22,8 +22,10 @@ module Rodauth
22
22
  end
23
23
 
24
24
  unless json_opt == :only
25
- require 'tilt/string'
26
- app.plugin :render
25
+ unless opts[:render] == false
26
+ require 'tilt/string'
27
+ app.plugin :render
28
+ end
27
29
 
28
30
  case opts.fetch(:csrf, app.opts[:rodauth_csrf])
29
31
  when false
@@ -1,4 +1,4 @@
1
1
  <div class="form-group mb-3">
2
2
  <label for="login" class="form-label">#{rodauth.login_label}#{rodauth.input_field_label_suffix}</label>
3
- #{rodauth.input_field_string(rodauth.login_param, 'login', :type=>rodauth.login_input_type, :autocomplete=>rodauth.login_uses_email? ? "email" : "on")}
3
+ #{rodauth.input_field_string(rodauth.login_param, 'login', :type=>rodauth.login_input_type, :autocomplete=>rodauth.login_field_autocomplete_value)}
4
4
  </div>
@@ -0,0 +1,9 @@
1
+ <form method="post" action="#{rodauth.webauthn_login_path}" class="rodauth" role="form" id="webauthn-login-form" data-credential-options="#{h((cred = rodauth.webauthn_credential_options_for_get).as_json.to_json)}">
2
+ #{rodauth.webauthn_auth_additional_form_tags}
3
+ #{rodauth.csrf_tag(rodauth.webauthn_login_path)}
4
+ <input type="hidden" name="#{rodauth.webauthn_auth_challenge_param}" value="#{cred.challenge}" />
5
+ <input type="hidden" name="#{rodauth.webauthn_auth_challenge_hmac_param}" value="#{rodauth.compute_hmac(cred.challenge)}" />
6
+ <input class="rodauth_hidden d-none" aria-hidden="true" type="text" name="#{rodauth.webauthn_auth_param}" id="webauthn-auth" value="" />
7
+ #{rodauth.button(rodauth.webauthn_auth_button, class: "d-none")}
8
+ </form>
9
+ <script src="#{rodauth.webauthn_js_host}#{rodauth.webauthn_autofill_js_path}"></script>
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rodauth
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.28.0
4
+ version: 2.30.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Evans
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-02-22 00:00:00.000000000 Z
11
+ date: 2023-05-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sequel
@@ -296,6 +296,7 @@ extra_rdoc_files:
296
296
  - doc/verify_account_grace_period.rdoc
297
297
  - doc/verify_login_change.rdoc
298
298
  - doc/webauthn.rdoc
299
+ - doc/webauthn_autofill.rdoc
299
300
  - doc/webauthn_login.rdoc
300
301
  - doc/webauthn_verify_account.rdoc
301
302
  - doc/release_notes/1.0.0.txt
@@ -344,7 +345,9 @@ extra_rdoc_files:
344
345
  - doc/release_notes/2.26.0.txt
345
346
  - doc/release_notes/2.27.0.txt
346
347
  - doc/release_notes/2.28.0.txt
348
+ - doc/release_notes/2.29.0.txt
347
349
  - doc/release_notes/2.3.0.txt
350
+ - doc/release_notes/2.30.0.txt
348
351
  - doc/release_notes/2.4.0.txt
349
352
  - doc/release_notes/2.5.0.txt
350
353
  - doc/release_notes/2.6.0.txt
@@ -459,7 +462,9 @@ files:
459
462
  - doc/release_notes/2.26.0.txt
460
463
  - doc/release_notes/2.27.0.txt
461
464
  - doc/release_notes/2.28.0.txt
465
+ - doc/release_notes/2.29.0.txt
462
466
  - doc/release_notes/2.3.0.txt
467
+ - doc/release_notes/2.30.0.txt
463
468
  - doc/release_notes/2.4.0.txt
464
469
  - doc/release_notes/2.5.0.txt
465
470
  - doc/release_notes/2.6.0.txt
@@ -478,9 +483,11 @@ files:
478
483
  - doc/verify_account_grace_period.rdoc
479
484
  - doc/verify_login_change.rdoc
480
485
  - doc/webauthn.rdoc
486
+ - doc/webauthn_autofill.rdoc
481
487
  - doc/webauthn_login.rdoc
482
488
  - doc/webauthn_verify_account.rdoc
483
489
  - javascript/webauthn_auth.js
490
+ - javascript/webauthn_autofill.js
484
491
  - javascript/webauthn_setup.js
485
492
  - lib/roda/plugins/rodauth.rb
486
493
  - lib/rodauth.rb
@@ -528,6 +535,7 @@ files:
528
535
  - lib/rodauth/features/verify_account_grace_period.rb
529
536
  - lib/rodauth/features/verify_login_change.rb
530
537
  - lib/rodauth/features/webauthn.rb
538
+ - lib/rodauth/features/webauthn_autofill.rb
531
539
  - lib/rodauth/features/webauthn_login.rb
532
540
  - lib/rodauth/features/webauthn_verify_account.rb
533
541
  - lib/rodauth/migrations.rb
@@ -583,6 +591,7 @@ files:
583
591
  - templates/verify-login-change-email.str
584
592
  - templates/verify-login-change.str
585
593
  - templates/webauthn-auth.str
594
+ - templates/webauthn-autofill.str
586
595
  - templates/webauthn-remove.str
587
596
  - templates/webauthn-setup.str
588
597
  homepage: https://rodauth.jeremyevans.net
@@ -616,7 +625,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
616
625
  - !ruby/object:Gem::Version
617
626
  version: '0'
618
627
  requirements: []
619
- rubygems_version: 3.4.6
628
+ rubygems_version: 3.4.10
620
629
  signing_key:
621
630
  specification_version: 4
622
631
  summary: Authentication and Account Management Framework for Rack Applications