brixta-auth
Reusable authentication and organization-aware authorization for FastAPI.
Documented API: 0.1.2Python: >=3.11
brixta-auth is the backend security/authentication subsystem. It handles registration, password verification, organizations, memberships, roles, short-lived access JWTs, rotating refresh sessions, CSRF protection, logout, /me, and login rate limiting.
It does not render a login page. The frontend renders UI and calls this backend directly or through @brixtaorg/auth-client.
Architecture
Browser / frontend | | HTTP + cookies v FastAPI application | +-- brixta-auth | registration/login | Argon2 password verification | Ed25519 access JWT | refresh rotation | CSRF | roles | +-- brixta-core settings/database/middleware | v PostgreSQL / SQLite Valkey (optional shared rate limiting)
Install
Recommended:
python -m pip install
"brixta-core==0.1.2"
"brixta-auth[postgres,valkey]==0.1.2"
Without extras:
python -m pip install "brixta-auth==0.1.2"
Verify:
python - <<'PY' import brixta_core import brixta_auth print("brixta-core", brixta_core.version) print("brixta-auth", brixta_auth.version) PY
Public API
from brixta_auth import ( AuthService, AuthSettings, InMemoryFixedWindowRateLimiter, Membership, NoopRateLimiter, Organization, Principal, RedisFixedWindowRateLimiter, RefreshSession, Role, TokenService, User, ValkeyFixedWindowRateLimiter, build_principal_dependency, create_auth_router, require_roles, )
Important pieces:
API
Purpose
AuthSettings
Auth configuration
TokenService
Issues/verifies Ed25519 access JWTs
AuthService
Registration/login/refresh/logout business logic
create_auth_router()
Creates /auth/* FastAPI routes
build_principal_dependency()
Bearer JWT -> Principal
require_roles()
RBAC dependency
User
User ORM model
Organization
Organization/tenant ORM model
Membership
User-to-organization role membership
RefreshSession
Refresh-session ORM model
Role
owner, admin, engineer, viewer
InMemoryFixedWindowRateLimiter
Process-local limiter
ValkeyFixedWindowRateLimiter
Shared Valkey/Redis-compatible limiter
Generate Ed25519 keys
mkdir -p secrets
openssl genpkey
-algorithm Ed25519
-out secrets/auth_private.pem
openssl pkey
-in secrets/auth_private.pem
-pubout
-out secrets/auth_public.pem
Do not commit production private keys.
Configuration
AuthSettings reads BRIXTA_AUTH_* environment variables.
Important defaults:
BRIXTA_AUTH_ISSUER=brixta-foundation BRIXTA_AUTH_AUDIENCE=brixta-api BRIXTA_AUTH_PRIVATE_KEY_PATH=secrets/dev_private.pem BRIXTA_AUTH_PUBLIC_KEY_PATH=secrets/dev_public.pem BRIXTA_AUTH_ACCESS_TTL_SECONDS=600 BRIXTA_AUTH_REFRESH_TTL_DAYS=30 BRIXTA_AUTH_REFRESH_COOKIE_NAME=brixta_refresh BRIXTA_AUTH_CSRF_COOKIE_NAME=brixta_csrf BRIXTA_AUTH_COOKIE_SECURE=true BRIXTA_AUTH_COOKIE_SAMESITE=lax BRIXTA_AUTH_COOKIE_PATH=/api/v1/auth BRIXTA_AUTH_CSRF_COOKIE_PATH=/ BRIXTA_AUTH_PASSWORD_MIN_LENGTH=12 BRIXTA_AUTH_PASSWORD_MAX_LENGTH=128 BRIXTA_AUTH_LOGIN_LIMIT=10 BRIXTA_AUTH_LOGIN_WINDOW_SECONDS=60 BRIXTA_AUTH_RATE_LIMIT_BACKEND=memory
Local HTTP development often needs:
BRIXTA_DATABASE_URL=sqlite:///./app.db BRIXTA_AUTO_CREATE_TABLES=true
BRIXTA_AUTH_PRIVATE_KEY_PATH=secrets/auth_private.pem BRIXTA_AUTH_PUBLIC_KEY_PATH=secrets/auth_public.pem BRIXTA_AUTH_COOKIE_SECURE=false
Production example:
BRIXTA_DATABASE_URL=postgresql+psycopg://app:password@postgres:5432/app BRIXTA_CACHE_URL=redis://valkey:6379/0
BRIXTA_AUTH_PRIVATE_KEY_PATH=/run/secrets/auth_private.pem BRIXTA_AUTH_PUBLIC_KEY_PATH=/run/secrets/auth_public.pem BRIXTA_AUTH_COOKIE_SECURE=true BRIXTA_AUTH_COOKIE_SAMESITE=lax BRIXTA_AUTH_RATE_LIMIT_BACKEND=valkey
Critical cookie-path rule
The default refresh-cookie path is:
/api/v1/auth
If you mount the router somewhere else, change:
BRIXTA_AUTH_COOKIE_PATH=/your/real/auth/path
Otherwise refresh/logout cookies may not be sent to the correct endpoint.
Minimal backend integration
from collections.abc import Iterator from contextlib import asynccontextmanager from typing import Any
from fastapi import FastAPI from sqlalchemy.orm import Session
from brixta_core import ( Base, CoreSettings, RequestIDMiddleware, SecurityHeadersMiddleware, create_database, )
from brixta_auth import ( AuthService, AuthSettings, InMemoryFixedWindowRateLimiter, TokenService, ValkeyFixedWindowRateLimiter, create_auth_router, )
core_settings = CoreSettings() auth_settings = AuthSettings()
engine, SessionLocal = create_database(core_settings.database_url)
token_service = TokenService.from_settings(auth_settings) auth_service = AuthService( settings=auth_settings, token_service=token_service, )
def get_session() -> Iterator[Session]: with SessionLocal() as session: yield session
valkey_client: Any | None = None
if auth_settings.rate_limit_backend == "valkey": from redis import Redis valkey_client = Redis.from_url( core_settings.cache_url, decode_responses=True, ) limiter = ValkeyFixedWindowRateLimiter(valkey_client) else: limiter = InMemoryFixedWindowRateLimiter()
@asynccontextmanager async def lifespan(app: FastAPI): if core_settings.auto_create_tables: Base.metadata.create_all(engine)
if valkey_client is not None:
valkey_client.ping()
yield
if valkey_client is not None:
valkey_client.close()
engine.dispose()
app = FastAPI(lifespan=lifespan)
app.add_middleware(RequestIDMiddleware) app.add_middleware( SecurityHeadersMiddleware, hsts=core_settings.environment.casefold() == "production", )
app.include_router( create_auth_router( auth_service, auth_settings, session_dependency=get_session, rate_limiter=limiter, ), prefix="/api/v1", )
This creates:
POST /api/v1/auth/register POST /api/v1/auth/login POST /api/v1/auth/refresh POST /api/v1/auth/logout GET /api/v1/auth/me
Database models
brixta-auth defines:
User Organization Membership RefreshSession
User includes:
id email full_name password_hash is_active is_verified token_version created_at updated_at
Membership links a user to an organization and carries one role.
Roles in 0.1.2:
owner admin engineer viewer
RefreshSession stores a hash of the refresh token, not the plaintext refresh token.
Register
POST /api/v1/auth/register Content-Type: application/json
{ "email": "owner@example.com", "password": "correct-horse-battery-staple", "full_name": "Plant Owner", "organization_name": "Example Cement Plant" }
Registration creates:
User + Organization + Membership(role="owner")
Registration does not automatically log the user in.
Login
POST /api/v1/auth/login Content-Type: application/json
{ "email": "owner@example.com", "password": "correct-horse-battery-staple" }
Optional organization:
{ "email": "owner@example.com", "password": "correct-horse-battery-staple", "organization_id": "79a7789b-46ef-4e21-b257-9edcaed03f60" }
Response:
{ "access_token": "", "token_type": "bearer", "expires_in": 600 }
The backend also sets:
brixta_refresh HttpOnly refresh-token cookie brixta_csrf readable CSRF cookie
Access tokens
Access JWT claims include:
iss aud sub sid jti iat nbf exp typ ver roles org
They are signed with EdDSA/Ed25519.
Default lifetime:
600 seconds
Verification is normally stateless/local. A disabled account can therefore retain access until the current short access token expires unless the consuming application adds a stricter database-backed check for a high-risk endpoint.
Refresh
POST /api/v1/auth/refresh X-CSRF-Token: <brixta_csrf-cookie-value>
Browser cookies are also sent.
The backend:
checks CSRF hashes + locates refresh token validates session validates active user/membership rotates refresh session revokes previous token issues new access token sets new refresh + CSRF cookies
Reuse of an already-rotated refresh token revokes the token family.
Logout
POST /api/v1/auth/logout X-CSRF-Token: <brixta_csrf-cookie-value>
The backend revokes the refresh session and clears auth cookies.
Current principal
GET /api/v1/auth/me Authorization: Bearer
Response:
{ "user_id": "5f7e5b6a-...", "session_id": "b7f07817-...", "organization_id": "79a7789b-...", "roles": ["owner"] }
/me is the authenticated principal represented by token claims, not a complete editable user profile.
Protect custom FastAPI routes
from typing import Annotated
from fastapi import Depends from brixta_auth import Principal, build_principal_dependency
principal_required = build_principal_dependency(token_service)
@app.get("/api/v1/plants") def list_plants( principal: Annotated[Principal, Depends(principal_required)], ): return { "organization_id": str(principal.organization_id), "roles": principal.roles, }
Role-protected routes
from typing import Annotated
from fastapi import Depends from brixta_auth import Principal, require_roles
engineer_or_admin = require_roles( principal_required, "engineer", "admin", )
@app.post("/api/v1/kilns/{kiln_id}/setpoint") def setpoint( kiln_id: str, principal: Annotated[Principal, Depends(engineer_or_admin)], ): return { "kiln_id": kiln_id, "changed_by": str(principal.user_id), }
No allowed role -> HTTP 403.
Rate limiting
Development:
from brixta_auth import InMemoryFixedWindowRateLimiter limiter = InMemoryFixedWindowRateLimiter()
Production/shared:
from redis import Redis from brixta_auth import ValkeyFixedWindowRateLimiter
client = Redis.from_url( "redis://localhost:6379/0", decode_responses=True, )
limiter = ValkeyFixedWindowRateLimiter(client)
Browser integration
Recommended:
npm install "@brixtaorg/auth-client@0.1.2"
import { BrixtaAuthClient } from "@brixtaorg/auth-client";
const auth = new BrixtaAuthClient({ baseUrl: "/api/v1", });
await auth.login({ email, password }); const me = await auth.me();
The NPM client keeps the short-lived access token in browser memory.
The Python backend remains responsible for:
password verification JWT signing JWT verification refresh-session persistence refresh rotation CSRF enforcement role claims / auth endpoint behavior
The refresh token is a server-issued HttpOnly cookie and is not readable by JavaScript.
Cross-origin frontend/API
The NPM client uses:
credentials: include
For a cross-origin frontend, configure credentialed CORS in the consuming FastAPI app:
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware( CORSMiddleware, allow_origins=["https://app.example.com"], allow_credentials=True, allow_methods=[""], allow_headers=[""], )
Also configure cookie Secure, SameSite, domain and path correctly for your deployment.
Production checklist
use proper DB migrations
keep Ed25519 private key backend-only
HTTPS
BRIXTA_AUTH_COOKIE_SECURE=true
explicit CORS origins for credentialed browser apps
correct cookie path/domain
Valkey/shared limiter for multi-process/multi-instance production
do not persist browser access tokens in localStorage
do not expose plaintext refresh tokens
test role/tenant authorization boundaries
Not provided in 0.1.2
Do not assume these exist:
password reset / forgot password complete email verification workflow MFA/TOTP OAuth/social login SAML/SSO organization invitations organization-admin REST endpoints profile-editing endpoints frontend UI components
There is an is_verified model field, but no complete email-verification delivery workflow in 0.1.2.
LLM IMPLEMENTATION CONTRACT
Copy this into an LLM prompt:
Use the installed brixta-auth package. Do not recreate its authentication internals.
Packages: brixta-core brixta-auth
Prefer these public APIs: AuthSettings AuthService TokenService create_auth_router build_principal_dependency require_roles Principal Role User Organization Membership RefreshSession InMemoryFixedWindowRateLimiter ValkeyFixedWindowRateLimiter
Rules:
- Create DB engine/session via brixta_core.create_database().
- Create AuthSettings.
- Create TokenService.from_settings(auth_settings).
- Create AuthService(settings=auth_settings, token_service=token_service).
- Supply a SQLAlchemy Session dependency.
- Mount create_auth_router under /api/v1 unless instructed otherwise.
- If the auth route prefix changes, make BRIXTA_AUTH_COOKIE_PATH match the real auth path.
- Use build_principal_dependency(token_service) for authenticated domain endpoints.
- Use require_roles(...) for RBAC.
- Do not create another password/JWT/refresh/CSRF implementation.
- Keep the Ed25519 private key backend-only.
- Do not store plaintext refresh tokens in the database.
- In production use migrations, HTTPS, Secure cookies and explicit CORS.
- In multi-instance production prefer Valkey rate limiting.
- Do not invent password reset, MFA, OAuth, invitation or admin APIs that are not implemented.
- For browser apps prefer @brixtaorg/auth-client.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file brixta_auth-0.1.3.tar.gz.
File metadata
- Download URL: brixta_auth-0.1.3.tar.gz
- Upload date:
- Size: 20.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
085186f579dd216f052676fbdfe3a1b7edc9775eb32822cde0fe7eb5c31608fd
|
|
| MD5 |
5057b56c51dbed69cdc3e7edcad31880
|
|
| BLAKE2b-256 |
c92f612f4a83c66414c5d02196188fe2364d63392693ffd4e281840cc5de9362
|
Provenance
The following attestation bundles were made for brixta_auth-0.1.3.tar.gz:
Publisher:
release.yml on habibieebhy/brixtafoundation
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
brixta_auth-0.1.3.tar.gz -
Subject digest:
085186f579dd216f052676fbdfe3a1b7edc9775eb32822cde0fe7eb5c31608fd - Sigstore transparency entry: 2396695828
- Sigstore integration time:
-
Permalink:
habibieebhy/brixtafoundation@5f95555f83d68af33fbe03eaf24b4e721ceb4e83 -
Branch / Tag:
refs/tags/v0.1.3 - Owner: https://github.com/habibieebhy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5f95555f83d68af33fbe03eaf24b4e721ceb4e83 -
Trigger Event:
push
-
Statement type:
File details
Details for the file brixta_auth-0.1.3-py3-none-any.whl.
File metadata
- Download URL: brixta_auth-0.1.3-py3-none-any.whl
- Upload date:
- Size: 18.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7b5c0282261199b15038beb09d47b6c23bb8b9fd685538ff418f7b6b33f73b08
|
|
| MD5 |
c5b05376743b0b0b0929c37af539acdb
|
|
| BLAKE2b-256 |
7790f7e507e1b31a298a9ae6a9be303a73ddb2b885952789a366640a335910dd
|
Provenance
The following attestation bundles were made for brixta_auth-0.1.3-py3-none-any.whl:
Publisher:
release.yml on habibieebhy/brixtafoundation
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
brixta_auth-0.1.3-py3-none-any.whl -
Subject digest:
7b5c0282261199b15038beb09d47b6c23bb8b9fd685538ff418f7b6b33f73b08 - Sigstore transparency entry: 2396695881
- Sigstore integration time:
-
Permalink:
habibieebhy/brixtafoundation@5f95555f83d68af33fbe03eaf24b4e721ceb4e83 -
Branch / Tag:
refs/tags/v0.1.3 - Owner: https://github.com/habibieebhy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5f95555f83d68af33fbe03eaf24b4e721ceb4e83 -
Trigger Event:
push
-
Statement type: