Skip to content

Commit

Permalink
Improve /reset w.r.t. OWASP
Browse files Browse the repository at this point in the history
- no longer send a new token upon receiving an expired token
- no longer auto-login on successful reset password (backwards compat config variable added)
- no longer send identity/email information as part of query params in unauthenticated requests
- add Referrer-Policy="no-referrer" as suggested by OWASP

Minor improvements to API doc.

closes #281
  • Loading branch information
jwag956 committed Jul 19, 2023
1 parent bf5a31f commit 14eb33e
Show file tree
Hide file tree
Showing 12 changed files with 221 additions and 151 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ repos:
- flake8-bugbear
- flake8-implicit-str-concat
- repo: https://github.com/Riverside-Healthcare/djLint
rev: v1.31.1
rev: v1.32.0
hooks:
- id: djlint-jinja
files: "\\.html"
Expand Down
18 changes: 16 additions & 2 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Version 5.3.0
Released TBD

This is a minor version bump due to the (possible) incompatibility w.r.t.
WebAuthn.
WebAuthn, and changes to /reset.

Fixes
++++++
Expand All @@ -21,14 +21,28 @@ Fixes
- (:issue:`806`) Login no longer, by default, check for email deliverability.
- (:issue:`791`) Token authentication is accepted on endpoints which only allow
'session' as authentication-method. (N247S)
- (:issue:`814`) /reset and /confirm and GENERIC_RESPONSES and addtional form args don't mix.
- (:issue:`814`) /reset and /confirm and GENERIC_RESPONSES and additional form args don't mix.
- (:issue:`281`) Reset password can be exploited and other OWASP improvements.

Backwards Compatibility Concerns
+++++++++++++++++++++++++++++++++

- To align with the W3C WebAuthn Level2 and 3 spec - transports are now part of the registration response.
This has been changed BOTH in the server code (using py_webauth data structures) as well as the sample
javascript code. If an application has their own javascript front end code - it might need to be changed.
- Reset password was changed to improve OWASP compliance and reduce possible exploitation:

- A new email (with new token) is no longer sent upon expired token. Users must restart
the reset password process.
- The user is no longer automatically logged in upon successful password reset. For
backwards compatibility :py:data:`SECURITY_AUTO_LOGIN_AFTER_RESET` can be set to ``True``.
Note that this compatibility feature is deprecated and will be removed in a future release.
- Identity information (identity, email) is no longer sent as part of the URL redirect
query params.
- The SECURITY_MSG_PASSWORD_RESET_EXPIRED message no longer contains the user's identity/email.
- The default for :py:data:`SECURITY_RESET_PASSWORD_WITHIN` has been changed from `5 days` to `1 days`.
- The response to GET /reset/<token> sets the HTTP header `Referrer-Policy` to `no-referrer` as suggested
by OWASP.

Version 5.2.0
-------------
Expand Down
1 change: 1 addition & 0 deletions docs/_static/openapi_view.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
allow-spec-file-load="false"
show-components="true"
schema-description-expanded="true"
default-schema-tab="schema"
heading-text="Flask Security External API">
<img
slot="logo"
Expand Down
75 changes: 46 additions & 29 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -993,22 +993,38 @@ Recoverable
Specifies the view to redirect to after a user successfully resets their password.
This value can be set to a URL or an endpoint name. If this
value is ``None``, the user is redirected to the value of ``SECURITY_POST_LOGIN_VIEW``.
value is ``None``, the user is redirected to the value of ``.login`` if
:py:data:`SECURITY_AUTO_LOGIN_AFTER_RESET` is ``False`` or :py:data:`SECURITY_POST_LOGIN_VIEW`
if ``True``

Default: ``None``.

.. py:data:: SECURITY_RESET_VIEW
Specifies the view/URL to redirect to after a GET reset-password link.
This is only valid if ``SECURITY_REDIRECT_BEHAVIOR`` == ``spa``.
Query params in the redirect will contain the ``token`` and ``email``.
This is only valid if :py:data:`SECURITY_REDIRECT_BEHAVIOR` == ``spa``.
Query params in the redirect will contain the ``token``.

Default: ``None``.

.. py:data:: SECURITY_AUTO_LOGIN_AFTER_RESET
If ``False`` then on successful reset the user will be required to signin again.
Note that the reset token is not valid after being used once.
If ``True``, then the user corresponding to the
reset token will be automatically signed in. Note: auto-login is contrary
to OWASP best security practices. This option is for backwards compatibility
and is deprecated.

