rodauth 2.3.0 → 2.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +42 -0
- data/MIT-LICENSE +1 -1
- data/README.rdoc +14 -0
- data/doc/base.rdoc +3 -1
- data/doc/jwt_refresh.rdoc +8 -0
- data/doc/login.rdoc +1 -0
- data/doc/password_pepper.rdoc +44 -0
- data/doc/recovery_codes.rdoc +2 -1
- data/doc/release_notes/2.4.0.txt +22 -0
- data/doc/release_notes/2.5.0.txt +20 -0
- data/doc/release_notes/2.6.0.txt +37 -0
- data/doc/release_notes/2.7.0.txt +33 -0
- data/doc/release_notes/2.8.0.txt +20 -0
- data/doc/remember.rdoc +1 -1
- data/doc/verify_login_change.rdoc +1 -0
- data/javascript/webauthn_auth.js +9 -9
- data/javascript/webauthn_setup.js +9 -6
- data/lib/rodauth.rb +14 -6
- data/lib/rodauth/features/base.rb +19 -4
- data/lib/rodauth/features/change_password.rb +1 -1
- data/lib/rodauth/features/confirm_password.rb +2 -2
- data/lib/rodauth/features/disallow_password_reuse.rb +4 -2
- data/lib/rodauth/features/jwt.rb +15 -4
- data/lib/rodauth/features/jwt_refresh.rb +62 -9
- data/lib/rodauth/features/login.rb +8 -2
- data/lib/rodauth/features/otp.rb +0 -2
- data/lib/rodauth/features/password_pepper.rb +45 -0
- data/lib/rodauth/features/recovery_codes.rb +22 -1
- data/lib/rodauth/features/remember.rb +7 -1
- data/lib/rodauth/features/verify_account.rb +6 -7
- data/lib/rodauth/features/verify_login_change.rb +2 -1
- data/lib/rodauth/version.rb +1 -1
- metadata +34 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a39430563fc9d81e610b8a28b5c044eedd7ebd361d6294f9bd02687ce2d24c27
|
4
|
+
data.tar.gz: a852e9713602b807bb7566b3db4038a6fda0ae0d2b90bf4b1ce2e45429c8efd2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ba8f83d3f9afc3f1bcca6f649e925b1b3f795002dc980ae5bfb219885a74c69c34bbacaccda83c153bf9f799f328970cd900a3abf5966b88956dc3476ef3f9f8
|
7
|
+
data.tar.gz: 9d2042a183a7fdc941cd2b8716aba581861b9e1831574eac3241979ab32e2fe2076baf94e3ab0782487aa34d4e7f98f063035c22dcd7088f1a4fd1f1b83a7e6d
|
data/CHANGELOG
CHANGED
@@ -1,3 +1,45 @@
|
|
1
|
+
=== 2.8.0 (2021-01-06)
|
2
|
+
|
3
|
+
* [SECURITY] Set HttpOnly on remember cookie by default so it cannot be accessed by Javascript (janko) (#142)
|
4
|
+
|
5
|
+
* Clear JWT session when rodauth.clear_session is called if the Roda sessions plugin is used (janko) (#140)
|
6
|
+
|
7
|
+
=== 2.7.0 (2020-12-22)
|
8
|
+
|
9
|
+
* Avoid method redefinition warnings in verbose warning mode (jeremyevans)
|
10
|
+
|
11
|
+
* Return expired access token error message in the JWT refresh feature when using an expired token when it isn't allowed (AlexyMatskevich) (#133)
|
12
|
+
|
13
|
+
* Allow Rodauth features to be preloaded, instead of always trying to require them (janko) (#136)
|
14
|
+
|
15
|
+
* Use a default remember cookie path of '/', though this may cause problem with multiple Rodauth configurations on the same domain (janko) (#134)
|
16
|
+
|
17
|
+
* Add auto_remove_recovery_codes? to the recovery_codes feature, for automatically removing the codes when disabling multifactor authentication (SilasSpet, jeremyevans) (#135)
|
18
|
+
|
19
|
+
=== 2.6.0 (2020-11-20)
|
20
|
+
|
21
|
+
* Avoid loading features multiple times (janko) (#131)
|
22
|
+
|
23
|
+
* Add around_rodauth method for running code around the handling of all Rodauth routes (bjeanes) (#129)
|
24
|
+
|
25
|
+
* Fix javascript for registration of multiple webauthn keys (bjeanes) (#127)
|
26
|
+
|
27
|
+
* Add allow_refresh_with_expired_jwt_access_token? configuration method to jwt_refresh feature, for allowing refresh with expired access token (jeremyevans)
|
28
|
+
|
29
|
+
* Promote setup_account_verification to public API, useful for automatically sending account verification emails (jeremyevans)
|
30
|
+
|
31
|
+
=== 2.5.0 (2020-10-22)
|
32
|
+
|
33
|
+
* Add change_login_needs_verification_notice_flash for easier translation of change_login_notice_flash when using verify_login_change (bjeanes, janko, jeremyevans) (#126)
|
34
|
+
|
35
|
+
* Add login_return_to_requested_location_path for controlling path to use as the requested location (HoneyryderChuck, jeremyevans) (#122, #123)
|
36
|
+
|
37
|
+
=== 2.4.0 (2020-09-21)
|
38
|
+
|
39
|
+
* Add session_key_prefix for more easily using separate session keys when using multiple configurations (janko) (#121)
|
40
|
+
|
41
|
+
* Add password_pepper feature for appending a secret key to passwords before they are hashed, supporting secret rotation (janko) (#119)
|
42
|
+
|
1
43
|
=== 2.3.0 (2020-08-21)
|
2
44
|
|
3
45
|
* Return an error status instead of an invalid access token when trying to refresh JWT without an access token in the jwt_refresh feature (jeremyevans)
|
data/MIT-LICENSE
CHANGED
data/README.rdoc
CHANGED
@@ -44,6 +44,7 @@ HTML and JSON API for all supported features.
|
|
44
44
|
* Verify Account Grace Period (Don't require verification before login)
|
45
45
|
* Password Grace Period (Don't require password entry if recently entered)
|
46
46
|
* Password Complexity (More sophisticated checks)
|
47
|
+
* Password Pepper
|
47
48
|
* Disallow Password Reuse
|
48
49
|
* Disallow Common Passwords
|
49
50
|
* Password Expiration
|
@@ -881,6 +882,7 @@ view the appropriate file in the doc directory.
|
|
881
882
|
* {Password Complexity}[rdoc-ref:doc/password_complexity.rdoc]
|
882
883
|
* {Password Expiration}[rdoc-ref:doc/password_expiration.rdoc]
|
883
884
|
* {Password Grace Period}[rdoc-ref:doc/password_grace_period.rdoc]
|
885
|
+
* {Password Pepper}[rdoc-ref:doc/password_pepper.rdoc]
|
884
886
|
* {Recovery Codes}[rdoc-ref:doc/recovery_codes.rdoc]
|
885
887
|
* {Remember}[rdoc-ref:doc/remember.rdoc]
|
886
888
|
* {Reset Password}[rdoc-ref:doc/reset_password.rdoc]
|
@@ -1062,6 +1064,18 @@ the name as an argument to use that configuration:
|
|
1062
1064
|
r.rodauth
|
1063
1065
|
end
|
1064
1066
|
|
1067
|
+
By default, alternate configurations will use the same session keys as the
|
1068
|
+
primary configuration, which may be undesirable. To ensure session state is
|
1069
|
+
separated between configurations, you can set a session key prefix for
|
1070
|
+
alternate configurations. If you are using the remember feature in both
|
1071
|
+
configurations, you may also want to set a different remember key in the
|
1072
|
+
alternate configuration:
|
1073
|
+
|
1074
|
+
plugin :rodauth, :name=>:secondary do
|
1075
|
+
session_key_prefix "secondary_"
|
1076
|
+
remember_cookie_key "_secondary_remember"
|
1077
|
+
end
|
1078
|
+
|
1065
1079
|
=== With Password Hashes Inside the Accounts Table
|
1066
1080
|
|
1067
1081
|
You can use Rodauth if you are storing password hashes in the same
|
data/doc/base.rdoc
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
= Documentation for Base Feature
|
2
2
|
|
3
3
|
The base feature is automatically loaded when you use Rodauth. It contains
|
4
|
-
shared functionality that is used by multiple features.
|
4
|
+
shared functionality that is used by multiple features.
|
5
5
|
|
6
6
|
== Auth Value Methods
|
7
7
|
|
@@ -17,6 +17,7 @@ mark_input_fields_as_required? :: Whether input fields should be marked as requi
|
|
17
17
|
prefix :: The routing prefix used for Rodauth routes. If you are calling in a routing subtree, this should be set to the root path of the subtree. This should include a leading slash if set, but not a trailing slash.
|
18
18
|
require_bcrypt? :: Set to false to not require bcrypt, useful if using custom authentication.
|
19
19
|
session_key :: The key in the session hash storing the primary key of the logged in account.
|
20
|
+
session_key_prefix :: The string that will be prepended to the default value for all session keys.
|
20
21
|
skip_status_checks? :: Whether status checks should be skipped for accounts. Defaults to true unless enabling the verify_account or close_account features.
|
21
22
|
title_instance_variable :: The instance variable to set in the Roda scope with the page title. The layout should use this instance variable if available to set the title of the page. You can use +set_title+ if setting the page title is not done through an instance variable.
|
22
23
|
|
@@ -87,6 +88,7 @@ account_session_value :: The primary value of the current account to store in th
|
|
87
88
|
after_login :: Run arbitrary code after a successful login.
|
88
89
|
after_login_failure :: Run arbitrary code after a login failure due to an invalid password.
|
89
90
|
already_logged_in :: What action to take if you are already logged in and attempt to access a page that only makes sense if you are not logged in.
|
91
|
+
around_rodauth(&block) :: Run arbitrary code around handling any rodauth route. Call <tt>super(&block)</tt> for Rodauth to handle the action.
|
90
92
|
authenticated? :: Whether the user has been authenticated. If multifactor authentication has been enabled for the account, this is true only if the session is multifactor authenticated.
|
91
93
|
before_login :: Run arbitrary code after password has been checked, but before updating the session.
|
92
94
|
before_login_attempt :: Run arbitrary code after an account has been located, but before the password has been checked.
|
data/doc/jwt_refresh.rdoc
CHANGED
@@ -21,19 +21,27 @@ a value of <tt>all</tt> as the token value.
|
|
21
21
|
|
22
22
|
When using the refresh token, you must provide a valid access token, as that contains
|
23
23
|
information about the current session, which is used to create the new access token.
|
24
|
+
If you change the +allow_refresh_with_expired_jwt_access_token?+ setting to +true+,
|
25
|
+
an expired but otherwise valid access token will be accepted, and Rodauth will check
|
26
|
+
that the access token was issued in the same session as the refresh token.
|
24
27
|
|
25
28
|
This feature depends on the jwt feature.
|
26
29
|
|
27
30
|
== Auth Value Methods
|
28
31
|
|
32
|
+
allow_refresh_with_expired_jwt_access_token? :: Whether refreshing should be allowed with an expired access token. Default is +false+. You must set an +hmac_secret+ if setting this value to +true+.
|
33
|
+
expired_jwt_access_token_status :: The HTTP status code to use when a access token (JWT) is expired is submitted in the Authorization header. Default is 400 for backwards compatibility, and it is recommended to set it to 401.
|
34
|
+
expired_jwt_access_token_message :: The error message to use when a access token (JWT) is expired is submitted in the Authorization header.
|
29
35
|
jwt_access_token_key :: Name of the key in the response json holding the access token. Default is +access_token+.
|
30
36
|
jwt_access_token_not_before_period :: How many seconds before the current time will the jwt be considered valid (to account for inaccurate clocks). Default is 5.
|
31
37
|
jwt_access_token_period :: Validity of an access token in seconds, default is 1800 (30 minutes).
|
32
38
|
jwt_refresh_route :: The route to the login action. Defaults to <tt>jwt-refresh</tt>.
|
33
39
|
jwt_refresh_invalid_token_message :: Error message when the provided refresh token is non existent, invalid or expired.
|
34
40
|
jwt_refresh_token_account_id_column :: The column name in the +jwt_refresh_token_table+ storing the account id, should be a foreign key referencing the accounts table.
|
41
|
+
jwt_refresh_token_data_session_key :: The key in the session hash storing random data, for access checking during refresh if +allow_refresh_with_expired_jwt_access_token?+ is set.
|
35
42
|
jwt_refresh_token_deadline_column :: The column name in the +jwt_refresh_token_table+ storing the deadline after which the refresh token will no longer be valid.
|
36
43
|
jwt_refresh_token_deadline_interval :: Validity of a refresh token. Default is 14 days.
|
44
|
+
jwt_refresh_token_hmac_session_key :: The key in the session hash storing the hmac, for access checking during refresh if +allow_refresh_with_expired_jwt_access_token?+ is set.
|
37
45
|
jwt_refresh_token_id_column :: The column name in the refresh token keys table storing the id of each token (the primary key of the table).
|
38
46
|
jwt_refresh_token_key :: Name of the key in the response json holding the refresh token. Default is +refresh_token+.
|
39
47
|
jwt_refresh_token_key_column :: The column name in the +jwt_refresh_token_table+ holding the refresh token key value.
|
data/doc/login.rdoc
CHANGED
@@ -34,4 +34,5 @@ use_multi_phase_login? :: Whether to ask for login first, and only ask for passw
|
|
34
34
|
|
35
35
|
before_login_route :: Run arbitrary code before handling a login route.
|
36
36
|
login_view :: The HTML to use for the login form.
|
37
|
+
login_return_to_requested_location_path :: If +login_return_to_requested_location?+ is true, the path to use as the requested location. By default, uses the full path of the request for GET requests, and is nil for non-GET requests (in which case the default +login_redirect+ will be used).
|
37
38
|
multi_phase_login_view :: The HTML to use for the login form after login has been entered when using multi phase login.
|
@@ -0,0 +1,44 @@
|
|
1
|
+
= Documentation for Password Pepper Feature
|
2
|
+
|
3
|
+
The password pepper feature appends a specified secret string to passwords
|
4
|
+
before they are hashed. This way, if the password hashes get compromised, an
|
5
|
+
attacker cannot use them to crack the passwords without also knowing the
|
6
|
+
pepper.
|
7
|
+
|
8
|
+
In the configuration block set the +password_pepper+ with your secret string.
|
9
|
+
It's recommended for the password pepper to be at last 32 characters long and
|
10
|
+
randomly generated.
|
11
|
+
|
12
|
+
password_pepper "<long secret key>"
|
13
|
+
|
14
|
+
If your database already contains password hashes that were created without a
|
15
|
+
password pepper, these will get automatically updated with a password pepper
|
16
|
+
next time the user successfully enters their password.
|
17
|
+
|
18
|
+
You can rotate the password pepper as well, just make sure to add the previous
|
19
|
+
pepper to the +previous_password_peppers+ array. Password hashes using the old
|
20
|
+
pepper will get automatically updated on the next successful password match.
|
21
|
+
|
22
|
+
password_pepper "new pepper"
|
23
|
+
previous_password_peppers ["old pepper", ""]
|
24
|
+
|
25
|
+
The empty string above ensures password hashes without pepper are handled as
|
26
|
+
well.
|
27
|
+
|
28
|
+
Note that each entry in +previous_password_peppers+ will multiply the amount of
|
29
|
+
possible password checks during login, at least for incorrect passwords.
|
30
|
+
|
31
|
+
Additionally, when using this feature with the disallow_password_reuse feature,
|
32
|
+
the number of passwords checked when changing or resetting a password will be
|
33
|
+
|
34
|
+
(previous_password_peppers.length + 1) * previous_passwords_to_check
|
35
|
+
|
36
|
+
So if you have 2 entries in +previous_password_peppers+, using the default
|
37
|
+
value of 6 for +previous_passwords_to_check+, every time a password
|
38
|
+
is changed, there will be 18 password checks done, which will be quite slow.
|
39
|
+
|
40
|
+
== Auth Value Methods
|
41
|
+
|
42
|
+
password_pepper :: The secret string appended to passwords before they are hashed.
|
43
|
+
previous_password_peppers :: An array of password peppers that will be tried on an unsuccessful password match. Defaults to <tt>[""]</tt>, which allows introducing this feature with existing passwords.
|
44
|
+
password_pepper_update? :: Whether to update password hashes that use a pepper from +previous_password_peppers+ with a new pepper. Defaults to +true+.
|
data/doc/recovery_codes.rdoc
CHANGED
@@ -17,7 +17,8 @@ add_recovery_codes_error_flash :: The flash error to show when adding recovery c
|
|
17
17
|
add_recovery_codes_heading :: Text to use for heading above the form to add recovery codes.
|
18
18
|
add_recovery_codes_page_title :: The page title to use on the add recovery codes form.
|
19
19
|
add_recovery_codes_param :: The parameter name to use for adding recovery codes.
|
20
|
-
auto_add_recovery_codes? :: Whether to automatically add recovery codes (or any missing recovery codes) when
|
20
|
+
auto_add_recovery_codes? :: Whether to automatically add recovery codes (or any missing recovery codes) when enabling otp, webauthn, or sms authentication (false by default).
|
21
|
+
auto_remove_recovery_codes? :: Whether to automatically remove recovery codes when disabling otp, webauthn, or sms authentication and not having one of the other two authentication methods enabled (false by default).
|
21
22
|
invalid_recovery_code_error_flash :: The flash error to show when an invalid recovery code is used.
|
22
23
|
invalid_recovery_code_message :: The error message to show when an invalid recovery code is used.
|
23
24
|
recovery_auth_additional_form_tags :: HTML fragment containing additional form tags when authenticating via a recovery code.
|
@@ -0,0 +1,22 @@
|
|
1
|
+
= New Features
|
2
|
+
|
3
|
+
* A password_pepper feature has been added. This allows you to use a
|
4
|
+
secret key (called a pepper) to append to passwords before hashing
|
5
|
+
and hash checking. Using this approach, if an attacker obtains the
|
6
|
+
password hash, it is unusable for cracking unless they can also
|
7
|
+
get access to the pepper.
|
8
|
+
|
9
|
+
The password_pepper feature also supports a list of previous peppers
|
10
|
+
that can be used to implement secret rotation and to support
|
11
|
+
compatibility with unpeppered passwords.
|
12
|
+
|
13
|
+
Rodauth by default uses database functions for password hash
|
14
|
+
checking on PostgreSQL, MySQL, and Microsoft SQL Server, which in
|
15
|
+
general provides more security than a password pepper, but both
|
16
|
+
approaches can be used simultaneously.
|
17
|
+
|
18
|
+
* A session_key_prefix configuration method has been added for
|
19
|
+
prefixing the values of all default session keys. This can be
|
20
|
+
useful if you are using multiple Rodauth configurations in the same
|
21
|
+
application and want to make sure the session keys for the separate
|
22
|
+
configurations do not overlap.
|
@@ -0,0 +1,20 @@
|
|
1
|
+
= New Features
|
2
|
+
|
3
|
+
* A login_return_to_requested_location_path configuration method has
|
4
|
+
been added to the login feature. This controls the path to redirect
|
5
|
+
to if using login_return_to_requested_location?. By default, this
|
6
|
+
is the same as the fullpath of the request that required login if
|
7
|
+
that request was a GET request, and nil if that request was not a
|
8
|
+
GET request. Previously, the fullpath of that request was used even
|
9
|
+
if it was not a GET request, which caused problems as browsers use a
|
10
|
+
GET request for redirects, and it is a bad idea to redirect to a path
|
11
|
+
that may not handle GET requests.
|
12
|
+
|
13
|
+
* A change_login_needs_verification_notice_flash configuration method
|
14
|
+
has been added to the verify_login_change feature, for allowing
|
15
|
+
translations when using the feature and not using the
|
16
|
+
change_login_notice_flash configuration method.
|
17
|
+
|
18
|
+
= Other Improvements
|
19
|
+
|
20
|
+
* new_password_label is now translatable.
|
@@ -0,0 +1,37 @@
|
|
1
|
+
= New Features
|
2
|
+
|
3
|
+
* An around_rodauth configuration method has been added, which is
|
4
|
+
called around all Rodauth actions. This configuration method
|
5
|
+
is passed a block, and is useful for cases where you want to wrap
|
6
|
+
Rodauth's handling of the request.
|
7
|
+
|
8
|
+
For example, if you had a method named time_block in your Roda scope
|
9
|
+
that timed block execution and added a response header, you could
|
10
|
+
time Rodauth actions using something like:
|
11
|
+
|
12
|
+
around_rodauth do |&block|
|
13
|
+
scope.time_block('Rodauth') do
|
14
|
+
super(&block)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
* The allow_refresh_with_expired_jwt_access_token? configuration has
|
19
|
+
been added to the jwt_refresh feature, allowing refreshing with an
|
20
|
+
expired but otherwise valid access token. When using this method,
|
21
|
+
it is required to have an hmac_secret specified, so that Rodauth
|
22
|
+
can make sure the access token matches the refresh token.
|
23
|
+
|
24
|
+
= Other Improvements
|
25
|
+
|
26
|
+
* The javascript for setting up a WebAuthn token has been fixed to
|
27
|
+
allow it to work correctly if there is already an existing
|
28
|
+
WebAuthn token for the account.
|
29
|
+
|
30
|
+
* The rodauth.setup_account_verification method has been promoted to
|
31
|
+
public API. You can use this method for automatically sending
|
32
|
+
account verification emails when automatically creating accounts.
|
33
|
+
|
34
|
+
* Rodauth no longer loads the same feature multiple times into a
|
35
|
+
single configuration. This didn't cause any problems before, but
|
36
|
+
could result in duplicate entries when looking at the loaded
|
37
|
+
features.
|
@@ -0,0 +1,33 @@
|
|
1
|
+
= New Features
|
2
|
+
|
3
|
+
* An auto_remove_recovery_codes? configuration method has been added
|
4
|
+
to the recovery_codes feature. This will automatically remove
|
5
|
+
recovery codes when the last multifactor authentication type other
|
6
|
+
than the recovery codes has been removed.
|
7
|
+
|
8
|
+
* The jwt_access_expired_status and expired_jwt_access_token_message
|
9
|
+
configuration methods have been added to the jwt_refresh feature,
|
10
|
+
for supporting custom statuses and messages for expired tokens.
|
11
|
+
|
12
|
+
= Other Improvements
|
13
|
+
|
14
|
+
* Rodauth will no longer attempt to require a feature that has
|
15
|
+
already been required. Related to this is you can now use a
|
16
|
+
a custom Rodauth feature without a rodauth/features/*.rb file
|
17
|
+
in the Ruby library path, as long as you load the feature
|
18
|
+
manually.
|
19
|
+
|
20
|
+
* Rodauth now avoids method redefinition warnings in verbose
|
21
|
+
warning mode. As Ruby 3 is dropping uninitialized instance
|
22
|
+
variable warnings, Rodauth will be verbose warning free in
|
23
|
+
Ruby 3.
|
24
|
+
|
25
|
+
= Backwards Compatibility
|
26
|
+
|
27
|
+
* The default remember cookie path is now set to '/'. This fixes
|
28
|
+
usage in the case where rodauth is loaded under a subpath of the
|
29
|
+
application (which is not the default behavior). Unfortunately,
|
30
|
+
this change can negatively affect cases where multiple rodauth
|
31
|
+
configurations are used in separate paths on the same domain.
|
32
|
+
In these cases, you should now use remember_cookie_options and
|
33
|
+
include a :path option.
|
@@ -0,0 +1,20 @@
|
|
1
|
+
= Improvements
|
2
|
+
|
3
|
+
* HttpOnly is now set by default on the remember cookie, so it is no
|
4
|
+
longer accessible from Javascript. This is a more secure approach
|
5
|
+
that makes applications using Rodauth's remember feature less
|
6
|
+
vulnerable in case they are subject to a separate XSS attack.
|
7
|
+
|
8
|
+
* When using the jwt feature, rodauth.clear_session now clears the
|
9
|
+
JWT session even when the Roda sessions plugin was in use. In most
|
10
|
+
cases, the jwt feature is not used with the Roda sessions plugin,
|
11
|
+
but in cases where the same application serves as both an JSON API
|
12
|
+
and as a HTML site, it is possible the two may be used together.
|
13
|
+
|
14
|
+
= Backwards Compatibility
|
15
|
+
|
16
|
+
* As the default remember cookie :httponly setting is now set to true,
|
17
|
+
applications using Rodauth that expected to be able to access the
|
18
|
+
remember cookie from Javascript will no longer work by default.
|
19
|
+
In these cases, you should now use remember_cookie_options and
|
20
|
+
include a :httponly=>false option.
|
data/doc/remember.rdoc
CHANGED
@@ -35,7 +35,7 @@ raw_remember_token_deadline :: A deadline before which to allow a raw remember t
|
|
35
35
|
remember_additional_form_tags :: HTML fragment containing additional form tags to use on the change remember setting form.
|
36
36
|
remember_button :: The text to use for the change remember settings button.
|
37
37
|
remember_cookie_key :: The cookie name to use for the remember token.
|
38
|
-
remember_cookie_options :: Any options to set for the remember cookie.
|
38
|
+
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`.
|
39
39
|
remember_deadline_column :: The column name in the +remember_table+ storing the deadline after which the token will be ignored.
|
40
40
|
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
41
|
remember_disable_label :: The label for disabling remembering.
|
@@ -14,6 +14,7 @@ control. Depends on the change login and email base features.
|
|
14
14
|
== Auth Value Methods
|
15
15
|
|
16
16
|
no_matching_verify_login_change_key_error_flash :: The flash error message to show when an invalid verify login change key is used.
|
17
|
+
change_login_needs_verification_notice_flash :: The flash notice to show after changing a login when using this feature, if +change_login_notice_flash+ is not overridden.
|
17
18
|
verify_login_change_additional_form_tags :: HTML fragment containing additional form tags to use on the verify login change form.
|
18
19
|
verify_login_change_autologin? :: Whether to autologin the user after successful login change verification, false by default.
|
19
20
|
verify_login_change_button :: The text to use for the verify login change button.
|
data/javascript/webauthn_auth.js
CHANGED
@@ -1,34 +1,34 @@
|
|
1
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)); };
|
2
4
|
var element = document.getElementById('webauthn-auth-form');
|
3
5
|
var f = function(e) {
|
4
6
|
//console.log(e);
|
5
7
|
e.preventDefault();
|
6
8
|
if (navigator.credentials) {
|
7
9
|
var opts = JSON.parse(element.getAttribute("data-credential-options"));
|
8
|
-
opts.challenge =
|
9
|
-
opts.allowCredentials.forEach(function(cred) {
|
10
|
-
cred.id = Uint8Array.from(atob(cred.id.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0));
|
11
|
-
});
|
10
|
+
opts.challenge = unpack(opts.challenge);
|
11
|
+
opts.allowCredentials.forEach(function(cred) { cred.id = unpack(cred.id); });
|
12
12
|
//console.log(opts);
|
13
13
|
navigator.credentials.get({publicKey: opts}).
|
14
14
|
then(function(cred){
|
15
15
|
//console.log(cred);
|
16
16
|
//window.cred = cred
|
17
17
|
|
18
|
-
var rawId =
|
18
|
+
var rawId = pack(cred.rawId);
|
19
19
|
var authValue = {
|
20
20
|
type: cred.type,
|
21
21
|
id: rawId,
|
22
22
|
rawId: rawId,
|
23
23
|
response: {
|
24
|
-
authenticatorData:
|
25
|
-
clientDataJSON:
|
26
|
-
signature:
|
24
|
+
authenticatorData: pack(cred.response.authenticatorData),
|
25
|
+
clientDataJSON: pack(cred.response.clientDataJSON),
|
26
|
+
signature: pack(cred.response.signature)
|
27
27
|
}
|
28
28
|
};
|
29
29
|
|
30
30
|
if (cred.response.userHandle) {
|
31
|
-
authValue.response.userHandle =
|
31
|
+
authValue.response.userHandle = pack(cred.response.userHandle);
|
32
32
|
}
|
33
33
|
|
34
34
|
document.getElementById('webauthn-auth').value = JSON.stringify(authValue);
|
@@ -1,26 +1,29 @@
|
|
1
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)); };
|
2
4
|
var element = document.getElementById('webauthn-setup-form');
|
3
5
|
var f = function(e) {
|
4
6
|
//console.log(e);
|
5
7
|
e.preventDefault();
|
6
8
|
if (navigator.credentials) {
|
7
9
|
var opts = JSON.parse(element.getAttribute("data-credential-options"));
|
8
|
-
opts.challenge =
|
9
|
-
opts.user.id =
|
10
|
+
opts.challenge = unpack(opts.challenge);
|
11
|
+
opts.user.id = unpack(opts.user.id);
|
12
|
+
opts.excludeCredentials.forEach(function(cred) { cred.id = unpack(cred.id); });
|
10
13
|
//console.log(opts);
|
11
14
|
navigator.credentials.create({publicKey: opts}).
|
12
15
|
then(function(cred){
|
13
16
|
//console.log(cred);
|
14
17
|
//window.cred = cred
|
15
|
-
|
16
|
-
var rawId =
|
18
|
+
|
19
|
+
var rawId = pack(cred.rawId);
|
17
20
|
document.getElementById('webauthn-setup').value = JSON.stringify({
|
18
21
|
type: cred.type,
|
19
22
|
id: rawId,
|
20
23
|
rawId: rawId,
|
21
24
|
response: {
|
22
|
-
attestationObject:
|
23
|
-
clientDataJSON:
|
25
|
+
attestationObject: pack(cred.response.attestationObject),
|
26
|
+
clientDataJSON: pack(cred.response.clientDataJSON)
|
24
27
|
}
|
25
28
|
});
|
26
29
|
element.removeEventListener("submit", f);
|
data/lib/rodauth.rb
CHANGED
@@ -66,6 +66,7 @@ module Rodauth
|
|
66
66
|
define_method(meth) do |&block|
|
67
67
|
@auth.send(:define_method, meth, &block)
|
68
68
|
@auth.send(:private, meth) if priv
|
69
|
+
@auth.send(:alias_method, meth, meth)
|
69
70
|
end
|
70
71
|
end
|
71
72
|
|
@@ -74,6 +75,7 @@ module Rodauth
|
|
74
75
|
define_method(meth) do |&block|
|
75
76
|
@auth.send(:define_method, umeth, &block)
|
76
77
|
@auth.send(:private, umeth)
|
78
|
+
@auth.send(:alias_method, umeth, umeth)
|
77
79
|
end
|
78
80
|
end
|
79
81
|
|
@@ -82,6 +84,7 @@ module Rodauth
|
|
82
84
|
block ||= proc{v}
|
83
85
|
@auth.send(:define_method, meth, &block)
|
84
86
|
@auth.send(:private, meth) if priv
|
87
|
+
@auth.send(:alias_method, meth, meth)
|
85
88
|
end
|
86
89
|
end
|
87
90
|
end
|
@@ -120,8 +123,10 @@ module Rodauth
|
|
120
123
|
define_method(handle_meth) do
|
121
124
|
request.is send(route_meth) do
|
122
125
|
check_csrf if check_csrf?
|
123
|
-
|
124
|
-
|
126
|
+
_around_rodauth do
|
127
|
+
before_rodauth
|
128
|
+
send(internal_handle_meth, request)
|
129
|
+
end
|
125
130
|
end
|
126
131
|
end
|
127
132
|
|
@@ -238,6 +243,7 @@ module Rodauth
|
|
238
243
|
instance_variable_set(iv, send(umeth))
|
239
244
|
end
|
240
245
|
end
|
246
|
+
alias_method(meth, meth)
|
241
247
|
auth_private_methods(meth)
|
242
248
|
end
|
243
249
|
|
@@ -288,15 +294,17 @@ module Rodauth
|
|
288
294
|
end
|
289
295
|
|
290
296
|
def enable(*features)
|
291
|
-
|
292
|
-
|
293
|
-
|
297
|
+
features.each do |feature|
|
298
|
+
next if @auth.features.include?(feature)
|
299
|
+
load_feature(feature)
|
300
|
+
@auth.features << feature
|
301
|
+
end
|
294
302
|
end
|
295
303
|
|
296
304
|
private
|
297
305
|
|
298
306
|
def load_feature(feature_name)
|
299
|
-
require "rodauth/features/#{feature_name}"
|
307
|
+
require "rodauth/features/#{feature_name}" unless FEATURES[feature_name]
|
300
308
|
feature = FEATURES[feature_name]
|
301
309
|
enable(*feature.dependencies)
|
302
310
|
extend feature.configuration
|
@@ -47,6 +47,7 @@ module Rodauth
|
|
47
47
|
session_key :authenticated_by_session_key, :authenticated_by
|
48
48
|
session_key :autologin_type_session_key, :autologin_type
|
49
49
|
auth_value_method :prefix, ''
|
50
|
+
auth_value_method :session_key_prefix, nil
|
50
51
|
auth_value_method :require_bcrypt?, true
|
51
52
|
auth_value_method :mark_input_fields_as_required?, true
|
52
53
|
auth_value_method :mark_input_fields_with_autocomplete?, true
|
@@ -101,7 +102,6 @@ module Rodauth
|
|
101
102
|
:set_redirect_error_flash,
|
102
103
|
:set_title,
|
103
104
|
:translate,
|
104
|
-
:unverified_account_message,
|
105
105
|
:update_session
|
106
106
|
)
|
107
107
|
|
@@ -110,7 +110,8 @@ module Rodauth
|
|
110
110
|
:account_from_session,
|
111
111
|
:field_attributes,
|
112
112
|
:field_error_attributes,
|
113
|
-
:formatted_field_error
|
113
|
+
:formatted_field_error,
|
114
|
+
:around_rodauth
|
114
115
|
)
|
115
116
|
|
116
117
|
configuration_module_eval do
|
@@ -259,6 +260,7 @@ module Rodauth
|
|
259
260
|
@password_field_autocomplete_value || 'current-password'
|
260
261
|
end
|
261
262
|
|
263
|
+
alias account_password_hash_column account_password_hash_column
|
262
264
|
# If the account_password_hash_column is set, the password hash is verified in
|
263
265
|
# ruby, it will not use a database function to do so, it will check the password
|
264
266
|
# hash using bcrypt.
|
@@ -393,9 +395,9 @@ module Rodauth
|
|
393
395
|
def password_match?(password)
|
394
396
|
if hash = get_password_hash
|
395
397
|
if account_password_hash_column || !use_database_authentication_functions?
|
396
|
-
|
398
|
+
password_hash_match?(hash, password)
|
397
399
|
else
|
398
|
-
|
400
|
+
database_function_password_match?(:rodauth_valid_password_hash, account_id, password, hash)
|
399
401
|
end
|
400
402
|
end
|
401
403
|
end
|
@@ -458,6 +460,18 @@ module Rodauth
|
|
458
460
|
|
459
461
|
private
|
460
462
|
|
463
|
+
def _around_rodauth
|
464
|
+
yield
|
465
|
+
end
|
466
|
+
|
467
|
+
def database_function_password_match?(name, hash_id, password, salt)
|
468
|
+
db.get(Sequel.function(function_name(name), hash_id, BCrypt::Engine.hash_secret(password, salt)))
|
469
|
+
end
|
470
|
+
|
471
|
+
def password_hash_match?(hash, password)
|
472
|
+
BCrypt::Password.new(hash) == password
|
473
|
+
end
|
474
|
+
|
461
475
|
def convert_token_key(key)
|
462
476
|
if key && hmac_secret
|
463
477
|
compute_hmac(key)
|
@@ -493,6 +507,7 @@ module Rodauth
|
|
493
507
|
end
|
494
508
|
|
495
509
|
def convert_session_key(key)
|
510
|
+
key = "#{session_key_prefix}#{key}".to_sym if session_key_prefix
|
496
511
|
scope.opts[:sessions_convert_symbols] ? key.to_s : key
|
497
512
|
end
|
498
513
|
|
@@ -26,11 +26,11 @@ module Rodauth
|
|
26
26
|
require_account_session
|
27
27
|
before_confirm_password_route
|
28
28
|
|
29
|
-
|
29
|
+
r.get do
|
30
30
|
confirm_password_view
|
31
31
|
end
|
32
32
|
|
33
|
-
|
33
|
+
r.post do
|
34
34
|
if password_match?(param(password_param))
|
35
35
|
transaction do
|
36
36
|
before_confirm_password
|
@@ -51,11 +51,13 @@ module Rodauth
|
|
51
51
|
return true if salts.empty?
|
52
52
|
|
53
53
|
salts.any? do |hash_id, salt|
|
54
|
-
|
54
|
+
database_function_password_match?(:rodauth_previous_password_hash_match, hash_id, password, salt)
|
55
55
|
end
|
56
56
|
else
|
57
57
|
# :nocov:
|
58
|
-
previous_password_ds.select_map(previous_password_hash_column).any?
|
58
|
+
previous_password_ds.select_map(previous_password_hash_column).any? do |hash|
|
59
|
+
password_hash_match?(hash, password)
|
60
|
+
end
|
59
61
|
# :nocov:
|
60
62
|
end
|
61
63
|
|
data/lib/rodauth/features/jwt.rb
CHANGED
@@ -47,7 +47,7 @@ module Rodauth
|
|
47
47
|
s = {}
|
48
48
|
if jwt_token
|
49
49
|
unless session_data = jwt_payload
|
50
|
-
json_response[json_response_error_key]
|
50
|
+
json_response[json_response_error_key] ||= invalid_jwt_format_error_message
|
51
51
|
response.status ||= json_response_error_status
|
52
52
|
response['Content-Type'] ||= json_response_content_type
|
53
53
|
response.write(_json_response_body(json_response))
|
@@ -73,7 +73,10 @@ module Rodauth
|
|
73
73
|
|
74
74
|
def clear_session
|
75
75
|
super
|
76
|
-
|
76
|
+
if use_jwt?
|
77
|
+
session.clear
|
78
|
+
set_jwt
|
79
|
+
end
|
77
80
|
end
|
78
81
|
|
79
82
|
def set_field_error(field, message)
|
@@ -229,10 +232,18 @@ module Rodauth
|
|
229
232
|
end
|
230
233
|
end
|
231
234
|
|
235
|
+
def _jwt_decode_opts
|
236
|
+
jwt_decode_opts
|
237
|
+
end
|
238
|
+
|
232
239
|
def jwt_payload
|
233
240
|
return @jwt_payload if defined?(@jwt_payload)
|
234
|
-
@jwt_payload = JWT.decode(jwt_token, jwt_secret, true,
|
235
|
-
rescue JWT::DecodeError
|
241
|
+
@jwt_payload = JWT.decode(jwt_token, jwt_secret, true, _jwt_decode_opts.merge(:algorithm=>jwt_algorithm))[0]
|
242
|
+
rescue JWT::DecodeError => e
|
243
|
+
rescue_jwt_payload(e)
|
244
|
+
end
|
245
|
+
|
246
|
+
def rescue_jwt_payload(_)
|
236
247
|
@jwt_payload = false
|
237
248
|
end
|
238
249
|
|
@@ -7,6 +7,9 @@ module Rodauth
|
|
7
7
|
after 'refresh_token'
|
8
8
|
before 'refresh_token'
|
9
9
|
|
10
|
+
auth_value_method :allow_refresh_with_expired_jwt_access_token?, false
|
11
|
+
session_key :jwt_refresh_token_data_session_key, :jwt_refresh_token_data
|
12
|
+
session_key :jwt_refresh_token_hmac_session_key, :jwt_refresh_token_hash
|
10
13
|
auth_value_method :jwt_access_token_key, 'access_token'
|
11
14
|
auth_value_method :jwt_access_token_not_before_period, 5
|
12
15
|
auth_value_method :jwt_access_token_period, 1800
|
@@ -21,12 +24,15 @@ module Rodauth
|
|
21
24
|
auth_value_method :jwt_refresh_token_table, :account_jwt_refresh_keys
|
22
25
|
translatable_method :jwt_refresh_without_access_token_message, 'no JWT access token provided during refresh'
|
23
26
|
auth_value_method :jwt_refresh_without_access_token_status, 401
|
27
|
+
translatable_method :expired_jwt_access_token_message, "expired JWT access token"
|
28
|
+
auth_value_method :expired_jwt_access_token_status, 400
|
24
29
|
|
25
30
|
auth_private_methods(
|
26
31
|
:account_from_refresh_token
|
27
32
|
)
|
28
33
|
|
29
34
|
route do |r|
|
35
|
+
@jwt_refresh_route = true
|
30
36
|
before_jwt_refresh_route
|
31
37
|
|
32
38
|
r.post do
|
@@ -38,6 +44,7 @@ module Rodauth
|
|
38
44
|
before_refresh_token
|
39
45
|
formatted_token = generate_refresh_token
|
40
46
|
remove_jwt_refresh_token_key(refresh_token)
|
47
|
+
set_jwt_refresh_token_hmac_session_key(formatted_token)
|
41
48
|
json_response[jwt_refresh_token_key] = formatted_token
|
42
49
|
json_response[jwt_access_token_key] = session_jwt
|
43
50
|
after_refresh_token
|
@@ -58,7 +65,9 @@ module Rodauth
|
|
58
65
|
# JWT login puts the access token in the header.
|
59
66
|
# We put the refresh token in the body.
|
60
67
|
# Note, do not put the access_token in the body here, as the access token content is not yet finalised.
|
61
|
-
json_response[
|
68
|
+
token = json_response[jwt_refresh_token_key] = generate_refresh_token
|
69
|
+
|
70
|
+
set_jwt_refresh_token_hmac_session_key(token)
|
62
71
|
end
|
63
72
|
|
64
73
|
def set_jwt_token(token)
|
@@ -85,12 +94,33 @@ module Rodauth
|
|
85
94
|
|
86
95
|
private
|
87
96
|
|
97
|
+
def rescue_jwt_payload(e)
|
98
|
+
if e.instance_of?(JWT::ExpiredSignature)
|
99
|
+
begin
|
100
|
+
# Some versions of jwt will raise JWT::ExpiredSignature even when the
|
101
|
+
# JWT is invalid for other reasons. Make sure the expiration is the
|
102
|
+
# only reason the JWT isn't valid before treating this as an expired token.
|
103
|
+
JWT.decode(jwt_token, jwt_secret, true, Hash[jwt_decode_opts].merge!(:verify_expiration=>false, :algorithm=>jwt_algorithm))[0]
|
104
|
+
rescue => e
|
105
|
+
else
|
106
|
+
json_response[json_response_error_key] = expired_jwt_access_token_message
|
107
|
+
response.status ||= expired_jwt_access_token_status
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
super
|
112
|
+
end
|
113
|
+
|
88
114
|
def _account_from_refresh_token(token)
|
89
115
|
id, token_id, key = _account_refresh_token_split(token)
|
90
116
|
|
91
|
-
|
92
|
-
|
93
|
-
|
117
|
+
unless key &&
|
118
|
+
(id == session_value.to_s) &&
|
119
|
+
(actual = get_active_refresh_token(id, token_id)) &&
|
120
|
+
timing_safe_eql?(key, convert_token_key(actual)) &&
|
121
|
+
jwt_refresh_token_match?(key)
|
122
|
+
return
|
123
|
+
end
|
94
124
|
|
95
125
|
ds = account_ds(id)
|
96
126
|
ds = ds.where(account_status_column=>account_open_status_value) unless skip_status_checks?
|
@@ -107,6 +137,23 @@ module Rodauth
|
|
107
137
|
[id, token_id, key]
|
108
138
|
end
|
109
139
|
|
140
|
+
def _jwt_decode_opts
|
141
|
+
if allow_refresh_with_expired_jwt_access_token? && @jwt_refresh_route
|
142
|
+
Hash[super].merge!(:verify_expiration=>false)
|
143
|
+
else
|
144
|
+
super
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def jwt_refresh_token_match?(key)
|
149
|
+
# We don't need to match tokens if we are requiring a valid current access token
|
150
|
+
return true unless allow_refresh_with_expired_jwt_access_token?
|
151
|
+
|
152
|
+
# If allowing with expired jwt access token, check the expired session contains
|
153
|
+
# hmac matching submitted and active refresh token.
|
154
|
+
timing_safe_eql?(compute_hmac(session[jwt_refresh_token_data_session_key].to_s + key), session[jwt_refresh_token_hmac_session_key].to_s)
|
155
|
+
end
|
156
|
+
|
110
157
|
def get_active_refresh_token(account_id, token_id)
|
111
158
|
jwt_refresh_token_account_ds(account_id).
|
112
159
|
where(Sequel::CURRENT_TIMESTAMP > jwt_refresh_token_deadline_column).
|
@@ -130,8 +177,7 @@ module Rodauth
|
|
130
177
|
end
|
131
178
|
|
132
179
|
def remove_jwt_refresh_token_key(token)
|
133
|
-
account_id,
|
134
|
-
token_id, _ = split_token(token)
|
180
|
+
account_id, token_id, _ = _account_refresh_token_split(token)
|
135
181
|
jwt_refresh_token_account_token_ds(account_id, token_id).delete
|
136
182
|
end
|
137
183
|
|
@@ -146,6 +192,15 @@ module Rodauth
|
|
146
192
|
hash
|
147
193
|
end
|
148
194
|
|
195
|
+
def set_jwt_refresh_token_hmac_session_key(token)
|
196
|
+
if allow_refresh_with_expired_jwt_access_token?
|
197
|
+
key = _account_refresh_token_split(token).last
|
198
|
+
data = random_key
|
199
|
+
set_session_value(jwt_refresh_token_data_session_key, data)
|
200
|
+
set_session_value(jwt_refresh_token_hmac_session_key, compute_hmac(data + key))
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
149
204
|
def before_logout
|
150
205
|
if token = param_or_nil(jwt_refresh_token_key_param)
|
151
206
|
if token == 'all'
|
@@ -154,9 +209,7 @@ module Rodauth
|
|
154
209
|
id, token_id, key = _account_refresh_token_split(token)
|
155
210
|
|
156
211
|
if id && token_id && key && (actual = get_active_refresh_token(session_value, token_id)) && timing_safe_eql?(key, convert_token_key(actual))
|
157
|
-
|
158
|
-
where(jwt_refresh_token_id_column=>token_id).
|
159
|
-
delete
|
212
|
+
jwt_refresh_token_account_token_ds(id, token_id).delete
|
160
213
|
end
|
161
214
|
end
|
162
215
|
end
|
@@ -23,6 +23,8 @@ module Rodauth
|
|
23
23
|
auth_cached_method :login_form_footer_links
|
24
24
|
auth_cached_method :login_form_footer
|
25
25
|
|
26
|
+
auth_value_methods :login_return_to_requested_location_path
|
27
|
+
|
26
28
|
route do |r|
|
27
29
|
check_already_logged_in
|
28
30
|
before_login_route
|
@@ -85,12 +87,16 @@ module Rodauth
|
|
85
87
|
end
|
86
88
|
|
87
89
|
def login_required
|
88
|
-
if login_return_to_requested_location?
|
89
|
-
set_session_value(login_redirect_session_key,
|
90
|
+
if login_return_to_requested_location? && (path = login_return_to_requested_location_path)
|
91
|
+
set_session_value(login_redirect_session_key, path)
|
90
92
|
end
|
91
93
|
super
|
92
94
|
end
|
93
95
|
|
96
|
+
def login_return_to_requested_location_path
|
97
|
+
request.fullpath if request.get?
|
98
|
+
end
|
99
|
+
|
94
100
|
def after_login_entered_during_multi_phase_login
|
95
101
|
set_notice_now_flash need_password_notice_flash
|
96
102
|
if multi_phase_login_forms.length == 1 && (meth = multi_phase_login_forms[0][2])
|
data/lib/rodauth/features/otp.rb
CHANGED
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
module Rodauth
|
4
|
+
Feature.define(:password_pepper, :PasswordPepper) do
|
5
|
+
depends :login_password_requirements_base
|
6
|
+
|
7
|
+
auth_value_method :password_pepper, nil
|
8
|
+
auth_value_method :previous_password_peppers, [""]
|
9
|
+
auth_value_method :password_pepper_update?, true
|
10
|
+
|
11
|
+
def password_match?(password)
|
12
|
+
if (result = super) && @previous_pepper_matched && password_pepper_update?
|
13
|
+
set_password(password)
|
14
|
+
end
|
15
|
+
|
16
|
+
result
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def password_hash(password)
|
22
|
+
super(password + password_pepper.to_s)
|
23
|
+
end
|
24
|
+
|
25
|
+
def password_hash_match?(hash, password)
|
26
|
+
return super if password_pepper.nil?
|
27
|
+
|
28
|
+
return true if super(hash, password + password_pepper)
|
29
|
+
|
30
|
+
@previous_pepper_matched = previous_password_peppers.any? do |pepper|
|
31
|
+
super(hash, password + pepper)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def database_function_password_match?(name, hash_id, password, salt)
|
36
|
+
return super if password_pepper.nil?
|
37
|
+
|
38
|
+
return true if super(name, hash_id, password + password_pepper, salt)
|
39
|
+
|
40
|
+
@previous_pepper_matched = previous_password_peppers.any? do |pepper|
|
41
|
+
super(name, hash_id, password + pepper, salt)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -34,6 +34,7 @@ module Rodauth
|
|
34
34
|
auth_value_method :add_recovery_codes_param, 'add'
|
35
35
|
translatable_method :add_recovery_codes_heading, '<h2>Add Additional Recovery Codes</h2>'
|
36
36
|
auth_value_method :auto_add_recovery_codes?, false
|
37
|
+
auth_value_method :auto_remove_recovery_codes?, false
|
37
38
|
translatable_method :invalid_recovery_code_message, "Invalid recovery code"
|
38
39
|
auth_value_method :recovery_codes_limit, 16
|
39
40
|
auth_value_method :recovery_codes_column, :code
|
@@ -56,7 +57,6 @@ module Rodauth
|
|
56
57
|
:can_add_recovery_codes?,
|
57
58
|
:new_recovery_code,
|
58
59
|
:recovery_code_match?,
|
59
|
-
:recovery_codes
|
60
60
|
)
|
61
61
|
|
62
62
|
route(:recovery_auth) do |r|
|
@@ -213,6 +213,21 @@ module Rodauth
|
|
213
213
|
super
|
214
214
|
end
|
215
215
|
|
216
|
+
def after_otp_disable
|
217
|
+
super if defined?(super)
|
218
|
+
auto_remove_recovery_codes
|
219
|
+
end
|
220
|
+
|
221
|
+
def after_sms_disable
|
222
|
+
super if defined?(super)
|
223
|
+
auto_remove_recovery_codes
|
224
|
+
end
|
225
|
+
|
226
|
+
def after_webauthn_remove
|
227
|
+
super if defined?(super)
|
228
|
+
auto_remove_recovery_codes
|
229
|
+
end
|
230
|
+
|
216
231
|
def new_recovery_code
|
217
232
|
random_key
|
218
233
|
end
|
@@ -227,6 +242,12 @@ module Rodauth
|
|
227
242
|
end
|
228
243
|
end
|
229
244
|
|
245
|
+
def auto_remove_recovery_codes
|
246
|
+
if auto_remove_recovery_codes? && (%w'totp webauthn sms_code' & possible_authentication_methods).empty?
|
247
|
+
recovery_codes_remove
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
230
251
|
def _recovery_codes
|
231
252
|
recovery_codes_ds.select_map(recovery_codes_column)
|
232
253
|
end
|
@@ -58,7 +58,9 @@ module Rodauth
|
|
58
58
|
if [remember_remember_param_value, remember_forget_param_value, remember_disable_param_value].include?(remember)
|
59
59
|
transaction do
|
60
60
|
before_remember
|
61
|
+
# :nocov:
|
61
62
|
case remember
|
63
|
+
# :nocov:
|
62
64
|
when remember_remember_param_value
|
63
65
|
remember_login
|
64
66
|
when remember_forget_param_value
|
@@ -130,11 +132,15 @@ module Rodauth
|
|
130
132
|
opts = Hash[remember_cookie_options]
|
131
133
|
opts[:value] = "#{account_id}_#{convert_token_key(remember_key_value)}"
|
132
134
|
opts[:expires] = convert_timestamp(active_remember_key_ds.get(remember_deadline_column))
|
135
|
+
opts[:path] = "/" unless opts.key?(:path)
|
136
|
+
opts[:httponly] = true unless opts.key?(:httponly)
|
133
137
|
::Rack::Utils.set_cookie_header!(response.headers, remember_cookie_key, opts)
|
134
138
|
end
|
135
139
|
|
136
140
|
def forget_login
|
137
|
-
|
141
|
+
opts = Hash[remember_cookie_options]
|
142
|
+
opts[:path] = "/" unless opts.key?(:path)
|
143
|
+
::Rack::Utils.delete_cookie_header!(response.headers, remember_cookie_key, opts)
|
138
144
|
end
|
139
145
|
|
140
146
|
def get_remember_key
|
@@ -47,7 +47,6 @@ module Rodauth
|
|
47
47
|
:get_verify_account_key,
|
48
48
|
:get_verify_account_email_last_sent,
|
49
49
|
:remove_verify_account_key,
|
50
|
-
:resend_verify_account_view,
|
51
50
|
:send_verify_account_email,
|
52
51
|
:set_verify_account_email_last_sent,
|
53
52
|
:verify_account,
|
@@ -245,6 +244,12 @@ module Rodauth
|
|
245
244
|
end
|
246
245
|
end
|
247
246
|
|
247
|
+
def setup_account_verification
|
248
|
+
generate_verify_account_key_value
|
249
|
+
create_verify_account_key
|
250
|
+
send_verify_account_email
|
251
|
+
end
|
252
|
+
|
248
253
|
private
|
249
254
|
|
250
255
|
def _login_form_footer_links
|
@@ -276,12 +281,6 @@ module Rodauth
|
|
276
281
|
super
|
277
282
|
end
|
278
283
|
|
279
|
-
def setup_account_verification
|
280
|
-
generate_verify_account_key_value
|
281
|
-
create_verify_account_key
|
282
|
-
send_verify_account_email
|
283
|
-
end
|
284
|
-
|
285
284
|
def verify_account_check_already_logged_in
|
286
285
|
check_already_logged_in
|
287
286
|
end
|
@@ -8,6 +8,7 @@ module Rodauth
|
|
8
8
|
error_flash "Unable to change login as there is already an account with the new login", 'verify_login_change_duplicate_account'
|
9
9
|
error_flash "There was an error verifying your login change: invalid verify login change key", 'no_matching_verify_login_change_key'
|
10
10
|
notice_flash "Your login change has been verified"
|
11
|
+
notice_flash "An email has been sent to you with a link to verify your login change", 'change_login_needs_verification'
|
11
12
|
loaded_templates %w'verify-login-change verify-login-change-email'
|
12
13
|
view 'verify-login-change', 'Verify Login Change'
|
13
14
|
additional_form_tags
|
@@ -131,7 +132,7 @@ module Rodauth
|
|
131
132
|
end
|
132
133
|
|
133
134
|
def change_login_notice_flash
|
134
|
-
|
135
|
+
change_login_needs_verification_notice_flash
|
135
136
|
end
|
136
137
|
|
137
138
|
def verify_login_change_old_login
|
data/lib/rodauth/version.rb
CHANGED
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.8.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:
|
11
|
+
date: 2021-01-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sequel
|
@@ -237,28 +237,33 @@ extra_rdoc_files:
|
|
237
237
|
- README.rdoc
|
238
238
|
- CHANGELOG
|
239
239
|
- MIT-LICENSE
|
240
|
-
- doc/change_password_notify.rdoc
|
241
240
|
- doc/account_expiration.rdoc
|
241
|
+
- doc/active_sessions.rdoc
|
242
|
+
- doc/audit_logging.rdoc
|
242
243
|
- doc/base.rdoc
|
243
244
|
- doc/change_login.rdoc
|
244
245
|
- doc/change_password.rdoc
|
245
|
-
- doc/
|
246
|
+
- doc/change_password_notify.rdoc
|
246
247
|
- doc/close_account.rdoc
|
247
|
-
- doc/
|
248
|
+
- doc/confirm_password.rdoc
|
248
249
|
- doc/create_account.rdoc
|
249
|
-
- doc/email_base.rdoc
|
250
250
|
- doc/disallow_common_passwords.rdoc
|
251
251
|
- doc/disallow_password_reuse.rdoc
|
252
|
-
- doc/
|
252
|
+
- doc/email_auth.rdoc
|
253
|
+
- doc/email_base.rdoc
|
254
|
+
- doc/http_basic_auth.rdoc
|
253
255
|
- doc/jwt.rdoc
|
256
|
+
- doc/jwt_cors.rdoc
|
257
|
+
- doc/jwt_refresh.rdoc
|
254
258
|
- doc/lockout.rdoc
|
255
259
|
- doc/login.rdoc
|
260
|
+
- doc/login_password_requirements_base.rdoc
|
256
261
|
- doc/logout.rdoc
|
257
262
|
- doc/otp.rdoc
|
258
|
-
- doc/
|
259
|
-
- doc/jwt_cors.rdoc
|
263
|
+
- doc/password_complexity.rdoc
|
260
264
|
- doc/password_expiration.rdoc
|
261
265
|
- doc/password_grace_period.rdoc
|
266
|
+
- doc/password_pepper.rdoc
|
262
267
|
- doc/recovery_codes.rdoc
|
263
268
|
- doc/remember.rdoc
|
264
269
|
- doc/reset_password.rdoc
|
@@ -268,16 +273,11 @@ extra_rdoc_files:
|
|
268
273
|
- doc/two_factor_base.rdoc
|
269
274
|
- doc/update_password_hash.rdoc
|
270
275
|
- doc/verify_account.rdoc
|
271
|
-
- doc/email_auth.rdoc
|
272
|
-
- doc/jwt_refresh.rdoc
|
273
276
|
- doc/verify_account_grace_period.rdoc
|
274
277
|
- doc/verify_login_change.rdoc
|
275
278
|
- doc/webauthn.rdoc
|
276
279
|
- doc/webauthn_login.rdoc
|
277
280
|
- doc/webauthn_verify_account.rdoc
|
278
|
-
- doc/active_sessions.rdoc
|
279
|
-
- doc/audit_logging.rdoc
|
280
|
-
- doc/release_notes/1.17.0.txt
|
281
281
|
- doc/release_notes/1.0.0.txt
|
282
282
|
- doc/release_notes/1.1.0.txt
|
283
283
|
- doc/release_notes/1.10.0.txt
|
@@ -287,7 +287,14 @@ extra_rdoc_files:
|
|
287
287
|
- doc/release_notes/1.14.0.txt
|
288
288
|
- doc/release_notes/1.15.0.txt
|
289
289
|
- doc/release_notes/1.16.0.txt
|
290
|
+
- doc/release_notes/1.17.0.txt
|
291
|
+
- doc/release_notes/1.18.0.txt
|
292
|
+
- doc/release_notes/1.19.0.txt
|
290
293
|
- doc/release_notes/1.2.0.txt
|
294
|
+
- doc/release_notes/1.20.0.txt
|
295
|
+
- doc/release_notes/1.21.0.txt
|
296
|
+
- doc/release_notes/1.22.0.txt
|
297
|
+
- doc/release_notes/1.23.0.txt
|
291
298
|
- doc/release_notes/1.3.0.txt
|
292
299
|
- doc/release_notes/1.4.0.txt
|
293
300
|
- doc/release_notes/1.5.0.txt
|
@@ -295,16 +302,15 @@ extra_rdoc_files:
|
|
295
302
|
- doc/release_notes/1.7.0.txt
|
296
303
|
- doc/release_notes/1.8.0.txt
|
297
304
|
- doc/release_notes/1.9.0.txt
|
298
|
-
- doc/release_notes/1.18.0.txt
|
299
|
-
- doc/release_notes/1.19.0.txt
|
300
|
-
- doc/release_notes/1.20.0.txt
|
301
|
-
- doc/release_notes/1.21.0.txt
|
302
|
-
- doc/release_notes/1.22.0.txt
|
303
|
-
- doc/release_notes/1.23.0.txt
|
304
305
|
- doc/release_notes/2.0.0.txt
|
305
306
|
- doc/release_notes/2.1.0.txt
|
306
307
|
- doc/release_notes/2.2.0.txt
|
307
308
|
- doc/release_notes/2.3.0.txt
|
309
|
+
- doc/release_notes/2.4.0.txt
|
310
|
+
- doc/release_notes/2.5.0.txt
|
311
|
+
- doc/release_notes/2.6.0.txt
|
312
|
+
- doc/release_notes/2.7.0.txt
|
313
|
+
- doc/release_notes/2.8.0.txt
|
308
314
|
files:
|
309
315
|
- CHANGELOG
|
310
316
|
- MIT-LICENSE
|
@@ -357,6 +363,7 @@ files:
|
|
357
363
|
- doc/password_complexity.rdoc
|
358
364
|
- doc/password_expiration.rdoc
|
359
365
|
- doc/password_grace_period.rdoc
|
366
|
+
- doc/password_pepper.rdoc
|
360
367
|
- doc/recovery_codes.rdoc
|
361
368
|
- doc/release_notes/1.0.0.txt
|
362
369
|
- doc/release_notes/1.1.0.txt
|
@@ -386,6 +393,11 @@ files:
|
|
386
393
|
- doc/release_notes/2.1.0.txt
|
387
394
|
- doc/release_notes/2.2.0.txt
|
388
395
|
- doc/release_notes/2.3.0.txt
|
396
|
+
- doc/release_notes/2.4.0.txt
|
397
|
+
- doc/release_notes/2.5.0.txt
|
398
|
+
- doc/release_notes/2.6.0.txt
|
399
|
+
- doc/release_notes/2.7.0.txt
|
400
|
+
- doc/release_notes/2.8.0.txt
|
389
401
|
- doc/remember.rdoc
|
390
402
|
- doc/reset_password.rdoc
|
391
403
|
- doc/session_expiration.rdoc
|
@@ -429,6 +441,7 @@ files:
|
|
429
441
|
- lib/rodauth/features/password_complexity.rb
|
430
442
|
- lib/rodauth/features/password_expiration.rb
|
431
443
|
- lib/rodauth/features/password_grace_period.rb
|
444
|
+
- lib/rodauth/features/password_pepper.rb
|
432
445
|
- lib/rodauth/features/recovery_codes.rb
|
433
446
|
- lib/rodauth/features/remember.rb
|
434
447
|
- lib/rodauth/features/reset_password.rb
|
@@ -528,7 +541,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
528
541
|
- !ruby/object:Gem::Version
|
529
542
|
version: '0'
|
530
543
|
requirements: []
|
531
|
-
rubygems_version: 3.
|
544
|
+
rubygems_version: 3.2.3
|
532
545
|
signing_key:
|
533
546
|
specification_version: 4
|
534
547
|
summary: Authentication and Account Management Framework for Rack Applications
|