microdot 2.0.7__tar.gz → 2.1.0__tar.gz
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.
- {microdot-2.0.7/src/microdot.egg-info → microdot-2.1.0}/PKG-INFO +2 -2
- {microdot-2.0.7 → microdot-2.1.0}/docs/api.rst +16 -0
- {microdot-2.0.7 → microdot-2.1.0}/docs/extensions.rst +184 -12
- {microdot-2.0.7 → microdot-2.1.0}/docs/intro.rst +20 -6
- {microdot-2.0.7 → microdot-2.1.0}/pyproject.toml +1 -1
- microdot-2.1.0/src/microdot/auth.py +144 -0
- microdot-2.1.0/src/microdot/login.py +163 -0
- {microdot-2.0.7 → microdot-2.1.0}/src/microdot/microdot.py +120 -51
- {microdot-2.0.7 → microdot-2.1.0/src/microdot.egg-info}/PKG-INFO +2 -2
- {microdot-2.0.7 → microdot-2.1.0}/src/microdot.egg-info/SOURCES.txt +4 -0
- {microdot-2.0.7 → microdot-2.1.0}/tests/__init__.py +2 -0
- microdot-2.1.0/tests/test_auth.py +125 -0
- microdot-2.1.0/tests/test_login.py +188 -0
- {microdot-2.0.7 → microdot-2.1.0}/tests/test_microdot.py +201 -2
- {microdot-2.0.7 → microdot-2.1.0}/tests/test_session.py +1 -1
- {microdot-2.0.7 → microdot-2.1.0}/LICENSE +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/MANIFEST.in +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/README.md +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/docs/Makefile +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/docs/_static/css/custom.css +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/docs/conf.py +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/docs/freezing.rst +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/docs/index.rst +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/docs/make.bat +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/docs/migrating.rst +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/setup.cfg +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/src/microdot/__init__.py +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/src/microdot/asgi.py +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/src/microdot/cors.py +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/src/microdot/helpers.py +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/src/microdot/jinja.py +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/src/microdot/session.py +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/src/microdot/sse.py +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/src/microdot/test_client.py +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/src/microdot/utemplate.py +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/src/microdot/websocket.py +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/src/microdot/wsgi.py +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/src/microdot.egg-info/dependency_links.txt +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/src/microdot.egg-info/not-zip-safe +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/src/microdot.egg-info/requires.txt +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/src/microdot.egg-info/top_level.txt +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/tests/files/test.bin +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/tests/files/test.css +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/tests/files/test.gif +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/tests/files/test.gz +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/tests/files/test.html +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/tests/files/test.jpg +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/tests/files/test.js +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/tests/files/test.json +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/tests/files/test.png +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/tests/files/test.txt +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/tests/files/test.txt.gz +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/tests/mock_socket.py +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/tests/templates/hello.jinja.txt +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/tests/templates/hello.utemplate.txt +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/tests/templates/hello_utemplate_txt.py +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/tests/test_asgi.py +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/tests/test_cors.py +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/tests/test_end2end.py +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/tests/test_jinja.py +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/tests/test_multidict.py +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/tests/test_request.py +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/tests/test_response.py +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/tests/test_sse.py +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/tests/test_url_pattern.py +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/tests/test_urlencode.py +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/tests/test_utemplate.py +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/tests/test_websocket.py +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/tests/test_wsgi.py +0 -0
- {microdot-2.0.7 → microdot-2.1.0}/tox.ini +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: microdot
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: The impossibly small web framework for MicroPython
|
|
5
5
|
Author-email: Miguel Grinberg <miguel.grinberg@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/miguelgrinberg/microdot
|
|
@@ -44,6 +44,22 @@ User Sessions
|
|
|
44
44
|
.. automodule:: microdot.session
|
|
45
45
|
:members:
|
|
46
46
|
|
|
47
|
+
Authentication
|
|
48
|
+
--------------
|
|
49
|
+
|
|
50
|
+
.. automodule:: microdot.auth
|
|
51
|
+
:inherited-members:
|
|
52
|
+
:special-members: __call__
|
|
53
|
+
:members:
|
|
54
|
+
|
|
55
|
+
User Logins
|
|
56
|
+
-----------
|
|
57
|
+
|
|
58
|
+
.. automodule:: microdot.login
|
|
59
|
+
:inherited-members:
|
|
60
|
+
:special-members: __call__
|
|
61
|
+
:members:
|
|
62
|
+
|
|
47
63
|
Cross-Origin Resource Sharing (CORS)
|
|
48
64
|
------------------------------------
|
|
49
65
|
|
|
@@ -5,8 +5,8 @@ Microdot is a highly extensible web application framework. The extensions
|
|
|
5
5
|
described in this section are maintained as part of the Microdot project in
|
|
6
6
|
the same source code repository.
|
|
7
7
|
|
|
8
|
-
WebSocket
|
|
9
|
-
|
|
8
|
+
WebSocket
|
|
9
|
+
~~~~~~~~-
|
|
10
10
|
|
|
11
11
|
.. list-table::
|
|
12
12
|
:align: left
|
|
@@ -39,8 +39,8 @@ Example::
|
|
|
39
39
|
message = await ws.receive()
|
|
40
40
|
await ws.send(message)
|
|
41
41
|
|
|
42
|
-
Server-Sent Events
|
|
43
|
-
|
|
42
|
+
Server-Sent Events
|
|
43
|
+
~~~~~~~~~~~~~~~~~~
|
|
44
44
|
|
|
45
45
|
.. list-table::
|
|
46
46
|
:align: left
|
|
@@ -78,8 +78,8 @@ Example::
|
|
|
78
78
|
the SSE object. For bidirectional communication with the client, use the
|
|
79
79
|
WebSocket extension.
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
Templates
|
|
82
|
+
~~~~~~~~~
|
|
83
83
|
|
|
84
84
|
Many web applications use HTML templates for rendering content to clients.
|
|
85
85
|
Microdot includes extensions to render templates with the
|
|
@@ -202,8 +202,8 @@ must be used.
|
|
|
202
202
|
.. note::
|
|
203
203
|
The Jinja extension is not compatible with MicroPython.
|
|
204
204
|
|
|
205
|
-
|
|
206
|
-
|
|
205
|
+
Secure User Sessions
|
|
206
|
+
~~~~~~~~~~~~~~~~~~~~
|
|
207
207
|
|
|
208
208
|
.. list-table::
|
|
209
209
|
:align: left
|
|
@@ -270,6 +270,178 @@ The :func:`save() <microdot.session.SessionDict.save>` and
|
|
|
270
270
|
:func:`delete() <microdot.session.SessionDict.delete>` methods are used to update
|
|
271
271
|
and destroy the user session respectively.
|
|
272
272
|
|
|
273
|
+
Authentication
|
|
274
|
+
~~~~~~~~~~~~~~
|
|
275
|
+
|
|
276
|
+
.. list-table::
|
|
277
|
+
:align: left
|
|
278
|
+
|
|
279
|
+
* - Compatibility
|
|
280
|
+
- | CPython & MicroPython
|
|
281
|
+
|
|
282
|
+
* - Required Microdot source files
|
|
283
|
+
- | `auth.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/auth.py>`_
|
|
284
|
+
|
|
285
|
+
* - Required external dependencies
|
|
286
|
+
- | None
|
|
287
|
+
|
|
288
|
+
* - Examples
|
|
289
|
+
- | `basic_auth.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/auth/basic_auth.py>`_
|
|
290
|
+
| `token_auth.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/auth/token_auth.py>`_
|
|
291
|
+
|
|
292
|
+
The authentication extension provides helper classes for two commonly used
|
|
293
|
+
authentication patterns, described below.
|
|
294
|
+
|
|
295
|
+
Basic Authentication
|
|
296
|
+
^^^^^^^^^^^^^^^^^^^^
|
|
297
|
+
|
|
298
|
+
`Basic Authentication <https://en.wikipedia.org/wiki/Basic_access_authentication>`_
|
|
299
|
+
is a method of authentication that is part of the HTTP specification. It allows
|
|
300
|
+
clients to authenticate to a server using a username and a password. Web
|
|
301
|
+
browsers have native support for Basic Authentication and will automatically
|
|
302
|
+
prompt the user for a username and a password when a protected resource is
|
|
303
|
+
accessed.
|
|
304
|
+
|
|
305
|
+
To use Basic Authentication, create an instance of the :class:`BasicAuth <microdot.auth.BasicAuth>`
|
|
306
|
+
class::
|
|
307
|
+
|
|
308
|
+
from microdot.auth import BasicAuth
|
|
309
|
+
|
|
310
|
+
auth = BasicAuth(app)
|
|
311
|
+
|
|
312
|
+
Next, create an authentication function. The function must accept a request
|
|
313
|
+
object and a username and password pair provided by the user. If the
|
|
314
|
+
credentials are valid, the function must return an object that represents the
|
|
315
|
+
user. If the authentication function cannot validate the user provided
|
|
316
|
+
credentials it must return ``None``. Decorate the function with
|
|
317
|
+
``@auth.authenticate``::
|
|
318
|
+
|
|
319
|
+
@auth.authenticate
|
|
320
|
+
async def verify_user(request, username, password):
|
|
321
|
+
user = await load_user_from_database(username)
|
|
322
|
+
if user and user.verify_password(password):
|
|
323
|
+
return user
|
|
324
|
+
|
|
325
|
+
To protect a route with authentication, add the ``auth`` instance as a
|
|
326
|
+
decorator::
|
|
327
|
+
|
|
328
|
+
@app.route('/')
|
|
329
|
+
@auth
|
|
330
|
+
async def index(request):
|
|
331
|
+
return f'Hello, {request.g.current_user}!'
|
|
332
|
+
|
|
333
|
+
While running an authenticated request, the user object returned by the
|
|
334
|
+
authenticaction function is accessible as ``request.g.current_user``.
|
|
335
|
+
|
|
336
|
+
Token Authentication
|
|
337
|
+
^^^^^^^^^^^^^^^^^^^^
|
|
338
|
+
|
|
339
|
+
To set up token authentication, create an instance of :class:`TokenAuth <microdot.auth.TokenAuth>`::
|
|
340
|
+
|
|
341
|
+
from microdot.auth import TokenAuth
|
|
342
|
+
|
|
343
|
+
auth = TokenAuth()
|
|
344
|
+
|
|
345
|
+
Then add a function that verifies the token and returns the user it belongs to,
|
|
346
|
+
or ``None`` if the token is invalid or expired::
|
|
347
|
+
|
|
348
|
+
@auth.authenticate
|
|
349
|
+
async def verify_token(request, token):
|
|
350
|
+
return load_user_from_token(token)
|
|
351
|
+
|
|
352
|
+
As with Basic authentication, the ``auth`` instance is used as a decorator to
|
|
353
|
+
protect your routes::
|
|
354
|
+
|
|
355
|
+
@app.route('/')
|
|
356
|
+
@auth
|
|
357
|
+
async def index(request):
|
|
358
|
+
return f'Hello, {request.g.current_user}!'
|
|
359
|
+
|
|
360
|
+
User Logins
|
|
361
|
+
~~~~~~~~~~~
|
|
362
|
+
|
|
363
|
+
.. list-table::
|
|
364
|
+
:align: left
|
|
365
|
+
|
|
366
|
+
* - Compatibility
|
|
367
|
+
- | CPython & MicroPython
|
|
368
|
+
|
|
369
|
+
* - Required Microdot source files
|
|
370
|
+
- | `login.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/auth.py>`_
|
|
371
|
+
| `session.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/session.py>`_
|
|
372
|
+
* - Required external dependencies
|
|
373
|
+
- | CPython: `PyJWT <https://pyjwt.readthedocs.io/>`_
|
|
374
|
+
| MicroPython: `jwt.py <https://github.com/micropython/micropython-lib/blob/master/python-ecosys/pyjwt/jwt.py>`_,
|
|
375
|
+
`hmac.py <https://github.com/micropython/micropython-lib/blob/master/python-stdlib/hmac/hmac.py>`_
|
|
376
|
+
* - Examples
|
|
377
|
+
- | `login.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/login/login.py>`_
|
|
378
|
+
|
|
379
|
+
The login extension provides user login functionality. The logged in state of
|
|
380
|
+
the user is stored in the user session cookie, and an optional "remember me"
|
|
381
|
+
cookie can also be added to keep the user logged in across browser sessions.
|
|
382
|
+
|
|
383
|
+
To use this extension, create instances of the
|
|
384
|
+
:class:`Session <microdot.session.Session>` and :class:`Login <microdot.login.Login>`
|
|
385
|
+
class::
|
|
386
|
+
|
|
387
|
+
Session(app, secret_key='top-secret!')
|
|
388
|
+
login = Login()
|
|
389
|
+
|
|
390
|
+
The ``Login`` class accept an optional argument with the URL of the login page.
|
|
391
|
+
The default for this URL is */login*.
|
|
392
|
+
|
|
393
|
+
The application must represent users as objects with an ``id`` attribute. A
|
|
394
|
+
function decorated with ``@login.user_loader`` is used to load a user object::
|
|
395
|
+
|
|
396
|
+
@login.user_loader
|
|
397
|
+
async def get_user(user_id):
|
|
398
|
+
return database.get_user(user_id)
|
|
399
|
+
|
|
400
|
+
The application must implement the login form. At the point in which the user
|
|
401
|
+
credentials have been received and verified, a call to the
|
|
402
|
+
:func:`login_user() <microdot.login.Login.login_user>` function must be made to
|
|
403
|
+
record the user in the user session::
|
|
404
|
+
|
|
405
|
+
@app.route('/login', methods=['GET', 'POST'])
|
|
406
|
+
async def login(request):
|
|
407
|
+
# ...
|
|
408
|
+
if user.check_password(password):
|
|
409
|
+
return await login.login_user(request, user, remember=remember_me)
|
|
410
|
+
return redirect('/login')
|
|
411
|
+
|
|
412
|
+
The optional ``remember`` argument is used to add a remember me cookie that
|
|
413
|
+
will log the user in automatically in future sessions. A value of ``True`` will
|
|
414
|
+
keep the log in active for 30 days. Alternatively, an integer number of days
|
|
415
|
+
can be passed in this argument.
|
|
416
|
+
|
|
417
|
+
Any routes that require the user to be logged in must be decorated with
|
|
418
|
+
:func:`@login <microdot.login.Login.__call__>`::
|
|
419
|
+
|
|
420
|
+
@app.route('/')
|
|
421
|
+
@login
|
|
422
|
+
async def index(request):
|
|
423
|
+
# ...
|
|
424
|
+
|
|
425
|
+
Routes that are of a sensitive nature can be decorated with
|
|
426
|
+
:func:`@login.fresh <microdot.login.Login.fresh>`
|
|
427
|
+
instead. This decorator requires that the user has logged in during the current
|
|
428
|
+
session, and will ask the user to logged in again if the session was
|
|
429
|
+
authenticated through a remember me cookie::
|
|
430
|
+
|
|
431
|
+
@app.get('/fresh')
|
|
432
|
+
@login.fresh
|
|
433
|
+
async def fresh(request):
|
|
434
|
+
# ...
|
|
435
|
+
|
|
436
|
+
To log out a user, the :func:`logout_user() <microdot.auth.Login.logout_user>`
|
|
437
|
+
is used::
|
|
438
|
+
|
|
439
|
+
@app.post('/logout')
|
|
440
|
+
@login
|
|
441
|
+
async def logout(request):
|
|
442
|
+
await login.logout_user(request)
|
|
443
|
+
return redirect('/')
|
|
444
|
+
|
|
273
445
|
Cross-Origin Resource Sharing (CORS)
|
|
274
446
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
275
447
|
|
|
@@ -305,8 +477,8 @@ Example::
|
|
|
305
477
|
cors = CORS(app, allowed_origins=['https://example.com'],
|
|
306
478
|
allow_credentials=True)
|
|
307
479
|
|
|
308
|
-
|
|
309
|
-
|
|
480
|
+
Test Client
|
|
481
|
+
~~~~~~~~~~~
|
|
310
482
|
|
|
311
483
|
.. list-table::
|
|
312
484
|
:align: left
|
|
@@ -342,8 +514,8 @@ Example::
|
|
|
342
514
|
See the documentation for the :class:`TestClient <microdot.test_client.TestClient>`
|
|
343
515
|
class for more details.
|
|
344
516
|
|
|
345
|
-
|
|
346
|
-
|
|
517
|
+
Production Deployments
|
|
518
|
+
~~~~~~~~~~~~~~~~~~~~~~
|
|
347
519
|
|
|
348
520
|
The ``Microdot`` class creates its own simple web server. This is enough for an
|
|
349
521
|
application deployed with MicroPython, but when using CPython it may be useful
|
|
@@ -445,7 +445,7 @@ Mounting a Sub-Application
|
|
|
445
445
|
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
446
446
|
|
|
447
447
|
Small Microdot applications can be written as a single source file, but this
|
|
448
|
-
is not the best option for applications that
|
|
448
|
+
is not the best option for applications that pass a certain size. To make it
|
|
449
449
|
simpler to write large applications, Microdot supports the concept of
|
|
450
450
|
sub-applications that can be "mounted" on a larger application, possibly with
|
|
451
451
|
a common URL prefix applied to all of its routes. For developers familiar with
|
|
@@ -501,11 +501,25 @@ The resulting application will have the customer endpoints available at
|
|
|
501
501
|
*/customers/* and the order endpoints available at */orders/*.
|
|
502
502
|
|
|
503
503
|
.. note::
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
504
|
+
During the handling of a request, the
|
|
505
|
+
:attr:`Request.url_prefix <microdot.Microdot.url_prefix>` attribute is
|
|
506
|
+
set to the URL prefix under which the sub-application was mounted, or an
|
|
507
|
+
empty string if the endpoint did not come from a sub-application or the
|
|
508
|
+
sub-application was mounted without a URL prefix. It is possible to issue a
|
|
509
|
+
redirect that is relative to the sub-application as follows::
|
|
510
|
+
|
|
511
|
+
return redirect(request.url_prefix + '/relative-url')
|
|
512
|
+
|
|
513
|
+
When mounting an application as shown above, before-request, after-request and
|
|
514
|
+
error handlers defined in the sub-application are copied over to the main
|
|
515
|
+
application at mount time. Once installed in the main application, these
|
|
516
|
+
handlers will apply to the whole application and not just the sub-application
|
|
517
|
+
in which they were created.
|
|
518
|
+
|
|
519
|
+
The :func:`mount() <microdot.Microdot.mount>` method has a ``local`` argument
|
|
520
|
+
that defaults to ``False``. When this argument is set to ``True``, the
|
|
521
|
+
before-request, after-request and error handlers defined in the sub-application
|
|
522
|
+
will only apply to the sub-application.
|
|
509
523
|
|
|
510
524
|
Shutting Down the Server
|
|
511
525
|
^^^^^^^^^^^^^^^^^^^^^^^^
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from microdot import abort
|
|
2
|
+
from microdot.microdot import invoke_handler
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class BaseAuth:
|
|
6
|
+
def __init__(self):
|
|
7
|
+
self.auth_callback = None
|
|
8
|
+
self.error_callback = None
|
|
9
|
+
|
|
10
|
+
def __call__(self, f):
|
|
11
|
+
"""Decorator to protect a route with authentication.
|
|
12
|
+
|
|
13
|
+
An instance of this class must be used as a decorator on the routes
|
|
14
|
+
that need to be protected. Example::
|
|
15
|
+
|
|
16
|
+
auth = BasicAuth() # or TokenAuth()
|
|
17
|
+
|
|
18
|
+
@app.route('/protected')
|
|
19
|
+
@auth
|
|
20
|
+
def protected(request):
|
|
21
|
+
# ...
|
|
22
|
+
|
|
23
|
+
Routes that are decorated in this way will only be invoked if the
|
|
24
|
+
authentication callback returned a valid user object, otherwise the
|
|
25
|
+
error callback will be executed.
|
|
26
|
+
"""
|
|
27
|
+
async def wrapper(request, *args, **kwargs):
|
|
28
|
+
auth = self._get_auth(request)
|
|
29
|
+
if not auth:
|
|
30
|
+
return await invoke_handler(self.error_callback, request)
|
|
31
|
+
request.g.current_user = await invoke_handler(
|
|
32
|
+
self.auth_callback, request, *auth)
|
|
33
|
+
if not request.g.current_user:
|
|
34
|
+
return await invoke_handler(self.error_callback, request)
|
|
35
|
+
return await invoke_handler(f, request, *args, **kwargs)
|
|
36
|
+
|
|
37
|
+
return wrapper
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class BasicAuth(BaseAuth):
|
|
41
|
+
"""Basic Authentication.
|
|
42
|
+
|
|
43
|
+
:param realm: The realm that is displayed when the user is prompted to
|
|
44
|
+
authenticate in the browser.
|
|
45
|
+
:param charset: The charset that is used to encode the realm.
|
|
46
|
+
:param scheme: The authentication scheme. Defaults to 'Basic'.
|
|
47
|
+
:param error_status: The error status code to return when authentication
|
|
48
|
+
fails. Defaults to 401.
|
|
49
|
+
"""
|
|
50
|
+
def __init__(self, realm='Please login', charset='UTF-8', scheme='Basic',
|
|
51
|
+
error_status=401):
|
|
52
|
+
super().__init__()
|
|
53
|
+
self.realm = realm
|
|
54
|
+
self.charset = charset
|
|
55
|
+
self.scheme = scheme
|
|
56
|
+
self.error_status = error_status
|
|
57
|
+
self.error_callback = self.authentication_error
|
|
58
|
+
|
|
59
|
+
def _get_auth(self, request):
|
|
60
|
+
auth = request.headers.get('Authorization')
|
|
61
|
+
if auth and auth.startswith('Basic '):
|
|
62
|
+
import binascii
|
|
63
|
+
try:
|
|
64
|
+
username, password = binascii.a2b_base64(
|
|
65
|
+
auth[6:]).decode().split(':', 1)
|
|
66
|
+
except Exception: # pragma: no cover
|
|
67
|
+
return None
|
|
68
|
+
return username, password
|
|
69
|
+
|
|
70
|
+
def authentication_error(self, request):
|
|
71
|
+
return '', self.error_status, {
|
|
72
|
+
'WWW-Authenticate': '{} realm="{}", charset="{}"'.format(
|
|
73
|
+
self.scheme, self.realm, self.charset)}
|
|
74
|
+
|
|
75
|
+
def authenticate(self, f):
|
|
76
|
+
"""Decorator to configure the authentication callback.
|
|
77
|
+
|
|
78
|
+
This decorator must be used with a function that accepts the request
|
|
79
|
+
object, a username and a password and returns a user object if the
|
|
80
|
+
credentials are valid, or ``None`` if they are not. Example::
|
|
81
|
+
|
|
82
|
+
@auth.authenticate
|
|
83
|
+
async def check_credentials(request, username, password):
|
|
84
|
+
user = get_user(username)
|
|
85
|
+
if user and user.check_password(password):
|
|
86
|
+
return get_user(username)
|
|
87
|
+
"""
|
|
88
|
+
self.auth_callback = f
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class TokenAuth(BaseAuth):
|
|
92
|
+
"""Token based authentication.
|
|
93
|
+
|
|
94
|
+
:param header: The name of the header that will contain the token. Defaults
|
|
95
|
+
to 'Authorization'.
|
|
96
|
+
:param scheme: The authentication scheme. Defaults to 'Bearer'.
|
|
97
|
+
:param error_status: The error status code to return when authentication
|
|
98
|
+
fails. Defaults to 401.
|
|
99
|
+
"""
|
|
100
|
+
def __init__(self, header='Authorization', scheme='Bearer',
|
|
101
|
+
error_status=401):
|
|
102
|
+
super().__init__()
|
|
103
|
+
self.header = header
|
|
104
|
+
self.scheme = scheme.lower()
|
|
105
|
+
self.error_status = error_status
|
|
106
|
+
self.error_callback = self.authentication_error
|
|
107
|
+
|
|
108
|
+
def _get_auth(self, request):
|
|
109
|
+
auth = request.headers.get(self.header)
|
|
110
|
+
if auth:
|
|
111
|
+
if self.header == 'Authorization':
|
|
112
|
+
try:
|
|
113
|
+
scheme, token = auth.split(' ', 1)
|
|
114
|
+
except Exception:
|
|
115
|
+
return None
|
|
116
|
+
if scheme.lower() == self.scheme:
|
|
117
|
+
return (token.strip(),)
|
|
118
|
+
else:
|
|
119
|
+
return (auth,)
|
|
120
|
+
|
|
121
|
+
def authenticate(self, f):
|
|
122
|
+
"""Decorator to configure the authentication callback.
|
|
123
|
+
|
|
124
|
+
This decorator must be used with a function that accepts the request
|
|
125
|
+
object, a username and a password and returns a user object if the
|
|
126
|
+
credentials are valid, or ``None`` if they are not. Example::
|
|
127
|
+
|
|
128
|
+
@auth.authenticate
|
|
129
|
+
async def check_credentials(request, token):
|
|
130
|
+
return get_user(token)
|
|
131
|
+
"""
|
|
132
|
+
self.auth_callback = f
|
|
133
|
+
|
|
134
|
+
def errorhandler(self, f):
|
|
135
|
+
"""Decorator to configure the error callback.
|
|
136
|
+
|
|
137
|
+
Microdot calls the error callback to allow the application to generate
|
|
138
|
+
a custom error response. The default error response is to call
|
|
139
|
+
``abort(401)``.
|
|
140
|
+
"""
|
|
141
|
+
self.error_callback = f
|
|
142
|
+
|
|
143
|
+
def authentication_error(self, request):
|
|
144
|
+
abort(self.error_status)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
from time import time
|
|
2
|
+
from microdot import redirect
|
|
3
|
+
from microdot.microdot import urlencode, invoke_handler
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Login:
|
|
7
|
+
"""User login support for Microdot.
|
|
8
|
+
|
|
9
|
+
:param login_url: the URL to redirect to when a login is required. The
|
|
10
|
+
default is '/login'.
|
|
11
|
+
"""
|
|
12
|
+
def __init__(self, login_url='/login'):
|
|
13
|
+
self.login_url = login_url
|
|
14
|
+
self.user_loader_callback = None
|
|
15
|
+
|
|
16
|
+
def user_loader(self, f):
|
|
17
|
+
"""Decorator to configure the user callback.
|
|
18
|
+
|
|
19
|
+
The decorated function receives the user ID as an argument and must
|
|
20
|
+
return the corresponding user object, or ``None`` if the user ID is
|
|
21
|
+
invalid.
|
|
22
|
+
"""
|
|
23
|
+
self.user_loader_callback = f
|
|
24
|
+
|
|
25
|
+
def _get_session(self, request):
|
|
26
|
+
return request.app._session.get(request)
|
|
27
|
+
|
|
28
|
+
def _update_remember_cookie(self, request, days, user_id=None):
|
|
29
|
+
remember_payload = request.app._session.encode({
|
|
30
|
+
'user_id': user_id,
|
|
31
|
+
'days': days,
|
|
32
|
+
'exp': time() + days * 24 * 60 * 60
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
@request.after_request
|
|
36
|
+
async def _set_remember_cookie(request, response):
|
|
37
|
+
response.set_cookie('_remember', remember_payload,
|
|
38
|
+
max_age=days * 24 * 60 * 60)
|
|
39
|
+
return response
|
|
40
|
+
|
|
41
|
+
def _get_user_id_from_session(self, request):
|
|
42
|
+
session = self._get_session(request)
|
|
43
|
+
if session and '_user_id' in session:
|
|
44
|
+
return session['_user_id']
|
|
45
|
+
if '_remember' in request.cookies:
|
|
46
|
+
remember_payload = request.app._session.decode(
|
|
47
|
+
request.cookies['_remember'])
|
|
48
|
+
user_id = remember_payload.get('user_id')
|
|
49
|
+
if user_id: # pragma: no branch
|
|
50
|
+
self._update_remember_cookie(
|
|
51
|
+
request, remember_payload.get('_days', 30), user_id)
|
|
52
|
+
session['_user_id'] = user_id
|
|
53
|
+
session['_fresh'] = False
|
|
54
|
+
session.save()
|
|
55
|
+
return user_id
|
|
56
|
+
|
|
57
|
+
async def _redirect_to_login(self, request):
|
|
58
|
+
return '', 302, {'Location': self.login_url + '?next=' + urlencode(
|
|
59
|
+
request.url)}
|
|
60
|
+
|
|
61
|
+
async def login_user(self, request, user, remember=False,
|
|
62
|
+
redirect_url='/'):
|
|
63
|
+
"""Log a user in.
|
|
64
|
+
|
|
65
|
+
:param request: the request object
|
|
66
|
+
:param user: the user object
|
|
67
|
+
:param remember: if the user's logged in state should be remembered
|
|
68
|
+
with a cookie after the session ends. Set to the
|
|
69
|
+
number of days the remember cookie should last, or to
|
|
70
|
+
``True`` to use a default duration of 30 days.
|
|
71
|
+
:param redirect_url: the URL to redirect to after login
|
|
72
|
+
|
|
73
|
+
This call marks the user as logged in by storing their user ID in the
|
|
74
|
+
user session. The application must call this method to log a user in
|
|
75
|
+
after their credentials have been validated.
|
|
76
|
+
|
|
77
|
+
The method returns a redirect response, either to the URL the user
|
|
78
|
+
originally intended to visit, or if there is no original URL to the URL
|
|
79
|
+
specified by the `redirect_url`.
|
|
80
|
+
"""
|
|
81
|
+
session = self._get_session(request)
|
|
82
|
+
session['_user_id'] = user.id
|
|
83
|
+
session['_fresh'] = True
|
|
84
|
+
session.save()
|
|
85
|
+
|
|
86
|
+
if remember:
|
|
87
|
+
days = 30 if remember is True else int(remember)
|
|
88
|
+
self._update_remember_cookie(request, days, session['_user_id'])
|
|
89
|
+
|
|
90
|
+
next_url = request.args.get('next', redirect_url)
|
|
91
|
+
if not next_url.startswith('/'):
|
|
92
|
+
next_url = redirect_url
|
|
93
|
+
return redirect(next_url)
|
|
94
|
+
|
|
95
|
+
async def logout_user(self, request):
|
|
96
|
+
"""Log a user out.
|
|
97
|
+
|
|
98
|
+
:param request: the request object
|
|
99
|
+
|
|
100
|
+
This call removes information about the user's log in from the user
|
|
101
|
+
session. If a remember cookie exists, it is removed as well.
|
|
102
|
+
"""
|
|
103
|
+
session = self._get_session(request)
|
|
104
|
+
session.pop('_user_id', None)
|
|
105
|
+
session.pop('_fresh', None)
|
|
106
|
+
session.save()
|
|
107
|
+
if '_remember' in request.cookies:
|
|
108
|
+
self._update_remember_cookie(request, 0)
|
|
109
|
+
|
|
110
|
+
def __call__(self, f):
|
|
111
|
+
"""Decorator to protect a route with authentication.
|
|
112
|
+
|
|
113
|
+
If the user is not logged in, Microdot will redirect to the login page
|
|
114
|
+
first. The decorated route will only run after successful login by the
|
|
115
|
+
user. If the user is already logged in, the route will run immediately.
|
|
116
|
+
Example::
|
|
117
|
+
|
|
118
|
+
login = Login()
|
|
119
|
+
|
|
120
|
+
@app.route('/secret')
|
|
121
|
+
@login
|
|
122
|
+
async def secret(request):
|
|
123
|
+
# only accessible to authenticated users
|
|
124
|
+
|
|
125
|
+
"""
|
|
126
|
+
async def wrapper(request, *args, **kwargs):
|
|
127
|
+
user_id = self._get_user_id_from_session(request)
|
|
128
|
+
if not user_id:
|
|
129
|
+
return await self._redirect_to_login(request)
|
|
130
|
+
request.g.current_user = await invoke_handler(
|
|
131
|
+
self.user_loader_callback, user_id)
|
|
132
|
+
if not request.g.current_user:
|
|
133
|
+
return await self._redirect_to_login(request)
|
|
134
|
+
return await invoke_handler(f, request, *args, **kwargs)
|
|
135
|
+
|
|
136
|
+
return wrapper
|
|
137
|
+
|
|
138
|
+
def fresh(self, f):
|
|
139
|
+
"""Decorator to protect a route with "fresh" authentication.
|
|
140
|
+
|
|
141
|
+
This decorator prevents the route from running when the login session
|
|
142
|
+
is not fresh. A fresh session is a session that has been created from
|
|
143
|
+
direct user interaction with the login page, while a non-fresh session
|
|
144
|
+
occurs when a login is restored from a "remember me" cookie. Example::
|
|
145
|
+
|
|
146
|
+
login = Login()
|
|
147
|
+
|
|
148
|
+
@app.route('/secret')
|
|
149
|
+
@auth.fresh
|
|
150
|
+
async def secret(request):
|
|
151
|
+
# only accessible to authenticated users
|
|
152
|
+
# users logged in via remember me cookie will need to
|
|
153
|
+
# re-authenticate
|
|
154
|
+
"""
|
|
155
|
+
base_wrapper = self.__call__(f)
|
|
156
|
+
|
|
157
|
+
async def wrapper(request, *args, **kwargs):
|
|
158
|
+
session = self._get_session(request)
|
|
159
|
+
if session.get('_fresh'):
|
|
160
|
+
return await base_wrapper(request, *args, **kwargs)
|
|
161
|
+
return await self._redirect_to_login(request)
|
|
162
|
+
|
|
163
|
+
return wrapper
|