DocumentationAuthentication patterns
API referenceGitHub

Authentication patterns

Choose JWT, API-key, or Basic authentication, then protect the whole app, a router, or an individual endpoint.

Work through each step in order. The examples use the generated LenAPI project structure and are designed to be adapted to your app.

01

Select the default authenticator in settings.py

Choose one primary mode for app.authenticator. For quick development, LenAPI uses its built-in User model and its api_key column. You can instead pass your own user_model to authentication(); for custom API-key auth, explicitly provide api_key_field naming the model column that stores the key digest. Authentication does not automatically protect routes or generate login and registration endpoints.

Python
# settings.py
AUTHENTICATION = "jwt"  # jwt, api_key, basic_login, opaque_bearer, or session

# JWT settings are generated with a unique SECRET_KEY.
TOKEN = {
    "SECRET_KEY": SECRET_KEY,
    "LIFETIME_SECONDS": 3600,
    "REFRESH_LIFETIME_SECONDS": 2592000,
    "ISSUER": API_TITLE,
}

# Used only by basic_login. This can be username, email, phone, etc.
BASIC_AUTH_FIELD = "username"
02

Protect the whole app, a router, or one route

Pass auth= to LENAPI to require it on all application routes. Router(auth=...) scopes protection to one group, while a route decorator's auth= protects one endpoint. Use Router scope when public registration or token routes share the same app; app-wide auth will also guard those routes.

Python
from lenapi import Depends, LENAPI, Router, authentication

# App-wide: every route registered on this app requires JWT.
auth = authentication("jwt")
app = LENAPI(auth=auth)

@app.get("/account")
def account(identity=Depends(auth)):
    return {"identity": identity}

# Or scope to a router using settings.py's selected authenticator:
app = LENAPI()
api_auth = app.authenticator
api = Router(prefix="/api", auth=api_auth)

@api.get("/profile")
def profile(identity=Depends(api_auth)):
    return {"user_id": identity["sub"]}

@api.get("/admin", auth=api_auth)
def admin(identity=Depends(api_auth)):
    return {"identity": identity}

app.include_router(api)
TIP

settings.AUTHENTICATION selects app.authenticator; it does not make auth global by itself. Attach the authenticator with auth= where you want it enforced. For email or phone login, the field must exist on the user model and your registration flow must populate it.

03

Use your own user model

JWT and Basic authentication can use a developer model by passing user_model. For Basic login, set BASIC_AUTH_FIELD in settings.py to a column on that model; the password column is named password and must contain a LenAPI-compatible one-way hash. The framework default User remains available for quick setup.

Python
from lenapi import authentication
from app.models import Customer

# settings.py: BASIC_AUTH_FIELD = "email"
basic_auth = authentication("basic_login", user_model=Customer)
jwt_auth = authentication("jwt", user_model=Customer)
04

Use JWT and mount token endpoints

The default JWT authenticator checks usernames and password hashes against the framework User model. Mount its router to provide POST /auth/token and POST /auth/refresh, then attach the same authenticator to private routes. The token and refresh operations are intentionally hidden from Swagger; protected operations still show Bearer authentication.

Python
from lenapi import Depends, LENAPI, Router
from lenapi.authentication import UserRegistration, register_user

app = LENAPI()  # settings.AUTHENTICATION = "jwt"
jwt = app.authenticator
app.include_router(jwt.router())  # POST /auth/token and /auth/refresh

public = Router(prefix="/auth")
@public.post("/register")
def register(data: UserRegistration):
    return register_user(data=data, authentication="jwt")

private = Router(prefix="/api", auth=jwt)
@private.get("/profile")
def profile(identity=Depends(jwt)):
    return {"user_id": identity["sub"]}

app.include_router(public)
app.include_router(private)
TIP

POST /auth/token accepts username and password and returns access_token plus refresh_token. Send the access token as Authorization: Bearer <access_token>. A refresh token is single-use; use shared database or Redis security storage when running multiple workers.

05

Enable API-key authentication from the CLI

Run the CLI to select API-key authentication, add the optional APIKey management model, and run migrations. At request time, the built-in authenticator uses the framework User model: it hashes the presented key and compares that digest with User.api_key. The generated APIKey table is a separate app-managed key registry linked to User; the built-in authenticator and register_user() do not read or populate it automatically.

Python
# From the generated project directory
python lenora.py apikey

