File: spa.rst

package info (click to toggle)
flask-security 5.6.1-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 3,420 kB
  • sloc: python: 23,164; javascript: 204; makefile: 138
file content (197 lines) | stat: -rw-r--r-- 8,377 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
Working with Single Page Applications
======================================
`Single Page Applications (spa)`_ are a popular model for both separating
user interface from application/backend code as well as providing a responsive
user experience. Angular and Vue are popular Javascript frameworks for writing SPAs.
An added benefit is that the UI can be developed completely independently (in a separate repo)
and take advantage of the latest Javascript packing and bundling technologies that are
evolving rapidly, and not make the Flask application have to deal with things
like Flask-Webpack or webassets.

For the purposes of this application note - this implies:

    * The user interface code is delivered by some other means than the Flask application.
      In particular this means that there are no opportunities to inject context/environment
      via a templating language.

    * The user interface interacts with the backend Flask application via JSON requests
      and responses - not forms. The external (json/form) API is described `here`_

.. _here: _static/openapi_view.html

    * SPAs are still browser based - so they have the same security vulnerabilities as
      traditional html/form-based applications.

    * SPAs handle all routing/redirection via code, so redirects need context.

Configuration
~~~~~~~~~~~~~
An example configuration::

    # no forms so no concept of flashing
    SECURITY_FLASH_MESSAGES = False

    # Need to be able to route backend flask API calls. Use 'accounts'
    # to be the Flask-Security endpoints.
    SECURITY_URL_PREFIX = '/api/accounts'

    # Turn on all the great Flask-Security features
    SECURITY_RECOVERABLE = True
    SECURITY_TRACKABLE = True
    SECURITY_CHANGEABLE = True
    SECURITY_CONFIRMABLE = True
    SECURITY_REGISTERABLE = True
    SECURITY_UNIFIED_SIGNIN = True

    # These need to be defined to handle redirects - these are part of the apps UI
    # As defined in the API documentation - they will receive the relevant context
    SECURITY_POST_CONFIRM_VIEW = "/confirmed"
    SECURITY_CONFIRM_ERROR_VIEW = "/confirm-error"
    SECURITY_RESET_VIEW = "/reset-password"
    SECURITY_RESET_ERROR_VIEW = "/reset-password-error"
    SECURITY_LOGIN_ERROR_VIEW = "/login-error"
    SECURITY_POST_OAUTH_LOGIN_VIEW = "/post-oauth-login"
    SECURITY_REDIRECT_BEHAVIOR = "spa"

    # CSRF protection is critical for all session-based browser UIs

    # enforce CSRF protection for session / browser - but allow token-based
    # API calls to go through
    SECURITY_CSRF_PROTECT_MECHANISMS = ["session", "basic"]
    SECURITY_CSRF_IGNORE_UNAUTH_ENDPOINTS = True

    # Send Cookie with csrf-token. This is the default for Axios and Angular.
    SECURITY_CSRF_COOKIE_NAME = "XSRF-TOKEN"
    WTF_CSRF_CHECK_DEFAULT = False
    WTF_CSRF_TIME_LIMIT = None

    # In your app
    # Enable CSRF on all api endpoints.
    flask_wtf.CSRFProtect(app)

    # Initialize Flask-Security
    user_datastore = SQLAlchemyUserDatastore(db, User, Role)
    security = Security(app, user_datastore)

    # Optionally define and set unauthorized callbacks
    security.unauthz_handler(<your unauth handler>)

When in development mode, the Flask application will run by default on port 5000.
The UI might want to run on port 8080. In order to test redirects you need to set::

    SECURITY_REDIRECT_HOST = 'localhost:8080'

.. tip::
    The `logout` endpoint doesn't take a body - be sure to add `content_type="application/json"`
    header to your POST("/logout") request so that no redirection is done.

Client side authentication options
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Depending on your SPA architecture and vision you can choose between cookie or token based authentication.

For both there is more documentation and some examples. In both cases, you need to understand and handle :ref:`csrf_topic` concerns.

