Usage¶
This page covers the complete feature set of Authority, both sync
(AuthManager) and async (AsyncAuthManager). The two managers expose the
same 55-method surface; the async version's methods are coroutines.
Core lifecycle¶
Sync¶
from authority import AuthConfig, AuthManager
from authority.storage import SQLiteStorage
config = AuthConfig(
jwt_secret_key="your-secret-key-min-32-chars",
fernet_key="your-fernet-key",
)
storage = SQLiteStorage("auth.db")
auth = AuthManager(config, storage)
user = auth.register(
name="Alice",
email="alice@example.com",
password="SecureP@ssw0rd!",
auto_verify=True,
)
result = auth.login(email="alice@example.com", password="SecureP@ssw0rd!")
payload = auth.verify_access_token(result["access_token"])
user = auth.get_user(payload["user_id"])
auth.logout(
user_id=user["id"],
refresh_token=result["refresh_token"],
access_token_jti=payload["jti"],
)
auth.close()
Async¶
import asyncio
from authority import AuthConfig, AsyncAuthManager
from authority.storage import AsyncSQLiteStorage
async def main():
config = AuthConfig(
jwt_secret_key="your-secret-key-min-32-chars",
fernet_key="your-fernet-key",
)
storage = AsyncSQLiteStorage("auth.db")
await storage.connect()
auth = AsyncAuthManager(config, storage)
user = await auth.register(
name="Bob",
email="bob@example.com",
password="SecureP@ssw0rd!",
auto_verify=True,
)
result = await auth.login(email="bob@example.com", password="SecureP@ssw0rd!")
payload = await auth.verify_access_token(result["access_token"])
count = await auth.logout_all(user_id=user["id"])
print(f"Revoked {count} tokens")
await auth.close()
asyncio.run(main())
Tokens¶
login() returns a dict with:
| Key | Description |
|---|---|
access_token |
Short-lived JWT (15 minutes by default) |
refresh_token |
Opaque token, hashed at rest, rotated on use |
token_type |
"Bearer" |
verify_access_token(token) returns the JWT payload (user_id, jti, iat,
exp, iss, typ).
Refresh tokens are rotated by default. The old token becomes unusable and a new
pair is issued; a refresh_token_reuse_grace_seconds window (default 10s)
absorbs legitimate race-condition retries. If a rotated token is reused after
the grace period, the entire token family is revoked.
User management¶
user = auth.get_user(user_id)
profile = auth.get_profile(user_id) # profile JSON blob
user = auth.update_user(user_id, {"name": "Alice Smith"})
profile = auth.update_profile(user_id, {"theme": "dark"})
auth.delete_user(user_id)
Email verification¶
token = auth.request_email_verification(user_id)
# email the token to the user...
user = auth.verify_email(token)
Password reset¶
request_password_reset() always returns a token string, even for unknown
emails (an empty string when the user does not exist), to prevent email
enumeration.
token = auth.request_password_reset("alice@example.com")
if token:
user = auth.reset_password(token, "NewSecureP@ssw0rd!")
Multi-factor authentication¶
TOTP-based, with secrets encrypted at rest using Fernet and single-use recovery codes.
# Step 1 — initiate setup; returns the shared secret and a provisioning URI
setup = auth.setup_mfa(user_id=user["id"])
print(setup["secret"])
print(setup["provisioning_uri"]) # render as a QR code in your frontend
# Step 2 — user scans the QR code and enters a 6-digit code
result = auth.verify_and_enable_mfa(user_id=user["id"], code="123456")
print(result["recovery_codes"]) # store these; shown only once
# Step 3 — during login, when MFA is enabled:
login_result = auth.login(email="alice@example.com", password="...")
if login_result.get("mfa_required"):
tokens = auth.verify_mfa_login(
user_id=login_result["user_id"],
code="654321", # a TOTP code or a recovery code
)
# Status
status = auth.get_mfa_status(user_id=user["id"])
print(status["mfa_enabled"], status["recovery_codes_remaining"])
# Disable (requires the current password)
auth.disable_mfa(user_id=user["id"], password="...")
# Regenerate recovery codes (invalidates old ones, requires password)
auth.regenerate_recovery_codes(user_id=user["id"], password="...")
WebAuthn / passkeys¶
The four WebAuthn methods map directly onto the browser API.
# 1. Get registration options for navigator.credentials.create()
options = auth.start_webauthn_registration(user_id=user["id"])
# 2. Verify the browser response
cred = auth.complete_webauthn_registration(
user_id=user["id"],
credential_data=credential_data, # dict from navigator.credentials.create()
)
# 3. Get authentication options for navigator.credentials.get()
options = auth.start_webauthn_authentication(user_id=user["id"])
# 4. Verify the assertion — returns tokens on success
result = auth.complete_webauthn_authentication(credential_data=assertion_data)
# List / remove credentials
auth.list_webauthn_credentials(user_id=user["id"])
auth.delete_webauthn_credential(user_id=user["id"], credential_id="...")
WebAuthn requires webauthn_rp_id and webauthn_expected_origin to be set in
the configuration.
RBAC¶
Roles and permissions are managed through the manager. Users gain permissions through their roles.
admin_role = auth.create_role("admin", "Full system access")
read_perm = auth.create_permission("posts:read")
auth.assign_permission_to_role(admin_role["id"], read_perm["id"])
auth.assign_role_to_user(user_id=user["id"], role_id=admin_role["id"])
if auth.has_permission(user_id=user["id"], permission_code="posts:read"):
print("Can read posts")
auth.require_permission(user_id=user["id"], permission_code="posts:read")
# raises InsufficientPermissionsError if the user lacks the permission
print(auth.get_user_permissions(user_id=user["id"]))
print(auth.get_user_roles(user_id=user["id"]))
Other RBAC methods: delete_role, list_roles, delete_permission,
list_permissions, remove_permission_from_role, get_role_permissions,
remove_role_from_user.
New users are automatically assigned the role named by default_user_role
("user" by default).
API keys¶
Scoped keys with prefix-based lookup for machines.
result = auth.create_api_key(
user_id=user["id"],
description="Production API access",
scopes=["read", "write"],
expires_in_days=90,
)
print(result["key"]) # plaintext, shown only once
print(result["prefix"])
info = auth.verify_api_key(result["key"])
print(info["user_id"], info["scopes"])
auth.list_api_keys(user_id=user["id"])
auth.revoke_api_key(user_id=user["id"], key_prefix=result["prefix"])
Sessions¶
Refresh tokens are tracked as sessions and can be listed or revoked.
auth.list_sessions(user_id=user["id"])
auth.revoke_session_by_id(user_id=user["id"], session_token_id=42)
count = auth.revoke_all_sessions_for_user(user_id=user["id"])
Audit log¶
Every sensitive operation writes an append-only audit entry. Chain hashing links entries so tampering is detectable.
Events¶
Pass an EventBus to the manager to react to lifecycle events with sync or
async handlers.
from authority import Event, EventBus
bus = EventBus()
def on_login(data):
print(f"Login: user {data['user_id']} from {data.get('ip_address')}")
async def on_register(data):
await send_welcome_email(data["email"])
bus.on(Event.USER_LOGIN_SUCCESS, on_login)
bus.on(Event.USER_REGISTERED, on_register)
auth = AuthManager(config, storage, event_bus=bus)
The 22 events: USER_REGISTERED, USER_LOGIN_SUCCESS, USER_LOGIN_FAILED,
USER_LOGOUT, USER_PASSWORD_CHANGED, USER_PASSWORD_RESET,
USER_EMAIL_CHANGED, USER_DELETED, MFA_SETUP_INITIATED, MFA_ENABLED,
MFA_DISABLED, MFA_FAILED, TOKEN_REFRESHED, TOKEN_REUSE_DETECTED,
TOKEN_REVOKED, WEBAUTHN_CREDENTIAL_ADDED, WEBAUTHN_CREDENTIAL_REMOVED,
RBAC_ROLE_ASSIGNED, RBAC_ROLE_REVOKED, API_KEY_CREATED,
API_KEY_REVOKED, AUDIT_EVENT_LOGGED.
Storage¶
Authority ships SQLiteStorage (sync) and AsyncSQLiteStorage (async,
requires the async extra). To use another database, implement
StorageInterface or AsyncStorageInterface and pass it to the manager:
from authority.storage.base import StorageInterface
class PostgresStorage(StorageInterface):
def get_user_by_id(self, user_id: int) -> dict | None:
...
# ... implement all abstract methods
Framework integrations¶
FastAPI¶
Requires the fastapi extra. Bind a global manager with init_auth, then use
the provided dependencies.
from fastapi import FastAPI, Depends
from authority import AuthConfig, AsyncAuthManager
from authority.storage import AsyncSQLiteStorage
from authority.fastapi import get_current_user, init_auth, require_permission
app = FastAPI()
config = AuthConfig(jwt_secret_key="...", fernet_key="...")
storage = AsyncSQLiteStorage("auth.db")
auth = AsyncAuthManager(config, storage)
init_auth(auth)
require_admin = require_permission("admin.access")
@app.get("/me")
async def me(user: dict = Depends(get_current_user)):
return {"id": user["id"], "name": user["name"], "email": user["email"]}
@app.get("/admin")
async def admin(user: dict = Depends(require_admin)):
return {"message": "Welcome, admin!"}
Connect storage and close the manager in the FastAPI lifespan or startup /
shutdown events. require_role(role_name) is available too.
Flask¶
Requires the flask extra. FlaskAuth provides instance decorators; module
helpers (init_auth, current_user, login_required, require_permission,
require_role) are also available.
from flask import Flask, jsonify
from authority import AuthConfig, AuthManager
from authority.storage.sqlite import SQLiteStorage
from authority.flask import FlaskAuth
app = Flask(__name__)
app.secret_key = "app-session-secret"
auth = FlaskAuth(
app,
AuthManager(
AuthConfig(jwt_secret_key="...", fernet_key="..."),
SQLiteStorage("auth.db"),
),
)
@app.route("/me")
@auth.login_required
def me():
user = auth.current_user()
return jsonify({"id": user["id"], "email": user["email"]})
@app.route("/admin")
@auth.require_permission("admin.access")
def admin():
return "Welcome, admin!"
Tokens are read from the Authorization: Bearer <token> header, or from
session["access_token"] for session-based flows.
Django¶
Requires the django extra. init_auth() binds the manager; decorators and
helpers protect views; AuthorityBackend plugs into Django's session auth.
from django.contrib.auth import authenticate
from django.http import JsonResponse
from django.urls import path
from authority.django import get_current_user, init_auth, login_required
init_auth(auth_manager) # an authority.AuthManager
@login_required
def me(request):
user = get_current_user(request)
return JsonResponse({"email": user["email"]})
urlpatterns = [path("me", me)]
Add authority.django.AuthorityBackend to AUTHENTICATION_BACKENDS to
authenticate Django users with authority credentials. Use
authority_user_to_django_user() to mirror authority users into
django.contrib.auth.models.User (same primary key, unusable password).
Starlette¶
Requires the starlette extra. StarletteAuth provides decorators and
current_user() for async endpoints.
from starlette.applications import Starlette
from starlette.routing import Route
from starlette.responses import JSONResponse
from authority.starlette import StarletteAuth
auth = StarletteAuth(async_manager) # an authority.AsyncAuthManager
@auth.require_role("admin")
async def admin(request):
return JSONResponse({"message": "Welcome, admin!"})
app = Starlette(routes=[Route("/admin", admin)])
Generic middleware¶
For bare ASGI/WSGI apps or frameworks without a dedicated helper, wrap the app
in the middleware. Auth state is stored at scope["authority"] /
environ["authority"].
# ASGI
from authority.asgi import AuthorityASGIMiddleware
app = AuthorityASGIMiddleware(inner_app, async_manager, auth_required=True)
# WSGI
from authority.wsgi import AuthorityWSGIMiddleware
app = AuthorityWSGIMiddleware(inner_app, manager, auth_required=True)
Helpers: get_user_state(), is_authenticated(),
user_id_from_scope() / user_id_from_environ(), get_current_user().
With auth_required=True unauthenticated requests receive a 401 JSON
response before reaching the application.
See the Examples for fully runnable applications for every integration.