Skip to content

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 lauren web framework
  • pyjwt and cryptography for JWT support
pip install lauren "lauren-guards[jwt]"

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