Default: ``False``.

.. versionadded:: 5.3.0
.. deprecated:: 5.3.0

.. py:data:: SECURITY_RESET_ERROR_VIEW
Specifies the view/URL to redirect to after a GET reset-password link when there is an error.
This is only valid if ``SECURITY_REDIRECT_BEHAVIOR`` == ``spa``.
This is only valid if :py:data:`SECURITY_REDIRECT_BEHAVIOR` == ``spa``.
Query params in the redirect will contain the error.

Default: ``None``.
Expand All @@ -1018,7 +1034,7 @@ Recoverable
Specifies the amount of time a user has before their password reset link expires.
Always pluralize the time unit for this value.

Default: ``"5 days"``.
Default: ``"1 days"``.

.. py:data:: SECURITY_SEND_PASSWORD_RESET_EMAIL
Expand Down Expand Up @@ -1667,13 +1683,13 @@ Feature Flags
-------------
All feature flags. By default all are 'False'/not enabled.

* ``SECURITY_CONFIRMABLE``
* ``SECURITY_REGISTERABLE``
* ``SECURITY_RECOVERABLE``
* ``SECURITY_TRACKABLE``
* ``SECURITY_PASSWORDLESS``
* ``SECURITY_CHANGEABLE``
* ``SECURITY_TWO_FACTOR``
* :py:data:`SECURITY_CONFIRMABLE`
* :py:data:`SECURITY_REGISTERABLE`
* :py:data:`SECURITY_RECOVERABLE`
* :py:data:`SECURITY_TRACKABLE`
* :py:data:`SECURITY_PASSWORDLESS`
* :py:data:`SECURITY_CHANGEABLE`
* :py:data:`SECURITY_TWO_FACTOR`
* :py:data:`SECURITY_UNIFIED_SIGNIN`
* :py:data:`SECURITY_WEBAUTHN`
* :py:data:`SECURITY_MULTI_FACTOR_RECOVERY_CODES`
Expand All @@ -1683,13 +1699,13 @@ URLs and Views
--------------
A list of all URLs and Views:

* ``SECURITY_LOGIN_URL``
* ``SECURITY_LOGOUT_URL``
* :py:data:`SECURITY_LOGIN_URL`
* :py:data:`SECURITY_LOGOUT_URL`
* :py:data:`SECURITY_VERIFY_URL`
* ``SECURITY_REGISTER_URL``
* ``SECURITY_RESET_URL``
* ``SECURITY_CHANGE_URL``
* ``SECURITY_CONFIRM_URL``
* :py:data:`SECURITY_REGISTER_URL`
* :py:data:`SECURITY_RESET_URL`
* :py:data:`SECURITY_CHANGE_URL`
* :py:data:`SECURITY_CONFIRM_URL`
* :py:data:`SECURITY_MULTI_FACTOR_RECOVERY_CODES_URL`
* :py:data:`SECURITY_MULTI_FACTOR_RECOVERY_URL`
* :py:data:`SECURITY_OAUTH_START_URL`
Expand All @@ -1700,17 +1716,17 @@ A list of all URLs and Views:
* :py:data:`SECURITY_TWO_FACTOR_RESCUE_URL`
* :py:data:`SECURITY_TWO_FACTOR_ERROR_VIEW`
* :py:data:`SECURITY_TWO_FACTOR_POST_SETUP_VIEW`
* ``SECURITY_POST_LOGIN_VIEW``
* ``SECURITY_POST_LOGOUT_VIEW``
* ``SECURITY_CONFIRM_ERROR_VIEW``
* ``SECURITY_POST_REGISTER_VIEW``
* ``SECURITY_POST_CONFIRM_VIEW``
* ``SECURITY_POST_RESET_VIEW``
* ``SECURITY_POST_CHANGE_VIEW``
* ``SECURITY_UNAUTHORIZED_VIEW``
* ``SECURITY_RESET_VIEW``
* ``SECURITY_RESET_ERROR_VIEW``
* ``SECURITY_LOGIN_ERROR_VIEW``
* :py:data:`SECURITY_POST_LOGIN_VIEW`
* :py:data:`SECURITY_POST_LOGOUT_VIEW`
* :py:data:`SECURITY_CONFIRM_ERROR_VIEW`
* :py:data:`SECURITY_POST_REGISTER_VIEW`
* :py:data:`SECURITY_POST_CONFIRM_VIEW`
* :py:data:`SECURITY_POST_RESET_VIEW`
* :py:data:`SECURITY_POST_CHANGE_VIEW`
* :py:data:`SECURITY_UNAUTHORIZED_VIEW`
* :py:data:`SECURITY_RESET_VIEW`
* :py:data:`SECURITY_RESET_ERROR_VIEW`
* :py:data:`SECURITY_LOGIN_ERROR_VIEW`
* :py:data:`SECURITY_US_SIGNIN_URL`
* :py:data:`SECURITY_US_SETUP_URL`
* :py:data:`SECURITY_US_SIGNIN_SEND_CODE_URL`
Expand Down Expand Up @@ -1800,6 +1816,7 @@ The default messages and error levels can be found in ``core.py``.
* ``SECURITY_MSG_PASSWORD_REQUIRED``
* ``SECURITY_MSG_PASSWORD_RESET``
* ``SECURITY_MSG_PASSWORD_RESET_EXPIRED``
* ``SECURITY_MSG_PASSWORD_RESET_NO_LOGIN``
* ``SECURITY_MSG_PASSWORD_RESET_REQUEST``
* ``SECURITY_MSG_PASSWORD_TOO_SIMPLE``
* ``SECURITY_MSG_PHONE_INVALID``
Expand Down
6 changes: 3 additions & 3 deletions docs/features.rst
Original file line number Diff line number Diff line change
Expand Up @@ -190,9 +190,9 @@ Password Reset/Recovery