# The CLI adds this model to app/models.py and runs migration:
from datetime import datetime
from lenapi import model

class APIKey(model.Model):
    user_id = model.ForeignKeyField("user.id", model=model.CASCADE, nullable=False)
    key_hash = model.StringField(max_length=64, unique=True, nullable=False)
    is_active = model.BooleanField(default=True, nullable=False)
    created_at = model.DateTimeField(default=datetime.utcnow, nullable=False)
    revoked_at = model.DateTimeField(nullable=True)
TIP

The framework User row stores the active key digest in its api_key column. APIKey.user_id links optional management records to that User. If you want authentication to check a custom model or the APIKey table, configure your own lookup/dependency; never store a raw key in key_hash.

06

Use a custom model and API-key column

For quick development, omit user_model and use the framework User.api_key field. For your own model, provide both user_model and api_key_field; there is deliberately no implicit field-name fallback. The selected column must exist and stores only the SHA-256 digest. LenAPI validates it during setup and raises a clear error if it is missing.

Python
from lenapi import authentication, hash_api_key, make_api_key, model

class Customer(model.Model):
    username = model.StringField(max_length=120, unique=True)
    password = model.StringField(max_length=255)
    key_digest = model.StringField(max_length=64, unique=True, nullable=True)

auth = authentication(
    "api_key",
    user_model=Customer,
    api_key_field="key_digest",
)

# In a trusted provisioning flow, show raw_key once and store only its digest.
raw_key = make_api_key()
customer = Customer.get(id=1).get()
customer.key_digest = hash_api_key(raw_key)
customer.save()
TIP

If you use register_user() with a custom model, also pass api_key_field='key_digest'. The custom model must expose the fields required by your registration flow.

07

Return the API key once and send it on requests

register_user creates a cryptographically random URL-safe key when authentication='api_key'. It stores only the SHA-256 digest on User.api_key and includes the raw key once in the response. Keep the raw value for the caller; it cannot be recovered later.

Python
from lenapi import LENAPI, Router
from lenapi.authentication import UserRegistration, register_user

app = LENAPI()
public = Router(prefix="/auth")

@public.post("/register")
def register(data: UserRegistration):
    return register_user(data=data, authentication="api_key")

app.include_router(public)

# A successful response includes: {"api_key": "<show once to the user>"}
# Later requests send: X-API-Key: <api_key>
08

Use database-backed Basic authentication

Set basic_login and optionally choose the identifier column. Passwords must be stored as one-way hashes; register_user handles hashing. The client sends the identifier and password in the HTTP Basic Authorization header on every request, so use HTTPS.

Python
# settings.py
AUTHENTICATION = "basic_login"
BASIC_AUTH_FIELD = "email"  # This column exists on the framework User model.

# main.py
from pydantic import Field
from lenapi import Depends, LENAPI, Router
from lenapi.authentication import UserRegistration, register_user

class EmailRegistration(UserRegistration):
    email: str = Field(min_length=3, max_length=254)

app = LENAPI()
basic = app.authenticator
public = Router(prefix="/auth")
private = Router(prefix="/api", auth=basic)

@public.post("/register")
def register(data: EmailRegistration):
    return register_user(data=data, authentication="basic_login")

@private.get("/profile")
def profile(identity=Depends(basic)):
    return {"user_id": identity}

app.include_router(public)
app.include_router(private)

# Client example:
# requests.get(url, auth=("ada@example.com", "password"))
09

Use server-side sessions

A session dependency reads a cookie and resolves the session identity from app.security_store. Create the session only after your login route verifies credentials. Store the identity server-side and send only a random, HttpOnly session ID to the browser.

Python
import json
import secrets
from lenapi import Depends, JSONResponse, LENAPI, Request
from lenapi.security import session_auth

app = LENAPI()  # SECURITY_STORAGE defaults to database
session = session_auth(store=app.security_store, cookie_name="session_id")

@app.post("/session/login")
async def login():
    # Replace with real credential verification first.
    identity = {"user_id": "123"}
    session_id = secrets.token_urlsafe(32)
    await app.security_store.set(
        f"session:{session_id}", json.dumps(identity), ttl=3600
    )
    response = JSONResponse({"authenticated": True})
    response.set_cookie(
        "session_id", session_id, max_age=3600, path="/",
        httponly=True, secure=True, samesite="lax",
    )
    return response

