Skip to content

Commit

Permalink
Basic and token authentication support
Browse files Browse the repository at this point in the history
  • Loading branch information
miguelgrinberg committed Feb 3, 2025
1 parent cd87abb commit e9b9c72
Show file tree
Hide file tree
Showing 9 changed files with 474 additions and 13 deletions.
8 changes: 8 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ User Sessions
.. automodule:: microdot.session
:members:

Authentication
--------------

.. automodule:: microdot.auth
:inherited-members:
:special-members: __call__
:members:

Cross-Origin Resource Sharing (CORS)
------------------------------------

Expand Down
110 changes: 98 additions & 12 deletions docs/extensions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ Microdot is a highly extensible web application framework. The extensions
described in this section are maintained as part of the Microdot project in
the same source code repository.

WebSocket Support
~~~~~~~~~~~~~~~~~
WebSocket
~~~~~~~~-

.. list-table::
:align: left
Expand Down Expand Up @@ -39,8 +39,8 @@ Example::
message = await ws.receive()
await ws.send(message)

Server-Sent Events Support
~~~~~~~~~~~~~~~~~~~~~~~~~~
Server-Sent Events
~~~~~~~~~~~~~~~~~~

.. list-table::
:align: left
Expand Down Expand Up @@ -78,8 +78,8 @@ Example::
the SSE object. For bidirectional communication with the client, use the
WebSocket extension.

Rendering Templates
~~~~~~~~~~~~~~~~~~~
Templates
~~~~~~~~~

Many web applications use HTML templates for rendering content to clients.
Microdot includes extensions to render templates with the
Expand Down Expand Up @@ -202,8 +202,8 @@ must be used.
.. note::
The Jinja extension is not compatible with MicroPython.

Maintaining Secure User Sessions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Secure User Sessions
~~~~~~~~~~~~~~~~~~~~

.. list-table::
:align: left
Expand Down Expand Up @@ -270,6 +270,92 @@ The :func:`save() <microdot.session.SessionDict.save>` and
:func:`delete() <microdot.session.SessionDict.delete>` methods are used to update
and destroy the user session respectively.

Authentication
~~~~~~~~~~~~~~

.. list-table::
:align: left

* - Compatibility
- | CPython & MicroPython

* - Required Microdot source files
- | `auth.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot/auth.py>`_

* - Required external dependencies
- | None

* - Examples
- | `basic_auth.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/auth/basic_auth.py>`_
| `token_auth.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/auth/token_auth.py>`_
The authentication extension provides helper classes for two commonly used
authentication patterns, described below.

Basic Authentication
^^^^^^^^^^^^^^^^^^^^

`Basic Authentication <https://en.wikipedia.org/wiki/Basic_access_authentication>`_
is a method of authentication that is part of the HTTP specification. It allows
clients to authenticate to a server using a username and a password. Web
browsers have native support for Basic Authentication and will automatically
prompt the user for a username and a password when a protected resource is
accessed.

To use Basic Authentication, create an instance of the :class:`BasicAuth <microdot.auth.BasicAuth>`
class::

from microdot.auth import BasicAuth

auth = BasicAuth(app)

Next, create an authentication function. The function must accept a request
object and a username and password pair provided by the user. If the
credentials are valid, the function must return an object that represents the
user. If the authentication function cannot validate the user provided
credentials it must return ``None``. Decorate the function with
``@auth.authenticate``::

@auth.authenticate
async def verify_user(request, username, password):
user = await load_user_from_database(username)
if user and user.verify_password(password):
return user

To protect a route with authentication, add the ``auth`` instance as a
decorator::

@app.route('/')
@auth
async def index(request):
return f'Hello, {request.g.current_user}!'

While running an authenticated request, the user object returned by the
authenticaction function is accessible as ``request.g.current_user``.

Token Authentication
^^^^^^^^^^^^^^^^^^^^

To set up token authentication, create an instance of :class:`TokenAuth <microdot.auth.TokenAuth>`::

from microdot.auth import TokenAuth

