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 +4 -4
- data/CHANGELOG +18 -0
- data/README.rdoc +25 -1
- data/doc/active_sessions.rdoc +1 -0
- data/doc/json.rdoc +1 -1
- data/doc/release_notes/2.29.0.txt +27 -0
- data/doc/release_notes/2.30.0.txt +15 -0
- data/doc/remember.rdoc +3 -1
- data/doc/webauthn_autofill.rdoc +14 -0
- data/doc/webauthn_login.rdoc +1 -1
- data/doc/webauthn_verify_account.rdoc +1 -1
- data/javascript/webauthn_autofill.js +38 -0
- data/lib/rodauth/features/active_sessions.rb +7 -1
- data/lib/rodauth/features/base.rb +9 -1
- data/lib/rodauth/features/json.rb +1 -1
- data/lib/rodauth/features/remember.rb +65 -32
- data/lib/rodauth/features/verify_account_grace_period.rb +1 -1
- data/lib/rodauth/features/webauthn.rb +31 -23
- data/lib/rodauth/features/webauthn_autofill.rb +58 -0
- data/lib/rodauth/features/webauthn_login.rb +9 -1
- data/lib/rodauth/version.rb +1 -1
- data/lib/rodauth.rb +4 -2
- data/templates/login-field.str +1 -1
- data/templates/webauthn-autofill.str +9 -0
- metadata +12 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8270d10e6c0fbe554fc322958893f2c8069363af6455c0d17b0a7d3aafab11bd
|
4
|
+
data.tar.gz: 6eae8a9487764a9b189b27b8f1588e1516d86a6953fdabe0cff6adfd84facb5a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/doc/active_sessions.rdoc
CHANGED
@@ -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
|
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.
|
data/doc/webauthn_login.rdoc
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
= Documentation for WebAuthn Verify Account Feature
|
2
2
|
|
3
|
-
The
|
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
|
-
|
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
|
-
|
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) &&
|
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
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
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
|
-
|
144
|
-
|
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
|
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 =>
|
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
|
-
|
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(
|
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
|
484
|
-
case
|
474
|
+
def webauthn_auth_data
|
475
|
+
case auth_data = raw_param(webauthn_auth_param)
|
485
476
|
when String
|
486
477
|
begin
|
487
|
-
|
478
|
+
JSON.parse(auth_data)
|
488
479
|
rescue
|
489
|
-
throw_error_reason(:
|
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
|
-
|
483
|
+
auth_data
|
493
484
|
else
|
494
|
-
throw_error_reason(:
|
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(
|
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
|
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?
|
data/lib/rodauth/version.rb
CHANGED
data/lib/rodauth.rb
CHANGED
@@ -22,8 +22,10 @@ module Rodauth
|
|
22
22
|
end
|
23
23
|
|
24
24
|
unless json_opt == :only
|
25
|
-
|
26
|
-
|
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
|
data/templates/login-field.str
CHANGED
@@ -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.
|
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.
|
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-
|
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.
|
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
|