Authentication Middleware¶
This special middleware comes from Lilya and is used to authenticate users on every request but adapted for Ravyn.
These are a very special middlewares and helps with any authentication middleware that can be used within a Ravyn application but like everything else, you can design your own.
AuthenticationMiddleware
is an implementation using backends and most people will prefer it.
See Authentication for more details.
Majority of the things here are from Lilya and we just added the ability to use it as a Ravyn middleware with almost no changes.
Example of a JWT middleware class¶
from typing import Sequence
from myapp.models import User
from myapp.security.jwt.token import Token
from edgy.exceptions import ObjectNotFound
from lilya._internal._connection import Connection
from lilya.authentication import AuthResult, AuthCredentials
from lilya.types import ASGIApp
from ravyn.exceptions import NotAuthorized
from ravyn.middleware.authentication import AuthenticationMiddleware, AuthenticationBackend
class JWTBackend(AuthenticationBackend):
async def retrieve_user(self, user_id: int) -> User:
try:
return await User.get(pk=user_id)
except ObjectNotFound:
raise NotAuthorized()
async def authenticate(
self, request: Connection, api_key_header: str, signing_key: str, algorithm: str
) -> AuthResult:
token = request.headers.get(api_key_header)
if not token:
raise NotAuthorized("JWT token not found.")
token = Token.decode(token=token, key=signing_key, algorithm=algorithm)
user = await self.retrieve_user(token.sub)
return AuthCredentials(), user
class JWTAuthMiddleware(AuthenticationMiddleware):
"""
An example how to integrate and design a JWT authentication
middleware assuming a `myapp` in Ravyn.
"""
def __init__(
self,
app: ASGIApp,
signing_key: str,
algorithm: str,
api_key_header: str,
backend: Sequence[AuthenticationBackend] | AuthenticationBackend | None = None,
):
super().__init__(app, backend=backend)
self.app = app
self.signing_key = signing_key
self.algorithm = algorithm
self.api_key_header = api_key_header
async def authenticate(self, conn: Connection) -> AuthResult | Exception:
for backend in self.backend:
# exceptions are passed through to __call__ and there handled
auth_result = await backend.authenticate(
conn,
signing_key=self.signing_key,
algorithm=self.algorithm,
api_key_header=self.api_key_header,
)
if auth_result is not None:
return auth_result
return NotAuthorized()
- Import the
AuthenticationMiddleware
fromravyn.middleware.authentication
. - Implement the
authenticate
and returntuple[AuthCredentials, UserInterface]
(AuthResult) or None or raise.
Import the middleware into a Lilya application¶
from ravyn import Ravyn
from lilya.middleware import DefineMiddleware
from .middleware.jwt import JWTAuthMiddleware
app = Ravyn(routes=[...], middleware=[DefineMiddleware(JWTAuthMiddleware, ...)])
from ravyn import Ravyn, RavynSettings
from lilya.middleware import DefineMiddleware
class AppSettings(RavynSettings):
@property
def middleware(self) -> list[DefineMiddleware]:
return [
# you can also use absolute import strings
DefineMiddleware("project.middleware.jwt.JWTAuthMiddleware")
]
# load the settings via RAVYN_SETTINGS_MODULE=src.configs.live.AppSettings
app = Ravyn(routes=[...])
Tip
To know more about loading the settings and the available properties, have a look at the settings docs.
Authentication¶
Ravyn provides a straightforward yet robust interface for managing authentication and permissions (from Lilya).
By installing AuthenticationMiddleware
with a suitable authentication backend, you can access the request.user
and request.auth
interfaces within your endpoints.
import base64
import binascii
from lilya.authentication import AuthCredentials, AuthenticationBackend, BasicUser
from lilya.middleware import DefineMiddleware
from ravyn import Ravyn, Gateway, Request, get
from ravyn.exceptions import AuthenticationError
from ravyn.middleware.sessions import SessionMiddleware
from ravyn.middleware.authentication import AuthenticationMiddleware
from ravyn.responses import PlainText
class SessionBackend(AuthenticationBackend):
async def authenticate(self, connection):
if "session" not in connection.scope:
return
if connection.scope["session"].get("username", None):
return
return AuthCredentials(["authenticated"]), BasicUser(
connection.scope["session"]["username"]
)
class BasicAuthBackend(AuthenticationBackend):
async def authenticate(self, connection):
if "Authorization" not in connection.headers:
return
auth = connection.headers["Authorization"]
try:
scheme, credentials = auth.split()
if scheme.lower() != "basic":
return
decoded = base64.b64decode(credentials).decode("ascii")
except (ValueError, UnicodeDecodeError, binascii.Error) as exc:
raise AuthenticationError("Invalid basic auth credentials")
username, _, password = decoded.partition(":")
return AuthCredentials(["authenticated"]), BasicUser(username)
@get()
async def homepage(request: Request) -> PlainText:
if request.user.is_authenticated:
return PlainText("Hello, " + request.user.display_name)
return PlainText("Hello, you")
app = Ravyn(
routes=[Gateway("/", handler=homepage)],
middleware=[
# must be defined before AuthenticationMiddleware, because of the SessionBackend
DefineMiddleware(SessionMiddleware, secret_key=...),
DefineMiddleware(AuthenticationMiddleware, backend=[SessionBackend(), BasicAuthBackend()]),
],
)
When not using an user management we can also do something like:
import base64
import binascii
from lilya.authentication import AuthCredentials, AuthenticationBackend, BasicUser
from lilya.middleware import DefineMiddleware
from ravyn import Ravyn, Gateway, Request, get
from ravyn.exceptions import AuthenticationError
from ravyn.middleware.authentication import AuthenticationMiddleware
from ravyn.responses import PlainText
import secrets
class HardCodedBasicAuthBackend(AuthenticationBackend):
def __init__(self, *, username: str = "admin", password: str) -> None:
self.basic_string = base64.b64encode(f"{username}:{password}".encode()).decode()
async def authenticate(self, connection) -> tuple[AuthCredentials, BasicUser] | None:
if "Authorization" not in connection.headers:
return None
auth = connection.headers["Authorization"]
try:
scheme, credentials = auth.split()
if scheme.lower() != "basic":
return None
if not secrets.compare_digest(credentials, self.basic_string):
raise ValueError()
username = base64.b64decode(credentials).decode("ascii").split(":", 1)[0]
except (ValueError, UnicodeDecodeError, binascii.Error) as exc:
raise AuthenticationError("Invalid basic auth credentials")
return AuthCredentials(["authenticated"]), BasicUser(username)
@get()
async def homepage(request: Request) -> PlainText:
if request.user.is_authenticated:
return PlainText("Hello, " + request.user.display_name)
return PlainText("Hello, you")
app = Ravyn(
routes=[Gateway("/", handler=homepage)],
middleware=[
DefineMiddleware(
AuthenticationMiddleware, backend=[HardCodedBasicAuthBackend(password="password")]
)
],
)
authenticate()
from AuthenticationMiddleware
¶
This is the main method that goes through the backends and tries to authenticate the user.
Sometimes you want to override this method to add some custom logic before or after the authentication.
from typing import Sequence
from myapp.models import User
from myapp.security.jwt.token import Token
from edgy.exceptions import ObjectNotFound
from lilya._internal._connection import Connection
from lilya.authentication import AuthResult, AuthCredentials
from lilya.types import ASGIApp
from ravyn.exceptions import NotAuthorized
from ravyn.middleware.authentication import AuthenticationMiddleware, AuthenticationBackend
class JWTBackend(AuthenticationBackend):
async def retrieve_user(self, user_id: int) -> User:
try:
return await User.get(pk=user_id)
except ObjectNotFound:
raise NotAuthorized()
async def authenticate(
self, request: Connection, api_key_header: str, signing_key: str, algorithm: str
) -> AuthResult:
token = request.headers.get(api_key_header)
if not token:
raise NotAuthorized("JWT token not found.")
token = Token.decode(token=token, key=signing_key, algorithm=algorithm)
user = await self.retrieve_user(token.sub)
return AuthCredentials(), user
class JWTAuthMiddleware(AuthenticationMiddleware):
"""
An example how to integrate and design a JWT authentication
middleware assuming a `myapp` in Ravyn.
"""
def __init__(
self,
app: ASGIApp,
signing_key: str,
algorithm: str,
api_key_header: str,
backend: Sequence[AuthenticationBackend] | AuthenticationBackend | None = None,
):
super().__init__(app, backend=backend)
self.app = app
self.signing_key = signing_key
self.algorithm = algorithm
self.api_key_header = api_key_header
async def authenticate(self, conn: Connection) -> AuthResult | Exception:
for backend in self.backend:
# exceptions are passed through to __call__ and there handled
auth_result = await backend.authenticate(
conn,
signing_key=self.signing_key,
algorithm=self.algorithm,
api_key_header=self.api_key_header,
)
if auth_result is not None:
return auth_result
return NotAuthorized()
Here we passed extra information to the authenticate
of the backend to be used for internal operations.
Backends¶
For backends you need the AuthenticationMiddleware
(not the BaseAuthMiddleware from Lilya). Only here you can provide them
via the backend
parameter. This can be a sequence of AuthenticationBackend instances or also a single one.
If a backend doesn't find the user it can return None in authenticate
to skip to the next Backend.
If a backend raises an error in authenticate
, the whole chain is stopped.
Backends are retrievable on the middleware via the backend
attribute. It is always a list.
Users¶
Once you have installed AuthenticationMiddleware
, the request.user
interface becomes
available to your endpoints and other middleware.
The implementation should implement the interface UserInterface
, which includes two properties and any additional information your user model requires.
.is_authenticated
.display_name
Ravyn provides two built-in user implementations: AnonymousUser()
and BasicUser(username)
.
AuthCredentials¶
Authentication credentials should be considered distinct from user identities. An authentication scheme must be capable of granting or restricting specific privileges independently of the user's identity.
The AuthCredentials
class provides the basic interface that request.auth
exposes:
.scopes
Custom authentication error responses¶
You can customize the error response sent when an AuthenticationError
is raised by an authentication backend:
from lilya.middleware import DefineMiddleware
from ravyn import JSONResponse, Ravyn, Request
from ravyn.middleware.authentication import AuthenticationMiddleware
def on_auth_error(request: Request, exc: Exception):
return JSONResponse({"error": str(exc)}, status_code=401)
app = Ravyn(
middleware=[
DefineMiddleware(AuthenticationMiddleware, backend=BasicAuthBackend(), on_error=on_auth_error),
],
)