@app.get("/session/me")
def session_me(identity=Depends(session)):
    print(identity)
    return identity

@app.post("/session/logout")
async def logout(request: Request, identity=Depends(session)):
    session_id = request.cookies.get("session_id")
    print(session_id)
    await app.security_store.delete(f"session:{session_id}")

    response = JSONResponse({"message": "Logged out"})
    response.delete_cookie("session_id")
    return response
TIP

For local plain-HTTP development, set secure=False; use secure=True behind HTTPS in production. Logout requires a valid session, deletes its server-side record, and expires the browser cookie. Add CSRF protection to state-changing cookie-authenticated routes.

10

Use opaque bearer tokens

Opaque tokens are random values checked against the shared security store on every request. They are not self-contained JWTs, so deleting the store entry revokes the token immediately.

Python
import json
import secrets
from lenapi import Depends, JSONResponse, LENAPI, Request
from lenapi.security import opaque_bearer_auth

app = LENAPI()
bearer = opaque_bearer_auth(store=app.security_store)

async def issue_token(identity: dict) -> str:
    token = secrets.token_urlsafe(32)
    await app.security_store.set(
        f"opaque_bearer:{token}", json.dumps(identity), ttl=3600
    )
    return token

async def revoke_token(token: str) -> bool:
    await app.security_store.delete(f"opaque_bearer:{token}")
    return True

@app.post("/opaque/token")
async def token(request: Request):
    # Verify credentials before issuing a token in a real app.
    value = await issue_token({"user_id": 1, "name": "Tolu"})
    return JSONResponse({"token": value})

@app.get("/opaque/profile", auth=bearer)
def profile(request: Request, identity=Depends(bearer)):
    # Do not log the raw bearer token in production.
    return identity

@app.post("/opaque/revoke", auth=bearer)
async def revoke(request: Request, identity=Depends(bearer)):
    token = request.headers["authorization"].removeprefix("Bearer").strip()
    await revoke_token(token)
    return {"message": "Token revoked"}
TIP

The sample token route uses a fixed identity to demonstrate storage; verify credentials before issuing tokens. The strip() after removeprefix('Bearer') is needed so revocation looks up the same token value that was stored. Return tokens only once, never log them, and use HTTPS. Database storage is the generated default; Redis is also available.

11

When to use HMAC

Use HMAC for trusted service-to-service requests and webhooks when both sides can share a secret. The sender signs each request; the receiver recomputes the signature. HMAC is not a login flow and does not issue bearer tokens. The signature covers the timestamp, uppercase method, path and query, SHA-256 digest of the exact body bytes, and a unique nonce.

  • Use it when another service posts webhooks to your LenAPI endpoint.
  • Use the same signing format when a LenAPI app calls a partner that accepts HMAC-signed requests.
  • Provision a shared secret of at least 32 bytes to both parties using a secure channel.
12

Verify a partner webhook in LenAPI

Configure hmac_auth with the shared secret. header_name is the partner-specific signature header; timestamp and nonce remain X-LenAPI-Timestamp and X-LenAPI-Nonce. Pass app.security_store as nonce_store to reject replayed requests across workers. The dependency verifies raw bytes before the route parses arbitrary JSON.

Python
# settings.py: PARTNER_HMAC_SECRET = a securely configured 32+ byte secret
from typing import Any
from lenapi import Depends, LENAPI, Request
from lenapi.security import hmac_auth
from settings import PARTNER_HMAC_SECRET

app = LENAPI()
partner_auth = hmac_auth(
    PARTNER_HMAC_SECRET,
    tolerance_seconds=300,
    nonce_store=app.security_store,
    header_name="X-Flutterwave",
)

@app.post("/partner/webhook", auth=partner_auth)
async def receive_partner_webhook(
    request: Request,
    identity=Depends(partner_auth),
):
    body: Any = await request.json()  # accepts variable JSON payloads
    # Validate event type and fields before performing business actions.
    return {"accepted": True, "body": body}
TIP

In production, use HTTPS, protect the shared secret, validate the webhook event before acting on it, and configure database or Redis security storage for multi-worker replay protection. Without nonce_store, replay tracking is process-local.

13

Send a signed webhook without installing LenAPI

A partner client only needs Python's standard library and requests. Serialize the JSON once, sign those exact bytes, and send the same bytes unchanged. The signature header must be the same name configured in hmac_auth.

