Metadata-Version: 2.4
Name: brixta-auth
Version: 0.1.3
Summary: Fast, secure, reusable authentication for FastAPI applications
Author: BRIXTA
License: MIT
Project-URL: Homepage, https://github.com/habibieebhy/brixtafoundation
Project-URL: Repository, https://github.com/habibieebhy/brixtafoundation
Project-URL: Issues, https://github.com/habibieebhy/brixtafoundation/issues
Keywords: brixta,fastapi,cement,digital-twin
Classifier: Development Status :: 3 - Alpha
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Typing :: Typed
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: argon2-cffi<26,>=25
Requires-Dist: brixta-core<0.2,>=0.1.0
Requires-Dist: cryptography<47,>=45
Requires-Dist: email-validator<3,>=2.2
Requires-Dist: fastapi<1,>=0.116
Requires-Dist: pyjwt[crypto]<3,>=2.10
Requires-Dist: sqlalchemy<2.2,>=2.0
Provides-Extra: postgres
Requires-Dist: psycopg[binary]<4,>=3.2; extra == "postgres"
Provides-Extra: valkey
Requires-Dist: redis<7,>=6.2; extra == "valkey"
Dynamic: license-file

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": "<JWT>",
  "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 <access-token>

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:
1. Create DB engine/session via brixta_core.create_database().
2. Create AuthSettings.
3. Create TokenService.from_settings(auth_settings).
4. Create AuthService(settings=auth_settings, token_service=token_service).
5. Supply a SQLAlchemy Session dependency.
6. Mount create_auth_router under /api/v1 unless instructed otherwise.
7. If the auth route prefix changes, make BRIXTA_AUTH_COOKIE_PATH match the real auth path.
8. Use build_principal_dependency(token_service) for authenticated domain endpoints.
9. Use require_roles(...) for RBAC.
10. Do not create another password/JWT/refresh/CSRF implementation.
11. Keep the Ed25519 private key backend-only.
12. Do not store plaintext refresh tokens in the database.
13. In production use migrations, HTTPS, Secure cookies and explicit CORS.
14. In multi-instance production prefer Valkey rate limiting.
15. Do not invent password reset, MFA, OAuth, invitation or admin APIs that are not implemented.
16. For browser apps prefer @brixtaorg/auth-client.