Security Considerations
~~~~~~~~~~~~~~~~~~~~~~~~
Static elements such as your UI should be served with an industrial-grade web server - such
as `Nginx`_. This is also where various security measures should be handled such as injecting
standard security headers such as:

    * ``Strict-Transport-Security``
    * ``X-Frame-Options``
    * ``Content Security Policy``
    * ``X-Content-Type-Options``
    * ``X-XSS-Protection``
    * ``Referrer policy``

There are a lot of different ways to host a SPA as the javascript part itself is quit easily hosted from any static
webserver. A couple of deployment options and their configurations will be describer here.

Nginx
~~~~~
When serving a SPA from a Nginx webserver the Flask backend, with Flask-Security, will probably be served via
Nginx's reverse proxy feature. The javascript is served from Nginx itself and all calls to a certain path will be routed
to the reversed proxy. The example below routes all http requests to *"/api/"* to the Flask backend and handles all other
requests directly from javascript. This has a couple of benefits as all the requests happen within the same domain so you
don't have to worry about `CORS`_ problems::

    server {
        listen       80;
        server_name  www.example.com;

        #access_log  /var/log/nginx/host.access.log  main;

        root   /usr/share/nginx/html;
        index index.html;

        location / {
            try_files $uri $uri/ /index.html;
        }

        # Location of assets folder
        location ~ ^/(static)/  {
            gzip_static on;
            gzip_types text/plain text/xml text/css text/comma-separated-values
                text/javascript application/x-javascript application/atom+xml;
            expires max;
        }

        # redirect server error pages to the static page /50x.html
        # 400 error's will be handled from the SPA
        error_page   500 502 503 504  /50x.html;
            location = /50x.html {
        }

        # route all api requests to the flask app, served by gunicorn
        location /api/ {
            proxy_pass http://localhost:8080/api/;
        }

        # OR served via uwsgi
        location /api/ {
            include ..../uwsgi_params;
            uwsgi_pass unix:/tmp/uwsgi.sock;
            uwsgi_pass_header AUTHENTICATION-TOKEN;
        }
    }

.. note:: The example doesn't include SSL setup to keep it simple and still suitable for a more complex kubernetes setup
    where Nginx is often used as a load balancer and another Nginx with SSL setup runs in front of it.

Amazon lambda gateway / Serverless
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Most Flask apps can be deployed to Amazon's lambda gateway without much hassle by using `Zappa`_.
You'll get automatic horizontal scaling, seamless upgrades, automatic SSL certificate renewal and a very cheap way of
hosting a backend without being responsible for any infrastructure. Depending on how you design your app you could
choose to host your backend from an api specific domain: e.g. *api.example.com*. When your SPA deployment structure is
capable of routing the AJAX/XHR request from your javascript app to the separate backend; use it. When you want to use
the backend from another e.g. *www.example.com* you have some deal with some `CORS`_ setup as your browser will block
cross-domain POST requests. There is a Flask package for that: `Flask-CORS`_.

The setup of CORS is simple::

    CORS(
        app,
        supports_credentials=True,  # needed for cross domain cookie support
        resources="/*",
        allow_headers="*",
        origins="https://www.example.com",
        expose_headers="Authorization,Content-Type,Authentication-Token,XSRF-TOKEN",
    )

You can then host your javascript app from an S3 bucket, with or without Cloudfront, GH-pages or from any static webserver.

Some background material:

    * Specific to `S3`_ but easily adaptable.

    * `Flask-Talisman`_ - useful if serving everything from your Flask application - also
      useful as a good list of things to consider.

.. _Single Page Applications (spa): https://en.wikipedia.org/wiki/Single-page_application
.. _Nginx: https://www.nginx.com/
.. _S3: https://www.savjee.be/2018/05/Content-security-policy-and-aws-s3-cloudfront/
.. _Flask-Talisman: https://pypi.org/project/flask-talisman/
.. _CORS: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
.. _Flask-CORS: https://github.com/corydolphin/flask-cors
.. _Zappa: https://github.com/Miserlou/Zappa