Python
import hashlib
import hmac
import json
import secrets
import time
import requests

client_secret = "same-secret-configured-on-the-LenAPI-server"
url = "https://api.example.com/partner/webhook"
method = "POST"
path = "/partner/webhook"
payload = {"event": "payment.completed", "reference": "order-123"}
body = json.dumps(payload, separators=(",", ":")).encode("utf-8")
timestamp = str(int(time.time()))
nonce = secrets.token_urlsafe(16)
body_digest = hashlib.sha256(body).hexdigest()
message = f"{timestamp}\n{method}\n{path}\n{body_digest}\n{nonce}".encode("utf-8")
signature = hmac.new(
    client_secret.encode("utf-8"), message, hashlib.sha256
).hexdigest()

response = requests.post(
    url,
    data=body,
    headers={
        "Content-Type": "application/json",
        "X-LenAPI-Timestamp": timestamp,
        "X-LenAPI-Nonce": nonce,
        "X-Flutterwave": signature,
    },
    timeout=15,
)
print(response.status_code, response.text)
TIP

The signed message is timestamp, uppercase method, path plus query string, lowercase-hex SHA-256 body digest, and nonce, joined by newline characters. A stale timestamp, reused nonce, mismatched custom header, or body/path changed after signing causes verification to fail.

14

Sign an outgoing request from a LenAPI app

LenAPI can also be the HMAC client. Generate the secret once and share it securely with the receiving service; HMAC does not issue a new token per request. Use hmac_request_signature for the same signing format, then send the body and signature headers.

Python
import json
import secrets
import time
import requests
from lenapi.security import hmac_request_signature

shared_secret = "same-secret-provisioned-to-the-receiver"
url = "https://partner.example.com/events"
method = "POST"
path = "/events"
payload = {"event": "account.updated", "account_id": "acct-42"}
body = json.dumps(payload, separators=(",", ":")).encode("utf-8")
timestamp = str(int(time.time()))
nonce = secrets.token_urlsafe(16)
signature = hmac_request_signature(
    shared_secret, method, path, timestamp, body, nonce=nonce
)
response = requests.post(
    url, data=body,
    headers={
        "Content-Type": "application/json",
        "X-LenAPI-Timestamp": timestamp,
        "X-LenAPI-Nonce": nonce,
        "X-LenAPI-Signature": signature,
    },
    timeout=15,
)
print(response.status_code, response.text)
15

Choose the OIDC integration

OIDC is a protocol for identity built on OAuth 2.0. LenAPI offers two different integration points. SocialAuth runs supported browser login and callback routes; oidc_auth protects an API route using a token that the client already obtained elsewhere. Choose based on which side owns the sign-in flow.

  • Choose SocialAuth for LenAPI-managed Google, GitHub, or LinkedIn login routes, callback handling, optional local user registration, and an optional LenAPI JWT for later API calls.
  • Choose oidc_auth when a frontend, mobile client, or external identity system already performs login and sends its provider token to your API.
  • oidc_auth does not create login/callback routes, register users, or mint a new token. It validates and exposes identity claims for protected routes.
TIP

In LenAPI's current SocialAuth integration, Google and LinkedIn use OIDC identity endpoints. GitHub uses OAuth 2.0 profile/email APIs and is not an OIDC provider in this integration.

16

Validate an external OIDC token on a route

Provide a callback that validates the token with a maintained OIDC/JWT library. It must verify the signature using trusted provider keys and validate issuer, audience, expiry, and nonce where required. Never accept claims by merely decoding the JWT.

Python
from lenapi import Depends, LENAPI
from lenapi.security import oidc_auth

app = LENAPI()

def verify_provider_token(token: str):
    # Replace with a real, cryptographically verified library call.
    claims = verify_with_provider_keys(
        token, issuer="https://issuer.example", audience="your-client-id"
    )
    if claims is None:
        return None
    return {
        "sub": claims["sub"],
        "email": claims.get("email"),
        "scope": claims.get("scope", ""),
    }

provider_auth = oidc_auth(verify_provider_token)

@app.get("/oidc/profile", auth=provider_auth)
def oidc_profile(identity=Depends(provider_auth)):
    return identity
TIP

The client sends Authorization: Bearer <provider-token>. auth= enforces authentication, and Depends(provider_auth) injects verified claims. Add authorization checks for roles, scopes, and resource ownership in your application.