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.
Files changed (70) hide show
  1. {microdot-2.0.7/src/microdot.egg-info → microdot-2.1.0}/PKG-INFO +2 -2
  2. {microdot-2.0.7 → microdot-2.1.0}/docs/api.rst +16 -0
  3. {microdot-2.0.7 → microdot-2.1.0}/docs/extensions.rst +184 -12
  4. {microdot-2.0.7 → microdot-2.1.0}/docs/intro.rst +20 -6
  5. {microdot-2.0.7 → microdot-2.1.0}/pyproject.toml +1 -1
  6. microdot-2.1.0/src/microdot/auth.py +144 -0
  7. microdot-2.1.0/src/microdot/login.py +163 -0
  8. {microdot-2.0.7 → microdot-2.1.0}/src/microdot/microdot.py +120 -51
  9. {microdot-2.0.7 → microdot-2.1.0/src/microdot.egg-info}/PKG-INFO +2 -2
  10. {microdot-2.0.7 → microdot-2.1.0}/src/microdot.egg-info/SOURCES.txt +4 -0
  11. {microdot-2.0.7 → microdot-2.1.0}/tests/__init__.py +2 -0
  12. microdot-2.1.0/tests/test_auth.py +125 -0
  13. microdot-2.1.0/tests/test_login.py +188 -0
  14. {microdot-2.0.7 → microdot-2.1.0}/tests/test_microdot.py +201 -2
  15. {microdot-2.0.7 → microdot-2.1.0}/tests/test_session.py +1 -1
  16. {microdot-2.0.7 → microdot-2.1.0}/LICENSE +0 -0
  17. {microdot-2.0.7 → microdot-2.1.0}/MANIFEST.in +0 -0
  18. {microdot-2.0.7 → microdot-2.1.0}/README.md +0 -0
  19. {microdot-2.0.7 → microdot-2.1.0}/docs/Makefile +0 -0
  20. {microdot-2.0.7 → microdot-2.1.0}/docs/_static/css/custom.css +0 -0
  21. {microdot-2.0.7 → microdot-2.1.0}/docs/conf.py +0 -0
  22. {microdot-2.0.7 → microdot-2.1.0}/docs/freezing.rst +0 -0
  23. {microdot-2.0.7 → microdot-2.1.0}/docs/index.rst +0 -0
  24. {microdot-2.0.7 → microdot-2.1.0}/docs/make.bat +0 -0
  25. {microdot-2.0.7 → microdot-2.1.0}/docs/migrating.rst +0 -0
  26. {microdot-2.0.7 → microdot-2.1.0}/setup.cfg +0 -0
  27. {microdot-2.0.7 → microdot-2.1.0}/src/microdot/__init__.py +0 -0
  28. {microdot-2.0.7 → microdot-2.1.0}/src/microdot/asgi.py +0 -0
  29. {microdot-2.0.7 → microdot-2.1.0}/src/microdot/cors.py +0 -0
  30. {microdot-2.0.7 → microdot-2.1.0}/src/microdot/helpers.py +0 -0
  31. {microdot-2.0.7 → microdot-2.1.0}/src/microdot/jinja.py +0 -0
  32. {microdot-2.0.7 → microdot-2.1.0}/src/microdot/session.py +0 -0
  33. {microdot-2.0.7 → microdot-2.1.0}/src/microdot/sse.py +0 -0
  34. {microdot-2.0.7 → microdot-2.1.0}/src/microdot/test_client.py +0 -0
  35. {microdot-2.0.7 → microdot-2.1.0}/src/microdot/utemplate.py +0 -0
  36. {microdot-2.0.7 → microdot-2.1.0}/src/microdot/websocket.py +0 -0
  37. {microdot-2.0.7 → microdot-2.1.0}/src/microdot/wsgi.py +0 -0
  38. {microdot-2.0.7 → microdot-2.1.0}/src/microdot.egg-info/dependency_links.txt +0 -0
  39. {microdot-2.0.7 → microdot-2.1.0}/src/microdot.egg-info/not-zip-safe +0 -0
  40. {microdot-2.0.7 → microdot-2.1.0}/src/microdot.egg-info/requires.txt +0 -0
  41. {microdot-2.0.7 → microdot-2.1.0}/src/microdot.egg-info/top_level.txt +0 -0
  42. {microdot-2.0.7 → microdot-2.1.0}/tests/files/test.bin +0 -0
  43. {microdot-2.0.7 → microdot-2.1.0}/tests/files/test.css +0 -0
  44. {microdot-2.0.7 → microdot-2.1.0}/tests/files/test.gif +0 -0
  45. {microdot-2.0.7 → microdot-2.1.0}/tests/files/test.gz +0 -0
  46. {microdot-2.0.7 → microdot-2.1.0}/tests/files/test.html +0 -0
  47. {microdot-2.0.7 → microdot-2.1.0}/tests/files/test.jpg +0 -0
  48. {microdot-2.0.7 → microdot-2.1.0}/tests/files/test.js +0 -0
  49. {microdot-2.0.7 → microdot-2.1.0}/tests/files/test.json +0 -0
  50. {microdot-2.0.7 → microdot-2.1.0}/tests/files/test.png +0 -0
  51. {microdot-2.0.7 → microdot-2.1.0}/tests/files/test.txt +0 -0
  52. {microdot-2.0.7 → microdot-2.1.0}/tests/files/test.txt.gz +0 -0
  53. {microdot-2.0.7 → microdot-2.1.0}/tests/mock_socket.py +0 -0
  54. {microdot-2.0.7 → microdot-2.1.0}/tests/templates/hello.jinja.txt +0 -0
  55. {microdot-2.0.7 → microdot-2.1.0}/tests/templates/hello.utemplate.txt +0 -0
  56. {microdot-2.0.7 → microdot-2.1.0}/tests/templates/hello_utemplate_txt.py +0 -0
  57. {microdot-2.0.7 → microdot-2.1.0}/tests/test_asgi.py +0 -0
  58. {microdot-2.0.7 → microdot-2.1.0}/tests/test_cors.py +0 -0
  59. {microdot-2.0.7 → microdot-2.1.0}/tests/test_end2end.py +0 -0
  60. {microdot-2.0.7 → microdot-2.1.0}/tests/test_jinja.py +0 -0
  61. {microdot-2.0.7 → microdot-2.1.0}/tests/test_multidict.py +0 -0
  62. {microdot-2.0.7 → microdot-2.1.0}/tests/test_request.py +0 -0
  63. {microdot-2.0.7 → microdot-2.1.0}/tests/test_response.py +0 -0
  64. {microdot-2.0.7 → microdot-2.1.0}/tests/test_sse.py +0 -0
  65. {microdot-2.0.7 → microdot-2.1.0}/tests/test_url_pattern.py +0 -0
  66. {microdot-2.0.7 → microdot-2.1.0}/tests/test_urlencode.py +0 -0
  67. {microdot-2.0.7 → microdot-2.1.0}/tests/test_utemplate.py +0 -0
  68. {microdot-2.0.7 → microdot-2.1.0}/tests/test_websocket.py +0 -0
  69. {microdot-2.0.7 → microdot-2.1.0}/tests/test_wsgi.py +0 -0
  70. {microdot-2.0.7 → microdot-2.1.0}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: microdot
3
- Version: 2.0.7
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 Support
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 Support
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
- Rendering Templates
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
- Maintaining Secure User Sessions
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
- Testing with the Test Client
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
- Deploying on a Production Web Server
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 past a certain size. To make it
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
- Before-request, after-request and error handlers defined in the
505
- sub-application are also copied over to the main application at mount time.
506
- Once installed in the main application, these handlers will apply to the
507
- whole application and not just the sub-application in which they were
508
- created.
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
  ^^^^^^^^^^^^^^^^^^^^^^^^
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "microdot"
3
- version = "2.0.7"
3
+ version = "2.1.0"
4
4
  authors = [
5
5
  { name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" },
6
6
  ]
@@ -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