rodauth 2.28.0 → 2.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|