Getting Started¶
This guide builds a small but complete API that requires JWT authentication and role-based authorization, from installation to running tests.
Prerequisites¶
- Python 3.11+ (3.11, 3.12, 3.13, and 3.14 are supported)
- The
laurenweb framework pyjwtandcryptographyfor JWT support
Minimal bearer example¶
The fastest path to a protected endpoint:
from lauren import LaurenFactory, controller, get, module, use_guards
from lauren_guards import bearer_token, AuthUser
# 1. Define a verifier
async def verify(token: str) -> AuthUser | None:
if token == "my-secret-token":
return AuthUser(id="u1", roles=("admin",), scopes=("read", "write"))
return None
# 2. Build the guard class
BearerGuard = bearer_token(verify=verify, realm="MyAPI")
# 3. Apply it to the controller
@use_guards(BearerGuard)
@controller("/api")
class ApiController:
@get("/me")
async def me(self) -> dict:
return {"id": "u1"}
@module(controllers=[ApiController])
class AppModule:
pass
# 4. Build and run
app = LaurenFactory.create(AppModule)
Test it:
# Rejected — no token
curl http://localhost:8000/api/me
# {"error": {"code": "unauthorized", ...}}
# Accepted
curl -H "Authorization: Bearer my-secret-token" http://localhost:8000/api/me
# {"id": "u1"}
JWT + role-based authorization¶
A more realistic setup with RS256 JWT and a database-backed verifier:
from __future__ import annotations
import os
from lauren import (
LaurenFactory,
Scope,
controller,
get,
injectable,
module,
post_construct,
use_guards,
)
from lauren.exceptions import ForbiddenError
from lauren_guards import AuthUser, jwt_bearer, require_roles
# ---------------------------------------------------------------------------
# Database service (skeleton)
# ---------------------------------------------------------------------------
@injectable(scope=Scope.SINGLETON)
class UserRepository:
async def get_by_id(self, user_id: str) -> dict | None:
# Replace with real DB lookup
fake_db = {"u1": {"id": "u1", "roles": ["admin", "editor"]}}
return fake_db.get(user_id)
# ---------------------------------------------------------------------------
# JWT guard with DB enrichment
# ---------------------------------------------------------------------------
JwtGuard = jwt_bearer(
jwks_url=os.environ.get("JWKS_URL", "https://auth.example.com/.well-known/jwks.json"),
audience="my-api",
issuer="https://auth.example.com/",
# jwt_bearer populates AuthUser from the JWT's sub/scope claims by
# default; pass role_claim=/scope_claim= (a claim name or callable)
# when roles/scopes live under non-standard claims, or read the user
# from your own DB inside the controller via request.state.user.
)
# ---------------------------------------------------------------------------
# Controllers
# ---------------------------------------------------------------------------
@use_guards(JwtGuard) # all routes in this controller require JWT
@controller("/api/v1")
class ApiV1Controller:
def __init__(self, users: UserRepository) -> None:
self._users = users
@get("/profile")
async def profile(self) -> dict:
"""Every authenticated user can access their profile."""
return {"ok": True}
@get("/admin")
@use_guards(require_roles("admin")) # additionally requires 'admin' role
async def admin_panel(self) -> dict:
return {"panel": "admin"}
@get("/editor")
@use_guards(require_roles("editor", "admin"))
async def editor_view(self) -> dict:
return {"panel": "editor"}
# ---------------------------------------------------------------------------
# Module & application
# ---------------------------------------------------------------------------
@module(
controllers=[ApiV1Controller],
providers=[UserRepository],
)
class AppModule:
pass
app = LaurenFactory.create(
AppModule,
openapi_url="/openapi.json",
docs_url="/docs",
)
Public routes¶
Use the @public decorator to bypass all guards on specific routes:
from lauren_guards import IS_PUBLIC_KEY, public
@use_guards(BearerGuard)
@controller("/api")
class ApiController:
@get("/status")
@public # no auth required
async def health(self) -> dict:
return {"status": "ok"}
@get("/protected")
async def protected(self) -> dict:
return {"secret": True}
All guard classes from lauren-guards check IS_PUBLIC_KEY metadata at the start of can_activate. Custom guards can do the same:
from lauren_guards import IS_PUBLIC_KEY
class MyCustomGuard:
async def can_activate(self, ctx) -> bool:
if ctx.get_metadata(IS_PUBLIC_KEY):
return True # bypass
# ... your logic ...
return True
See Public routes for details.
Testing guards¶
Use lauren.testing.TestClient for full integration tests:
from lauren.testing import TestClient
from lauren_guards import bearer_token, AuthUser
async def verify(t: str) -> AuthUser | None:
return AuthUser(id="u1") if t == "ok" else None
Guard = bearer_token(verify=verify)
@use_guards(Guard)
@controller("/secure")
class SecureController:
@get("/data")
async def data(self) -> dict:
return {"ok": True}
@module(controllers=[SecureController])
class Mod:
pass
def test_auth():
client = TestClient(LaurenFactory.create(Mod))
# Unauthenticated
r = client.get("/secure/data")
assert r.status_code == 401
# Authenticated
r = client.get("/secure/data", headers={"Authorization": "Bearer ok"})
assert r.status_code == 200
assert r.json()["ok"] is True
Where to go next¶
- Guard catalog — every authentication guard in detail.
- Authorization — role and scope checks.
- Reference — complete API surface.