auth = TokenAuth()

Then add a function that verifies the token and returns the user it belongs to,
or ``None`` if the token is invalid or expired::

@auth.authenticate
async def verify_token(request, token):
return load_user_from_token(token)

As with Basic authentication, the ``auth`` instance is used as a decorator to
protect your routes::

@app.route('/')
@auth
async def index(request):
return f'Hello, {request.g.current_user}!'

Cross-Origin Resource Sharing (CORS)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down Expand Up @@ -305,8 +391,8 @@ Example::
cors = CORS(app, allowed_origins=['https://example.com'],
allow_credentials=True)

Testing with the Test Client
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Test Client
~~~~~~~~~~~

.. list-table::
:align: left
Expand Down Expand Up @@ -342,8 +428,8 @@ Example::
See the documentation for the :class:`TestClient <microdot.test_client.TestClient>`
class for more details.

Deploying on a Production Web Server
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Production Deployments
~~~~~~~~~~~~~~~~~~~~~~

The ``Microdot`` class creates its own simple web server. This is enough for an
application deployed with MicroPython, but when using CPython it may be useful
Expand Down
1 change: 1 addition & 0 deletions examples/auth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This directory contains examples that demonstrate basic and token authentication.
27 changes: 27 additions & 0 deletions examples/auth/basic_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from microdot import Microdot
from microdot.auth import BasicAuth
from pbkdf2 import generate_password_hash, check_password_hash


USERS = {
'susan': generate_password_hash('hello'),
'david': generate_password_hash('bye'),
}
app = Microdot()
auth = BasicAuth()


@auth.authenticate
async def check_credentials(request, username, password):
if username in USERS and check_password_hash(USERS[username], password):
return username


@app.route('/')
@auth
async def index(request):
return f'Hello, {request.g.current_user}!'


if __name__ == '__main__':
app.run(debug=True)
44 changes: 44 additions & 0 deletions examples/auth/pbkdf2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import os
import hashlib

# PBKDF2 algorithm obtained from:
# https://codeandlife.com/2023/01/06/how-to-calculate-pbkdf2-hmac-sha256-with-
# python,-example-code/


def sha256(b):
return hashlib.sha256(b).digest()


def ljust(b, n, f):
return b + f * (n - len(b))


def gethmac(key, content):
okeypad = bytes(v ^ 0x5c for v in ljust(key, 64, b'\0'))
ikeypad = bytes(v ^ 0x36 for v in ljust(key, 64, b'\0'))
return sha256(okeypad + sha256(ikeypad + content))


def pbkdf2(pwd, salt, iterations=1000):
U = salt + b'\x00\x00\x00\x01'
T = bytes(64)
for _ in range(iterations):
U = gethmac(pwd, U)
T = bytes(a ^ b for a, b in zip(U, T))
return T


def generate_password_hash(password, salt=None, iterations=1000):
salt = salt or os.urandom(16)
dk = pbkdf2(password.encode(), salt, iterations)
return f'pbkdf2-hmac-sha256:{salt.hex()}:{iterations}:{dk.hex()}'


def check_password_hash(password_hash, password):
algorithm, salt, iterations, dk = password_hash.split(':')
iterations = int(iterations)
if algorithm != 'pbkdf2-hmac-sha256':
return False
return pbkdf2(password.encode(), salt=bytes.fromhex(salt),
iterations=iterations) == bytes.fromhex(dk)
26 changes: 26 additions & 0 deletions examples/auth/token_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from microdot import Microdot
from microdot.auth import TokenAuth

app = Microdot()
auth = TokenAuth()

TOKENS = {
'susan-token': 'susan',
'david-token': 'david',
}


@auth.authenticate
async def check_token(request, token):
if token in TOKENS:
return TOKENS[token]


@app.route('/')
@auth
async def index(request):
return f'Hello, {request.g.current_user}!'


if __name__ == '__main__':
app.run(debug=True)
Loading

0 comments on commit e9b9c72

Please sign in to comment.