Skip to content

Commit

Permalink
improved JWT and Token auth docs with examples (#221)
Browse files Browse the repository at this point in the history
* improved JWT and Token auth docs with examples

* fix CodeQL notices

* added `token_login` endpoint, and more source code links

---------

Co-authored-by: Daniel Townsend <dan@dantownsend.co.uk>
  • Loading branch information
sinisaos and dantownsend committed Apr 3, 2023
1 parent 1efb862 commit f560820
Show file tree
Hide file tree
Showing 18 changed files with 600 additions and 214 deletions.
91 changes: 91 additions & 0 deletions docs/source/jwt/endpoints.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
Endpoints
=========

An endpoint is provided for JWT login, and is designed to integrate with an
ASGI app, such as Starlette or FastAPI.

-------------------------------------------------------------------------------

jwt_login
---------

This creates an endpoint for logging in, and getting a JSON Web Token (JWT).

.. code-block:: python
from piccolo_api.jwt_auth.endpoints import jwt_login
from starlette import Starlette
from starlette.routing import Route, Router
app = Starlette(
routes=[
Route(
path="/login/",
endpoint=jwt_login(
secret='mysecret123'
)
),
]
)
secret
~~~~~~

This is used for signing the JWT.

expiry
~~~~~~

An optional argument, which allows you to control when a token expires. By
default it's set to 1 day.

.. code-block:: python
from datetime import timedelta
jwt_login(
secret='mysecret123',
expiry=timedelta(minutes=10)
)
.. hint:: You generally want short expiry tokens for web applications, and
longer expiry times for mobile applications.

.. hint:: See :class:`JWTMiddleware <piccolo_api.jwt_auth.middleware.JWTMiddleware>`
for how to protect your endpoints.

-------------------------------------------------------------------------------

Usage
-----

You can use any HTTP client to get the JWT token. In our example we use ``curl``.

To get a JWT token:

.. code-block:: shell
curl -X POST \
-H "Content-Type: application/json" \
-d '{"username": "piccolo", "password": "piccolo123"}' \
http://localhost:8000/login/
To get data from a protected endpoint:

.. code-block:: shell
curl -H "Authorization: Bearer your-JWT-token" \
http://localhost:8000/private/movies/
.. hint:: You can use all ``HTTP`` methods by passing a valid JWT token in the ``Authorization`` header.

-------------------------------------------------------------------------------

Source
------

.. currentmodule:: piccolo_api.jwt_auth.endpoints

.. autofunction:: jwt_login
22 changes: 22 additions & 0 deletions docs/source/jwt/example.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Full Example
============

Let's combine all of previous examples into a complete app.

-------------------------------------------------------------------------------

FastAPI
-------

.. include:: ./examples/example.py
:code: python

-------------------------------------------------------------------------------

Starlette
---------

Is almost identical to the FastAPI example - just replace ``FastAPI`` with
``Starlette``, and use Starlette's ``HTTPEndpoint`` for your endpoints.
You also need to write your own crud endpoints because ``Starlette``
can't use ``FastAPIWrapper``. An example is in `SessionAuth <https://piccolo-api.readthedocs.io/en/latest/session_auth/example.html>`_.
65 changes: 65 additions & 0 deletions docs/source/jwt/examples/example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from datetime import timedelta

from fastapi import FastAPI, Request
from home.tables import Movie # An example Table
from piccolo_admin.endpoints import create_admin
from piccolo_api.crud.endpoints import PiccoloCRUD
from piccolo_api.fastapi.endpoints import FastAPIKwargs, FastAPIWrapper
from piccolo_api.jwt_auth.endpoints import jwt_login
from piccolo_api.jwt_auth.middleware import JWTBlacklist, JWTMiddleware
from starlette.routing import Mount, Route

public_app = FastAPI(
routes=[
Mount(
"/admin/",
create_admin(tables=[Movie]),
),
Route(
path="/login/",
endpoint=jwt_login(
secret="mysecret123",
expiry=timedelta(minutes=60), # default is 1 day
),
),
],
)


BLACKLISTED_TOKENS = []


class MyBlacklist(JWTBlacklist):
async def in_blacklist(self, token: str) -> bool:
return token in BLACKLISTED_TOKENS


private_app = FastAPI()

protected_app = JWTMiddleware(
private_app,
auth_table=BaseUser,
secret="mysecret123",
blacklist=MyBlacklist(),
)

FastAPIWrapper(
"/movies/",
fastapi_app=private_app,
piccolo_crud=PiccoloCRUD(Movie, read_only=False),
fastapi_kwargs=FastAPIKwargs(
all_routes={"tags": ["Movies"]},
),
)

public_app.mount("/private", protected_app)

# This is optional if you want to provide a logout endpoint
# in your application. By adding a token to the token blacklist,
# you are invalidating the token and need to login again to get
# new valid token
@private_app.get("/logout/")
async def logout(request: Request) -> None:
BLACKLISTED_TOKENS.append(
request.headers.get("authorization").split(" ")[-1]
)
110 changes: 9 additions & 101 deletions docs/source/jwt/index.rst
Original file line number Diff line number Diff line change
@@ -1,104 +1,12 @@
.. _JWT:
.. _JWTAuth:

JWT
===
JWT Auth
========

Introduction
------------
.. toctree::
:maxdepth: 1

JWT is a token format, often used for authentication.

-------------------------------------------------------------------------------

jwt_login
---------

This creates an endpoint for logging in, and getting a JSON Web Token (JWT).

.. code-block:: python
from piccolo_api.jwt_auth.endpoints import jwt_login
from starlette import Starlette
from starlette.routing import Route, Router
app = Starlette(
routes=[
Route(
path="/login/",
endpoint=jwt_login(
secret='mysecret123'
)
),
]
)
Required arguments
~~~~~~~~~~~~~~~~~~

You have to pass in two arguments:

* auth_table - a subclass of Piccolo's ``BaseUser`` class, which is used to
authenticate the user.
* secret - this is used for signing the JWT.

expiry
~~~~~~

An optional argument, which allows you to control when a token expires. By
default it's set to 1 day.

.. code-block:: python
from datetime import timedelta
jwt_login(
secret='mysecret123',
expiry=timedelta(minutes=10)
)
.. hint:: You generally want short expiry tokens for web applications, and
longer expiry times for mobile applications.

.. hint:: See ``JWTMiddleware`` for how to protect your endpoints.

-------------------------------------------------------------------------------

JWTMiddleware
-------------

This wraps an ASGI app, and ensures a valid token is passed in the header.
Otherwise a 403 error is returned. If the token is valid, the corresponding
``user_id`` is added to the ``scope``.

blacklist
~~~~~~~~~

Optionally, you can pass in a ``blacklist`` argument, which is a subclass of
:class:`JWTBlacklist`. The implementation of the ``in_blacklist`` method is up to
the user - the data could come from a database, a file, a Python list, or
anywhere else.

.. code-block:: python
# An example blacklist.
BLACKLISTED_TOKENS = ['abc123', 'def456']
class MyBlacklist(JWTBlacklist):
async def in_blacklist(self, token: str) -> bool:
return token in BLACKLISTED_TOKENS
asgi_app = JWTMiddleware(
my_endpoint,
auth_table=User,
secret='mysecret123',
blacklist=MyBlacklist()
)
.. hint:: Blacklists are important if you have tokens with a long expiry date.

.. todo - show example POST using requests
./introduction
./endpoints
./middleware
./example
5 changes: 5 additions & 0 deletions docs/source/jwt/introduction.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Introduction
============

JSON Web Token (JWT) is a token format, often used for authentication. For more information
see the `jwt.io website <https://jwt.io/introduction>`_.
62 changes: 62 additions & 0 deletions docs/source/jwt/middleware.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
.. _JWTMiddleware:

Middleware
==========

This middleware protects endpoints using JWT tokens.

-------------------------------------------------------------------------------

Setup
-----

``JWTMiddleware`` wraps an ASGI app, and ensures a valid token is passed in the header.
Otherwise a 403 error is returned. If the token is valid, the corresponding
``user_id`` is added to the ``scope``.

blacklist
~~~~~~~~~

Optionally, you can pass in a ``blacklist`` argument, which is a subclass of
:class:`JWTBlacklist`. The implementation of the ``in_blacklist`` method is up to
the user - the data could come from a database, a file, a Python list, or
anywhere else.

.. code-block:: python
# An example blacklist.
BLACKLISTED_TOKENS = ['abc123', 'def456']
class MyBlacklist(JWTBlacklist):
async def in_blacklist(self, token: str) -> bool:
return token in BLACKLISTED_TOKENS
asgi_app = JWTMiddleware(
my_endpoint,
auth_table=User,
secret='mysecret123',
blacklist=MyBlacklist()
)
.. hint:: Blacklists are important if you have tokens with a long expiry date.

-------------------------------------------------------------------------------

Source
------

JWTMiddleware
~~~~~~~~~~~~~~

.. currentmodule:: piccolo_api.jwt_auth.middleware

.. autoclass:: JWTMiddleware

JWTBlacklist
~~~~~~~~~~~~

.. autoclass:: JWTBlacklist
:members:
4 changes: 2 additions & 2 deletions docs/source/session_auth/tables.rst
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
Tables
======

We store the session tokens in :class:`SessionsBase <piccolo_api.session_auth.tables.SessionsBase>`,
and the user credentials in :class:`BaseUser <piccolo.apps.user.tables.BaseUser>`.
We store the session tokens in :class:`SessionsBase <piccolo_api.session_auth.tables.SessionsBase>` table,
and the user credentials in :class:`BaseUser <piccolo.apps.user.tables.BaseUser>` table.

-------------------------------------------------------------------------------

Expand Down

0 comments on commit f560820

Please sign in to comment.