Skip to content

Commit

Permalink
feature - Support Json/Single-Page-Applications (#81)
Browse files Browse the repository at this point in the history
Reset Password, passwordless login, and confirmation did not support SPA/json since
those three had confirmation links that only returned redirects or forms. SPAs need to get control
of all redirects and have the appropriate context. Three new 'views' are available:
RESET_VIEW, RESET_ERROR_VIEW, and LOGIN_ERROR_VIEW to enable easy routing within the UI.

This change introduces a new config variable - SECURITY_REDIRECT_BEHAVIOR which will change those redirects to SPA-friendly
redirects. A new overridable UserMixin method - get_redirect_qparams allows for customizing precisely what
query arguments are sent via the redirect.
By default of course the existing form-based redirects are done.

A new configuration variable - REDIRECT_HOST can be used during development to force redirects to a different netloc
useful when the UI is running separately (e.g. via npm).

Continued to improve the openapi.yaml file to document these changes

Improved unit tests by:
1) verifying 'flashes' - for json/SPA - we don't want any.
2) improve performance by not using bcrypt for login tokens during testing.
  • Loading branch information
jwag956 committed May 27, 2019
1 parent 9a50396 commit 9e26249
Show file tree
Hide file tree
Showing 10 changed files with 735 additions and 67 deletions.
35 changes: 33 additions & 2 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,20 @@ Core
``500``
``VERIFY_HASH_CACHE_TTL`` Time to live for password check cache entries.
Defaults to ``300`` (5 minutes)
``SECURITY_REDIRECT_BEHAVIOR`` Passwordless login, confirmation, and
reset password have GET endpoints that validate
the passed token and redirect to an action form.
For Single-Page-Applications style UIs which need
to control their own internal URL routing these redirects
need to not contain forms, but contain relevant information
as query parameters. Setting this to ``spa`` will enable
that behavior. Defaults to ``None`` which is existing
html-style form redirects.
``SECURITY_REDIRECT_HOST`` Mostly for development purposes, the UI is often developed
separately and is running on a different port than the
Flask application. In order to test redirects, the `netloc`
of the redirect URL needs to be rewritten. Setting this
to e.g. `localhost:8080` does that. Defaults to ``None``
======================================== =======================================


Expand Down Expand Up @@ -117,8 +131,10 @@ URLs and Views
confirmation error occurs. This value can be set
to a URL or an endpoint name. If this value is
``None``, the user is presented the default view
to resend a confirmation link. Defaults to
``None``.
to resend a confirmation link.
In the case of ``SECURITY_REDIRECT_BEHAVIOR`` == ``spa``
query params in the redirect will contain the error.
Defaults to``None``.
``SECURITY_POST_REGISTER_VIEW`` Specifies the view to redirect to after a user
successfully registers. This value can be set to
a URL or an endpoint name. If this value is
Expand Down Expand Up @@ -148,6 +164,21 @@ URLs and Views
not have permission to access. If this value is
``None``, the user is presented with a default
HTTP 403 response. Defaults to ``None``.
``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.
Defaults to ``None``
``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``. Query params
in the redirect will contain the error.
Defaults to ``None``
``SECURITY_LOGIN_ERROR_VIEW`` Specifies the view/URL to redirect to after a GET
passwordless link when there is an error. This is only valid if
``SECURITY_REDIRECT_BEHAVIOR`` == ``spa``. Query params
in the redirect will contain the error.
Defaults to ``None``
=============================== ================================================


