Authentication
LightAPI provides JWT-based authentication via the Authentication Meta option. Authentication is per-endpoint and composable with permission classes.
Quick Start
import os
from typing import Optional
from sqlalchemy import create_engine
from lightapi import (
LightApi, RestEndpoint, Field,
Authentication, JWTAuthentication, IsAuthenticated, IsAdminUser,
)
class PostEndpoint(RestEndpoint):
title: str
body: str
author_id: Optional[int] = None
class Meta:
authentication = Authentication(
backend=JWTAuthentication,
permission=IsAuthenticated,
)
engine = create_engine("sqlite:///app.db")
app = LightApi(engine=engine)
app.register({"/posts": PostEndpoint})
Any request that does not carry a valid Authorization: Bearer <token> header now receives 401 Unauthorized.
Authentication class
from lightapi import Authentication
from lightapi.authentication import JWTAuthentication, IsAuthenticated
Authentication(
backend=JWTAuthentication, # Authentication backend class
permission=IsAuthenticated, # Permission class (or dict for per-method permissions)
)
| Parameter | Type | Description |
|---|---|---|
backend |
type \| None |
Authentication backend. None = public access. |
permission |
type \| dict[str, type] \| None |
Permission class applied globally, or a {method: class} dict for per-method control. |
JWTAuthentication
Authenticates requests using a Bearer token in the Authorization header.
Required environment variable:
export LIGHTAPI_JWT_SECRET="your-secret-key-at-least-32-bytes-long" # 32 bytes or more: PyJWT warns on shorter HMAC keys (RFC 7518, 3.2)
Token format:
The token payload is stored in request.state.user after successful authentication.
Auto-registered login endpoint
When any endpoint declares Authentication(backend=JWTAuthentication) (or
BasicAuthentication), LightAPI automatically registers POST /auth/login
and POST /auth/token (the same handler under both paths). The base path
defaults to /auth and can be changed via LightApi(auth_path="/api/auth").
Pass a login_validator(username, password) -> dict | None to LightApi(...)
to validate credentials:
def login_validator(username: str, password: str):
if username == "admin" and password == "secret":
return {"sub": "1", "username": "admin", "is_admin": True}
return None
app = LightApi(engine=engine, login_validator=login_validator)
Raising an exception from login_validator is treated identically to returning None — the client receives 401 Unauthorized. The exception is logged at WARNING level and is never surfaced to the API consumer.
The dict returned by the validator is the JWT payload. On JWT-protected apps,
POST /auth/login returns {"token": "<jwt>", "user": {...}}; the client
then sends Authorization: Bearer <jwt> on subsequent requests.
# 1. Log in to obtain a token
curl -X POST http://localhost:8000/auth/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"secret"}'
# → 200 {"token": "eyJhbGc...", "user": {"sub": "1", ...}}
# 2. Call a protected endpoint
curl -H 'Authorization: Bearer eyJhbGc...' http://localhost:8000/posts
Generating tokens manually
If you do not want to use login_validator and the auto-registered endpoint,
issue tokens directly:
import jwt, os, datetime
def make_token(user_id: int, is_admin: bool = False) -> str:
return jwt.encode(
{
"sub": str(user_id),
"is_admin": is_admin,
"exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1),
},
os.environ["LIGHTAPI_JWT_SECRET"],
algorithm="HS256",
)
Permission Classes
AllowAny
No authentication check — equivalent to having no Meta.authentication. Default when permission is None.
IsAuthenticated
Allows access only if JWTAuthentication.authenticate() returns True (valid token present).
IsAdminUser
Allows access only if the token payload contains "is_admin": true.
from lightapi import Authentication
from lightapi.authentication import JWTAuthentication, IsAdminUser
class AdminEndpoint(RestEndpoint):
name: str
class Meta:
authentication = Authentication(
backend=JWTAuthentication,
permission=IsAdminUser,
)
Per-Method Permissions
Pass a dict[str, type] to permission to apply different rules per HTTP verb:
from lightapi import (
Authentication, JWTAuthentication,
IsAuthenticated, IsAdminUser, AllowAny,
)
class ArticleEndpoint(RestEndpoint):
title: str
body: str
class Meta:
authentication = Authentication(
backend=JWTAuthentication,
permission={
"GET": AllowAny, # public reads
"POST": IsAuthenticated, # authenticated creates
"PUT": IsAuthenticated,
"PATCH": IsAuthenticated,
"DELETE": IsAdminUser, # admin-only deletes
},
)
Custom Authentication Backend
Subclass BaseAuthentication to implement your own logic:
from lightapi.authentication import BaseAuthentication
class ApiKeyAuthentication(BaseAuthentication):
def authenticate(self, request) -> bool:
key = request.headers.get("X-Api-Key")
return key == "my-secret-api-key"
Use it as the backend:
class SecureEndpoint(RestEndpoint):
value: str
class Meta:
authentication = Authentication(backend=ApiKeyAuthentication)
Custom Permission Class
Subclass BasePermission to implement your own access control:
from lightapi.authentication import BasePermission
class IsOwner(BasePermission):
def has_permission(self, request) -> bool:
user = getattr(request.state, "user", None)
if user is None:
return False
resource_owner_id = request.path_params.get("id")
return str(user.get("sub")) == str(resource_owner_id)
Public Endpoints
To make an endpoint fully public, either omit Meta.authentication or set:
from lightapi import Authentication, AllowAny
class PublicEndpoint(RestEndpoint):
name: str
class Meta:
authentication = Authentication(permission=AllowAny)
CORS Preflight
OPTIONS requests are automatically allowed by JWTAuthentication without checking the token, ensuring CORS preflight requests always succeed.
Complete Example
import os
from typing import Optional
from sqlalchemy import create_engine
from lightapi import (
LightApi, RestEndpoint, Field,
Authentication, JWTAuthentication,
IsAuthenticated, IsAdminUser, AllowAny,
)
os.environ.setdefault("LIGHTAPI_JWT_SECRET", "dev-secret-at-least-32-bytes-long")
class UserEndpoint(RestEndpoint):
username: str = Field(min_length=3, unique=True)
email: str = Field(unique=True)
is_admin: Optional[bool] = None
class Meta:
authentication = Authentication(
backend=JWTAuthentication,
permission={
"GET": AllowAny,
"POST": IsAdminUser,
"PUT": IsAuthenticated,
"PATCH": IsAuthenticated,
"DELETE": IsAdminUser,
},
)
engine = create_engine("sqlite:///app.db")
app = LightApi(engine=engine)
app.register({"/users": UserEndpoint})
app.run()
# Unauthenticated read — allowed
curl http://localhost:8000/users
# Create without token — 401
curl -X POST http://localhost:8000/users \
-H "Content-Type: application/json" \
-d '{"username": "bob", "email": "[email protected]"}'
# Create with admin token — 201
TOKEN=$(python -c "
import jwt, datetime
print(jwt.encode({'sub':'1','is_admin':True,'exp':datetime.datetime.utcnow()+datetime.timedelta(hours=1)},
'dev-secret-at-least-32-bytes-long', algorithm='HS256'))
")
curl -X POST http://localhost:8000/users \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"username": "bob", "email": "[email protected]"}'