Authentication¶
Lilya provides a straightforward yet robust interface for managing authentication and permissions.
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.apps import Lilya
from lilya.authentication import (
AuthCredentials, AuthenticationBackend, AuthenticationError, BasicUser
)
from lilya.middleware import DefineMiddleware
from lilya.middleware.sessions import SessionMiddleware
from lilya.middleware.authentication import AuthenticationMiddleware
from lilya.responses import PlainText
from lilya.routing import Path
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)
async def homepage(request):
if request.user.is_authenticated:
return PlainTextResponse('Hello, ' + request.user.display_name)
return PlainTextResponse('Hello, you')
routes = [
Path("/", handler=homepage)
]
middleware = [
# must be defined before AuthenticationMiddleware, because of the SessionBackend
DefineMiddleware(SessionMiddleware, secret_key=...),
DefineMiddleware(AuthenticationMiddleware, backend=[SessionBackend(), BasicAuthBackend()])
]
app = Lilya(routes=routes, middleware=middleware)
Backends¶
For backends you need the AuthenticationMiddleware (not the BaseAuthMiddleware). Only here you can provide them
via the backend
parameter. This can be a sequence of AuthenticationBackend instances or a 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 inteface UserInterface
, which includes two properties and any additional information your user model requires.
.is_authenticated
.display_name
Lilya 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
Permissions¶
Permissions are enforced using an endpoint decorator that ensures the incoming request contains the necessary authentication scopes.
from lilya.authentication import requires
@requires('authenticated')
async def dashboard():
...
You can include either one or multiple required scopes:
from lilya.authentication import requires
@requires(['authenticated', 'admin'])
async def dashboard():
...
By default, a 403 response is returned when permissions are not granted. However, you may want to customize this behavior to prevent revealing information about the URL structure to unauthenticated users.
from lilya.authentication import requires
@requires(['authenticated', 'admin'], status_code=404)
async def dashboard():
...
Note
The status_code
parameter is not applicable for WebSockets. For WebSocket connections, a 403 (Forbidden) status code will always be used.
Alternatively, you may want to redirect unauthenticated users to a different page.
from lilya.authentication import requires
async def homepage():
...
@requires('authenticated', redirect='homepage')
async def dashboard():
...
When redirecting users, the destination page will include the original URL they requested in the next
query parameter:
from lilya.authentication import requires
from lilya.responses import RedirectResponse
@requires('authenticated', redirect='login')
async def admin():
...
async def login(request):
if request.method == "POST":
# Now that the user is authenticated,
# we can send them to their original request destination
if request.user.is_authenticated:
next_url = request.query_params.get("next")
if next_url:
return RedirectResponse(next_url)
return RedirectResponse("/")
For class-based controllers, apply the decorator to the specific method within the class.
from lilya.authentication import requires
from lilya.controllers import Controller
class Dashboard(Controller):
@requires("authenticated")
async def get(self, request):
...
Custom authentication error responses¶
You can customize the error response sent when an AuthenticationError
is raised by an authentication backend:
from lilya.apps import Lilya
from lilya.middleware import DefineMiddleware
from lilya.middleware.authentication import AuthenticationMiddleware
from lilya.requests import Request
from lilya.responses import JSONResponse
def on_auth_error(request: Request, exc: Exception):
return JSONResponse({"error": str(exc)}, status_code=401)
app = Lilya(
middleware=[
Middleware(AuthenticationMiddleware, backend=BasicAuthBackend(), on_error=on_auth_error),
],
)