Expand Down
140 changes: 124 additions & 16 deletions docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,22 @@ openapi: 3.0.0
info:
description: |
Default API for Flask-Security.
N.B. This is preliminary.
__N.B. This is preliminary.__
Since Flask-Security is middleware, with many possible configurations this is a
guide to how the APIs will behave using standard defaults.
_Be aware that the current renderer is great! but has some limitations._
In particular
it can't represent both form input and JSON input - but all APIs take both. Also
it currently doesn't render 'examples' correctly.
You can download the latest spec from: https://github.com/jwag956/flask-security/blob/master/docs/openapi.yaml
version: 1.0.0
title: "Flask-Security External API"
contact:
name: Flask-Security-Too
url: https://github.com/jwag956/flask-security
license:
name: MIT
Expand All @@ -22,7 +32,7 @@ paths:
content:
text/html:
schema:
example: render_template(config_value('LOGIN_USER_TEMPLATE')
example: render_template(cv('LOGIN_USER_TEMPLATE')
post:
summary: Login to application
description: Supports both json and form request types
Expand Down Expand Up @@ -52,17 +62,17 @@ paths:
text/html:
schema:
description: Unsuccessful login
example: render_template(config_value('LOGIN_USER_TEMPLATE') with error values
example: render_template(cv('LOGIN_USER_TEMPLATE') with error values
302:
description: >
Successful login with form data body.
Success or failure with form data body.
redirect('next') or redirect(config_value('POST_LOGIN_VIEW'))
headers:
Location:
description: redirect
schema:
type: string
example: redirect(config_value('POST_LOGIN_VIEW'))
example: redirect(cv('POST_LOGIN_VIEW'))
400:
description: Errors while validating login
content:
Expand All @@ -74,13 +84,12 @@ paths:
summary: Log out current user
responses:
302:
description: >
Successful logout
redirect(config_value('POST_LOGOUT_VIEW'))
description: Successful logout
headers:
Location:
description: redirect(cv('POST_LOGOUT_VIEW'))
schema:
example: redirect(config_value('POST_LOGOUT_VIEW'))
example: redirect(cv('POST_LOGOUT_VIEW'))
post:
summary: Log out current user
responses:
Expand Down Expand Up @@ -109,7 +118,7 @@ paths:
content:
text/html:
schema:
example: render_template(config_value('REGISTER_USER_TEMPLATE')
example: render_template(cv('REGISTER_USER_TEMPLATE')
post:
summary: Register with application
description: Supports both json and form request types
Expand Down Expand Up @@ -140,23 +149,112 @@ paths:
text/html:
schema:
description: Unsuccessful registration
example: render_template(config_value('REGISTER_USER_TEMPLATE') with error values
example: render_template(cv('REGISTER_USER_TEMPLATE') with error values
302:
description: >
Successful registration with form data body.
redirect('next') or redirect(config_value('POST_REGISTER_VIEW'))
headers:
Location:
description: redirect
description: redirect to cv('next') or cv('POST_REGISTER_VIEW')
schema:
type: string
example: redirect(config_value('POST_REGISTER_VIEW'))
400:
description: Errors while validating registration form
content:
application/json:
schema:
$ref: "#/components/schemas/DefaultJsonErrorResponse"
/reset/{token}:
parameters:
- name: token
in: path
required: true
schema:
type: string
get:
summary: Request to reset password.
description: >
This is the result of getting a reset-password link and is usually
the result of clicking the link from a reset-password email.
If cv('REDIRECT_BEHAVIOR') == 'spa' then a 302 is always returned.
responses:
200:
description: Reset password form
content:
text/html:
schema:
example: render_template(cv('RESET_PASSWORD_TEMPLATE'))
302:
description: >
Redirects depending on success/error and whether
cv('REDIRECT_BEHAVIOR') == 'spa'.
headers:
Location:
description: |
On spa-success: cv('RESET_VIEW')?token={token}&email={email}
On spa-error-expired: cv('RESET_ERROR_VIEW')?error={msg}&email={email}
On spa-error-invalid-token: cv('RESET_ERROR_VIEW')?error={msg}
On default-error: redirect(cv('FORGOT_PASSWORD'))
schema:
type: string
examples:
spa-success:
value: cv('RESET_VIEW')?token={token}&email={email}
spa-error-expired:
value: cv('RESET_ERROR_VIEW')?error={msg}&email={email}
spa-error-invalid-token:
value: cv('RESET_ERROR_VIEW')?error={msg}
default-error:
value: redirect(cv('FORGOT_PASSWORD'))
post:
summary: Reset password.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ResetPassword"
application/x-www-form-urlencoded:
schema:
$ref: "#/components/schemas/ResetPassword"
responses:
200:
description: Reset response.
content:
text/html:
schema:
description: Reset form validation error.
example: render_template(cv('RESET_PASSWORD_TEMPLATE') with error values
application/json:
schema:
$ref: "#/components/schemas/DefaultJsonResponse"
302:
description: Password has been reset or validation error (non-json)
headers:
Location:
description: |
On success: redirect(cv('POST_RESET_VIEW')) or
redirect(cv('POST_LOGIN_VIEW'))
On invalid/expired token: redirect(cv('FORGOT_PASSWORD'))
schema:
type: string
examples:
success:
value: redirect(cv('POST_RESET_VIEW')) or
redirect(cv('POST_LOGIN_VIEW'))
invalid/expired-token:
value: redirect(cv('FORGOT_PASSWORD'))
400:
description: Errors while validating form
content:
application/json:
schema:
$ref: "#/components/schemas/DefaultJsonErrorResponse"

components:
schemas:
Login:
Expand All @@ -183,7 +281,7 @@ components:
type: object
required: [id]
description: >
By default just 'id' and 'authentication_token' are returned. However by overriding User::get_security_payload() any attributes of the User model can be returned.
By default just 'id' and 'authentication_token' are returned. However by overriding _User::get_security_payload()_ any attributes of the User model can be returned.
properties:
id:
type: integer
Expand Down Expand Up @@ -216,7 +314,7 @@ components:
type: array
items:
type: string
example: Email requires confirmation.
example: Email issues.
description: Error message (localized)
Register:
type: object
Expand Down Expand Up @@ -250,5 +348,15 @@ components:
type: string
description: >
Redirect URL. Overrides POST_REGISTER_VIEW
ResetPassword:
type: object
required: [password, password_confirm]
properties:
password:
type: string
description: Password
password_confirm:
type: string
description: Password - again


12 changes: 12 additions & 0 deletions flask_security/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@
'POST_RESET_VIEW': None,
'POST_CHANGE_VIEW': None,
'UNAUTHORIZED_VIEW': None,
'RESET_ERROR_VIEW': None,
'RESET_VIEW': None,
'LOGIN_ERROR_VIEW': None,
'REDIRECT_HOST': None,
'REDIRECT_BEHAVIOR': None,
'FORGOT_PASSWORD_TEMPLATE': 'security/forgot_password.html',
'LOGIN_USER_TEMPLATE': 'security/login_user.html',
'REGISTER_USER_TEMPLATE': 'security/register_user.html',
Expand Down Expand Up @@ -431,6 +436,13 @@ def get_security_payload(self):
"""Serialize user object as response payload."""
return {'id': str(self.id)}

def get_redirect_qparams(self, existing=None):
"""Return user info that will be added to redirect query params."""
if not existing:
existing = {}
existing.update({'email': self.email})
return existing

def verify_and_update_password(self, password):
"""Verify and update user password using configured hash."""
return verify_and_update_password(password, self)
Expand Down
Loading

0 comments on commit 9e26249

Please sign in to comment.