Password reset and recovery is available for when a user forgets their
password. Flask-Security sends an email to the user with a link to a view which
allows them to reset their password. Once the password is reset they are automatically
logged in and can use the new password from then on. Password reset links can
be configured to expire after a specified amount of time.
allows them to reset their password. Once the password is reset they are redirected to
the login page where they need to authenticate using the new password.
Password reset links can be configured to expire after a specified amount of time.

As with password change - this will update the the user's ``fs_uniquifier`` attribute
which will invalidate all existing sessions AND (by default) all authentication tokens.
Expand Down
46 changes: 22 additions & 24 deletions docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ info:
paths:
/login:
get:
summary: Retrieve login form and/or user information
summary: GET login form and/or user information
responses:
200:
description: >
Expand Down Expand Up @@ -111,7 +111,7 @@ paths:
$ref: "#/components/schemas/DefaultJsonErrorResponse"
/login(passwordless):
get:
summary: Return passwordless login form
summary: GET passwordless login form
responses:
200:
description: Passwordless login form
Expand Down Expand Up @@ -216,7 +216,7 @@ paths:
description: Http status code
/verify:
get:
summary: Basic re-authentication.
summary: GET Basic re-authentication form
description: >
If an endpoint is protected with @auth_required() with a freshness declaration
this endpoint will be called to request an already signed in user to re-authenticate.
Expand Down Expand Up @@ -279,7 +279,7 @@ paths:
$ref: "#/components/schemas/DefaultJsonErrorResponse"
/register:
get:
summary: Return register form
summary: GET register form
responses:
200:
description: Register form
Expand Down Expand Up @@ -342,7 +342,7 @@ paths:
$ref: "#/components/schemas/DefaultJsonErrorResponse"
/change:
get:
summary: Return change password form
summary: GET change password form
responses:
200:
description: change password form
Expand Down Expand Up @@ -408,7 +408,7 @@ paths:
$ref: "#/components/schemas/DefaultJsonErrorResponse"
/reset:
get:
summary: Return reset password form
summary: GET reset password form
responses:
200:
description: Reset password form
Expand Down Expand Up @@ -482,9 +482,9 @@ paths:
headers:
Location:
description: |
On spa-success: SECURITY_RESET_VIEW?token={token}&identity={identity}&email={email}
On spa-success: SECURITY_RESET_VIEW?token={token}
On spa-error-expired: SECURITY_RESET_ERROR_VIEW?error={msg}&identity={identity}&email={email}
On spa-error-expired: SECURITY_RESET_ERROR_VIEW?error={msg}
On spa-error-invalid-token: SECURITY_RESET_ERROR_VIEW?error={msg}
Expand All @@ -493,8 +493,6 @@ paths:
type: string
post:
summary: Reset password
parameters:
- $ref: "#/components/parameters/include_auth_token"
requestBody:
required: true
content:
Expand All @@ -515,14 +513,14 @@ paths:
example: render_template(SECURITY_RESET_PASSWORD_TEMPLATE) with error values
application/json:
schema:
$ref: "#/components/schemas/JsonResponseWithToken"
$ref: "#/components/schemas/BaseJsonResponse"
302:
description: Password has been reset or validation error (non-json)
headers:
Location:
description: |
On success: redirect(SECURITY_POST_RESET_VIEW) or
redirect(SECURITY_POST_LOGIN_VIEW)
redirect(".login")
On invalid/expired token: redirect(SECURITY_FORGOT_PASSWORD)
schema:
Expand All @@ -535,7 +533,7 @@ paths:
$ref: "#/components/schemas/DefaultJsonErrorResponse"
/confirm:
get:
summary: Return send confirmation form
summary: GET send confirmation form
responses:
200:
description: Confirmation form
Expand Down Expand Up @@ -617,7 +615,7 @@ paths:
type: string
/us-signin:
get:
summary: Unified Sign In.
summary: GET Unified Sign In form
responses:
200:
description: Sign in form
Expand All @@ -639,7 +637,7 @@ paths:
type: string
description: Configuration setting SECURITY_USER_IDENTITY_ATTRIBUTES
post:
summary: Unified Sign In.
summary: Unified Sign In
parameters:
- $ref: "#/components/parameters/include_auth_token"
requestBody:
Expand Down Expand Up @@ -721,7 +719,7 @@ paths:

/us-verify:
get:
summary: Unified sign in re-authentication.
summary: GET Unified sign in re-authentication form/information
description: >
If an endpoint is protected with @auth_required() with a freshness declaration
this endpoint will be called to request an already signed in user to re-authenticate.
Expand Down Expand Up @@ -832,7 +830,7 @@ paths:
$ref: "#/components/schemas/DefaultJsonErrorResponse"
/us-setup:
get:
summary: Unified sign in setup passcode options.
summary: GET Unified sign in setup passcode options.
responses:
200:
description: Setup form
Expand Down Expand Up @@ -1006,7 +1004,7 @@ paths:

/tf-setup:
get:
summary: Two-factor authentication setup.
summary: GET Two-factor authentication setup form/information
responses:
200:
description: Setup form
Expand Down Expand Up @@ -1082,7 +1080,7 @@ paths:
$ref: "#/components/schemas/DefaultJsonErrorResponse"
/tf-validate:
get:
summary: Retrieve form based on current two-factor state.
summary: GET current two-factor state form
responses:
200:
description: Code validation
Expand Down Expand Up @@ -1350,7 +1348,7 @@ paths:

/mf-recovery:
get:
summary: Get recovery code form.
summary: GET recovery code form.
description: >
If a user has two-factor authentication enabled, they can generate and
use a recovery code if they lose or otherwise can't use their second factor
Expand Down Expand Up @@ -1406,7 +1404,7 @@ paths:

/wan-register:
get:
summary: Register a new WebAuthn key - Step 1
summary: GET Register WebAuthn form
responses:
200:
description: Register WebAuthn form
Expand Down Expand Up @@ -1519,7 +1517,7 @@ paths:
$ref: "#/components/schemas/DefaultJsonErrorResponse"
/wan-signin:
get:
summary: Sign in using a WebAuthn key - Step 1
summary: GET WebAuthn sign in form
responses:
200:
description: Sign in with WebAuthn form
Expand Down Expand Up @@ -1627,7 +1625,7 @@ paths:
$ref: "#/components/schemas/DefaultJsonErrorResponse"
/wan-delete:
get:
summary: Delete an existing WebAuthn key
summary: GET Delete WebAuthn key form
responses:
200:
description: Delete an existing WebAuthn Key
Expand Down Expand Up @@ -1678,7 +1676,7 @@ paths:
$ref: "#/components/schemas/DefaultJsonErrorResponse"
/wan-verify:
get:
summary: Re-authenticate using a WebAuthn Key.
summary: GET Re-authenticate using WebAuthn form
description: >
If an endpoint is protected with @auth_required() with a freshness declaration
this endpoint can be used to re-authenticate with a previously registered WebAuthn Key.
Expand Down
Loading

0 comments on commit 14eb33e

Please sign in to comment.