Authentication & Authorization¶
Hypern provides a complete authentication and authorization module with JWT tokens, API key validation, and Role-Based Access Control (RBAC).
All components are pure Python (no external JWT library needed) and integrate seamlessly with Hypern's request context and OpenAPI generation.
JWT Authentication¶
Setup¶
from hypern import Hypern, JWTAuth
app = Hypern()
jwt = JWTAuth(
secret="your-secret-key",
expiry_seconds=3600, # 1 hour (default)
issuer="my-app", # optional iss claim
audience="web-app", # optional aud claim
)
Issuing Tokens¶
@app.post("/login")
def login(req, res, ctx):
data = req.json()
user = authenticate(data["username"], data["password"])
if not user:
res.status(401).json({"error": "Invalid credentials"})
return
token = jwt.encode({
"sub": str(user.id),
"roles": user.roles,
"name": user.name,
})
res.json({"token": token})
Protecting Routes¶
Use the @jwt.required decorator to enforce authentication:
@app.get("/me")
@jwt.required
def get_me(req, res, ctx):
user = ctx.get("auth_user") # decoded JWT payload
res.json({"user_id": user["sub"], "name": user["name"]})
On success the decorator stores:
| Context Key | Value |
|---|---|
auth_user |
Full decoded JWT payload |
auth_token |
Raw token string |
It also calls ctx.set_auth(user_id, roles) for the built-in Rust context.
Optional Authentication¶
Use @jwt.optional when a route should work for both authenticated and anonymous users:
@app.get("/feed")
@jwt.optional
def feed(req, res, ctx):
user = ctx.get("auth_user") # None if not authenticated
if user:
res.json({"feed": "personalized", "user": user["sub"]})
else:
res.json({"feed": "public"})
Token Lifecycle¶
# Refresh a token (preserves claims, resets expiry)
new_token = jwt.refresh(old_token)
# Revoke a token (adds to in-memory blacklist)
jwt.revoke(token)
# Custom expiry
short_token = jwt.encode({"sub": "1"}, expiry_seconds=300) # 5 minutes
Configuration Options¶
| Parameter | Default | Description |
|---|---|---|
secret |
(required for HS256) | HMAC-SHA256 signing key |
algorithm |
"HS256" |
Signing algorithm (HS256, RS256, ES256) |
expiry_seconds |
3600 |
Token lifetime in seconds |
issuer |
None |
Expected iss claim |
audience |
None |
Expected aud claim |
header_name |
"Authorization" |
HTTP header to read token from |
header_prefix |
"Bearer" |
Required prefix before the token |
auto_error |
True |
Auto-respond 401 on failure |
private_key_pem |
None |
PEM private key (RS256/ES256, for signing) |
public_key_pem |
None |
PEM public key (RS256/ES256, for verification) |
Asymmetric Algorithms (RS256 / ES256)¶
For production deployments that need asymmetric signing, Hypern supports RS256 (RSA + SHA-256) and ES256 (ECDSA P-256 + SHA-256). Signing and verification are performed in Rust for maximum performance.
RS256 Example¶
from hypern import JWTAuth
jwt = JWTAuth(
algorithm="RS256",
private_key_pem=open("private.pem").read(), # RSA private key
public_key_pem=open("public.pem").read(), # RSA public key
expiry_seconds=3600,
)
token = jwt.encode({"sub": "user-1", "roles": ["admin"]})
payload = jwt.decode(token) # verified with public key
ES256 Example¶
jwt = JWTAuth(
algorithm="ES256",
private_key_pem=open("ec_private.pem").read(), # EC P-256 private key
public_key_pem=open("ec_public.pem").read(), # EC P-256 public key
)
token = jwt.encode({"sub": "user-2"})
payload = jwt.decode(token)
Key Notes¶
- RS256/ES256 require at least one of
private_key_pemorpublic_key_pem. - Use
private_key_pemfor signing (encode). Usepublic_key_pemfor verification (decode). - A verify-only service can be configured with only
public_key_pem(it won't be able to sign). - Keys must be PEM-encoded. Pass the key string directly — no file path.
API Key Authentication¶
Setup¶
from hypern import APIKeyAuth
api_key = APIKeyAuth(
keys={
"sk-abc123": "service-a",
"sk-def456": "service-b",
},
header_name="X-API-Key", # default
query_param="api_key", # also check query string
cookie_name="api_key", # also check cookies
)
Protecting Routes¶
@app.get("/api/data")
@api_key.required
def get_data(req, res, ctx):
client = ctx.get("api_key_client") # e.g. "service-a"
res.json({"client": client, "data": [1, 2, 3]})
Dynamic Key Management¶
# Add a new key at runtime
api_key.add_key("sk-new999", "new-service")
# Remove a key
api_key.remove_key("sk-abc123")
# Validate programmatically
client = api_key.validate_key("sk-def456") # "service-b" or None
Extraction Priority¶
Keys are looked up in this order:
- HTTP header (
X-API-Keyby default) - Query parameter (if
query_paramis set) - Cookie (if
cookie_nameis set)
Role-Based Access Control (RBAC)¶
Setup¶
from hypern import RBACPolicy
rbac = RBACPolicy({
"admin": ["users:read", "users:write", "users:delete", "system:admin"],
"editor": ["users:read", "users:write"],
"viewer": ["users:read"],
})
Role Enforcement¶
@app.get("/users")
@jwt.required
@rbac.requires_role("viewer") # any of these roles
def list_users(req, res, ctx):
res.json({"users": get_all_users()})
@app.delete("/users/:id")
@jwt.required
@rbac.requires_role("admin")
def delete_user(req, res, ctx):
delete_by_id(req.param("id"))
res.status(204)
Permission Enforcement¶
@app.put("/users/:id")
@jwt.required
@rbac.requires_permission("users:write")
def update_user(req, res, ctx):
data = req.json()
res.json(update_by_id(req.param("id"), data))
Multiple Roles / Permissions¶
# Require ANY of these roles (default)
@rbac.requires_role("admin", "editor")
# Require ALL of these roles
@rbac.requires_role("admin", "editor", match_all=True)
# Require all permissions (default for permissions)
@rbac.requires_permission("users:read", "users:write")
# Require any permission
@rbac.requires_permission("users:read", "users:write", match_all=False)
Dynamic Role Management¶
# Add a new role
rbac.add_role("moderator", ["comments:delete", "users:read"])
# Grant additional permissions
rbac.grant("editor", "comments:delete")
# Revoke a permission
rbac.revoke("editor", "comments:delete")
# Remove a role entirely
rbac.remove_role("moderator")
Programmatic Checks¶
rbac.has_role(["admin", "viewer"], "admin") # True
rbac.has_any_role(["viewer"], ["admin", "viewer"]) # True
rbac.has_all_roles(["admin"], ["admin", "editor"]) # False
rbac.has_permission(["editor"], "users:write") # True
rbac.get_all_permissions(["editor", "viewer"]) # {"users:read", "users:write"}
Standalone Decorators¶
For simple cases where you don't need a full RBACPolicy instance, use the standalone decorators. They read roles/permissions directly from ctx.get("auth_user"):
from hypern import requires_role, requires_permission
@app.get("/admin")
@jwt.required
@requires_role("admin")
def admin_only(req, res, ctx):
res.json({"admin": True})
@app.put("/articles/:id")
@jwt.required
@requires_permission("articles:write")
def update_article(req, res, ctx):
...
OpenAPI Integration¶
When you use @jwt.required, @rbac.requires_role(...), or @rbac.requires_permission(...), Hypern's OpenAPI generator automatically:
- Marks the endpoint as requiring authentication
- Adds Required roles and Required permissions to the endpoint description
No additional configuration needed.
Complete Example¶
from hypern import Hypern, JWTAuth, APIKeyAuth, RBACPolicy, requires_role
app = Hypern()
jwt = JWTAuth(secret="super-secret-key", issuer="my-app")
api_key = APIKeyAuth(keys={"sk-prod-001": "frontend"})
rbac = RBACPolicy({
"admin": ["users:*"],
"user": ["users:read"],
})
@app.post("/login")
def login(req, res, ctx):
token = jwt.encode({"sub": "user-1", "roles": ["admin"]})
res.json({"token": token})
@app.get("/profile")
@jwt.required
def profile(req, res, ctx):
res.json(ctx.get("auth_user"))
@app.get("/admin/dashboard")
@jwt.required
@rbac.requires_role("admin")
def dashboard(req, res, ctx):
res.json({"stats": "..."})
@app.get("/api/public")
@api_key.required
def public_api(req, res, ctx):
res.json({"client": ctx.get("api_key_client")})
if __name__ == "__main__":
app.start(host="0.0.0.0", port=8000)