JWT authentication and authorization service for the OmniBioAI platform.
Central identity layer for the OmniBioAI zero-trust security plane. Handles user registration, login, token issuance, refresh, logout, and validation across all platform services.
omnibioai-auth runs as a containerized service inside the OmniBioAI Docker
Compose stack. All platform services — TES (workflow execution), Studio
(Electron UI), LIMS (data management), and Control Center — delegate token
validation to this service rather than implementing their own auth logic.
Studio / TES / LIMS / Control Center / SDK
|
POST /auth/validate
|
omnibioai-auth (:8001)
/ \
MySQL Redis
(users, tokens) (blacklist, pub/sub)
Redis also carries a policy:invalidate pub/sub channel. On logout,
omnibioai-auth publishes an invalidation event so downstream services can
flush any cached token state immediately.
| Method | Path | Description |
|---|---|---|
POST |
/auth/register |
Create a new user account |
POST |
/auth/login |
Authenticate and issue access + refresh tokens; sets the omnibioai_session cookie |
POST |
/auth/refresh |
Exchange a valid refresh token (body or omnibioai_session cookie) for a new access + refresh token pair |
POST |
/auth/logout |
Revoke refresh token; blacklist access token in Redis; clears the omnibioai_session cookie |
POST |
/auth/validate |
Validate a token and return user identity, roles, and org context |
GET |
/auth/{provider}/login |
Redirect to google/github/microsoft OAuth2.1+PKCE authorize URL |
GET/POST |
/auth/{provider}/callback |
Complete third-party OAuth login (browser-redirect and SPA-JSON variants) |
POST |
/auth/link/confirm |
Confirm linking an OAuth identity to an existing password account |
GET |
/auth/sso/discover?email= |
Domain-based lookup: does this email's org enforce Enterprise OIDC? |
GET |
/auth/sso/{org_slug}/login |
Redirect to the organization's configured Enterprise OIDC IdP |
GET/POST |
/auth/sso/{org_slug}/callback |
Complete an Enterprise OIDC login for that organization |
POST |
/oauth/token |
client_credentials grant — issues a service-identity access token |
GET/POST/DELETE |
/orgs/{org_id}/oauth-clients |
Manage an organization's client_credentials clients |
GET |
/.well-known/jwks.json |
RS256 public-key set (JWKS), for future RS256 verifiers |
GET |
/health |
Liveness check — returns {"status": "ok"} |
GET |
/metrics |
Prometheus metrics (jwt_auth_total counter) |
This is the core identity/session surface. The service also registers 17 more routers covering organization management, platform administration, MFA, and service credentials — grouped below rather than flattened into one table, since each group has its own permission model.
| Router | Prefix | Covers | Gate |
|---|---|---|---|
routes_orgs.py |
/orgs |
Create/list/get/update an org, invite a member, list/assign/revoke a member's org-scoped roles | manage_org (org-scoped) or platform_admin |
routes_teams.py |
/orgs/{org_id}/teams |
Create/list teams, manage team membership, delete a team | manage_teams (org-scoped) or platform_admin |
routes_organization_roles.py |
/organizations/{organization_id} |
Org-scoped custom role CRUD (/roles), org permission catalog (/permissions), member role assignment (/members/{user_id}/roles) — PR13's dynamic RBAC activation |
manage_org (org-scoped) or platform_admin |
Everything under /platform/* requires the manage_all_orgs permission
(the platform_admin role's defining grant) — this is the backend
omnibioai-control-center's Admin Console proxies to for its
Organizations/Users/Roles/Audit Logs pages.
| Router | Prefix | Covers |
|---|---|---|
routes_platform_admin.py |
/platform/orgs |
Cross-tenant org listing/detail |
routes_platform_users.py |
/platform/users |
Cross-tenant user listing/detail/update, remote MFA reset |
routes_platform_roles.py |
/platform/roles |
Platform-wide role catalog CRUD, user↔role assignment |
routes_platform_permissions.py |
/platform/permissions |
Permission Registry listing + usage stats |
routes_platform_audit.py |
/platform/audit-events |
Cross-tenant audit event stream |
routes_roles.py (/roles, /users/{user_id}/roles) — the original,
pre-PR13 global role CRUD API, gated by manage_roles. Distinct from
both the platform-wide catalog (/platform/roles, above) and org-scoped
custom roles (/organizations/{id}/roles, above); all three currently
coexist. app/rbac.py is what actually enforces a role's permissions at
request time regardless of which of the three surfaces created it.
| Router | Prefix | Covers | Gate |
|---|---|---|---|
routes_mfa.py |
/users/me/mfa |
TOTP enroll/verify, device list/remove, recovery-code generate/list/regenerate, login challenge | Authenticated self-service (own account only) |
routes_org_mfa.py |
/orgs/{org_id}/mfa-policy |
Per-org MFA requirement CRUD + platform-admin override | manage_sso (org-scoped) for the policy itself; override_mfa_policy for the override endpoints |
Full design trail: docs/pr11-mfa-database-foundation-discovery.md,
pr11-totp-enrollment-discovery.md, pr11-mfa-login-challenge-discovery.md,
pr11-mfa-org-policy-discovery.md, pr11-mfa-recovery-codes-discovery.md.
| Router | Prefix | Covers | Gate |
|---|---|---|---|
routes_apikeys.py |
/orgs/{org_id}/api-keys |
Create/list/revoke an org's API keys | manage_api_keys (org-scoped) or platform_admin |
routes_oauth_clients.py |
/orgs/{org_id}/oauth-clients |
Create/list/revoke an org's client_credentials OAuth clients |
manage_oauth_clients (org-scoped) or platform_admin |
routes_service_identity.py |
/service/me, /platform/services/{client_id} |
A service-identity token's own claims; platform-admin lookup of a registered service client | require_service_identity() / manage_all_orgs |
| Router | Prefix | Covers | Gate |
|---|---|---|---|
routes_config.py |
/auth/config (GET/PUT) |
Platform-wide global config (LLM/cloud credentials) — the exact endpoint Control Center's Admin Console Settings page reads | manage_config |
routes_license.py |
/license |
Validate/pull-token/generate/status/revoke | manage_licenses |
routes_identity.py |
/me, /platform/users/{user_id}/identity |
The caller's own resolved identity; platform-admin lookup of another user's | self / manage_all_orgs |
POST /auth/login
Content-Type: application/json
{"email": "user@example.com", "password": "..."}{
"access_token": "<jwt>",
"refresh_token": "<jwt>",
"token_type": "bearer"
}POST /auth/validate
Content-Type: application/json
{"token": "<jwt>"}{
"valid": true,
"user_id": 1,
"email": "user@example.com",
"roles": ["researcher"],
"permissions": ["workflow:run", "dataset:read"]
}omnibioai-auth is the ecosystem's single identity provider. Every other
service delegates authentication to it rather than implementing its own —
see Architecture Role above. It supports four ways
for a user to establish a session, all converging on the same signed-JWT
output:
POST /auth/login — authenticate_user verifies the password hash and
generate_tokens issues an access + refresh token pair (auth_method="password").
If the email's domain belongs to an organization that enforces Enterprise
OIDC (see below), password login is rejected outright with a 403 pointing
at that org's SSO login — credentials are never checked for an
SSO-enforced identity.
Three static, ecosystem-wide providers — Google, GitHub, Microsoft
(app/core/oauth_providers.py). GET /auth/{provider}/login redirects to the
provider's own authorize endpoint; /auth/{provider}/callback (both a
browser-redirect and an SPA-JSON variant) completes the exchange. A returning
identity already linked to an account signs straight in
(auth_method="oauth"); an unlinked identity whose email matches an
existing password account gets a short-lived confirmation token instead of
auto-linking (POST /auth/link/confirm); no match creates a new user.
Every OAuth and Enterprise OIDC authorize step uses PKCE (RFC 7636, S256
only) — code_verifier/code_challenge are generated per attempt, and the
verifier travels inside the signed, short-lived state JWT itself (not a
server-side session), so it survives a stateless redirect round-trip. On
this callback, the state JWT is signature/expiry/type/provider-checked
before the code exchange runs. PKCE here is defense-in-depth, not a load-bearing
boundary — this service is a confidential OAuth client holding its own
client_secret.
A per-organization identity provider, distinct from the three static OAuth
providers above. GET /auth/sso/discover?email= lets a client check —
without revealing whether the account exists — whether an email's domain
belongs to an org with SSO configured. GET /auth/sso/{org_slug}/login
redirects to that org's own IdP (PKCE + an OIDC nonce for replay
protection, carried the same way as the OAuth flow's state token); the
callback fully validates the returned id_token (signature via the IdP's
own JWKS, issuer, audience, expiry, nonce — RS256/ES256 only, an explicit
algorithm-confusion guard) before trusting any claim from it. A successful
login JIT-provisions org membership and issues auth_method="sso" with
idp_org_id set. When an org sets enforced=true on its SSO config,
password and generic OAuth login are both blocked for that org's domains —
Enterprise OIDC becomes the only way in.
Every issued token carries org_id/org_role alongside the base identity
claims, resolved fresh from the database on every login and every
refresh (build_user_claims, never replayed from a stale payload). A
token predating this claim set (schema v1) simply omits these fields
rather than erroring — every consumer treats them as optional. A separate,
narrower identity exists for machine-to-machine calls: POST /oauth/token
issues a client_credentials-grant token scoped to one org, carrying no
sub/email at all, gated by its own require_service_scope dependency
so it can never be mistaken for a user session.
| Token | Format | TTL | Storage | Revocation |
|---|---|---|---|---|
| Access token | JWT | 15 min | Not persisted (bearer, stateless) | JTI written to the Redis blacklist on logout, checked on every /auth/validate and every downstream verify_token() call |
| Refresh token | JWT | 7 days | MySQL (refresh_tokens table) |
Single-use — POST /auth/refresh rotates it: the presented token is revoked and a new one issued on every call. Re-presenting an already-rotated token is treated as token-family compromise and revokes the whole family |
Session cookie (omnibioai_session) |
Same value as the current refresh token | Matches refresh TTL | Browser-held, HttpOnly |
Cleared on /auth/logout; rotates in lockstep with the refresh token on every /auth/refresh |
Rotation — rotate_refresh_token never trusts the presented token's own
claims; it re-derives the user's current roles/org context from the
database and mints both a new access token and a new refresh token,
invalidating the one just used.
Revocation — two independent mechanisms, by design: refresh tokens are
revoked in MySQL (revoked_tokens), access tokens are blacklisted by
jti in Redis with a TTL matching their remaining lifetime (never longer
than 15 minutes of exposure after logout). A policy:invalidate Redis
pub/sub event is also published on logout so services with their own
short-lived caches (e.g. the API gateway's IAM client cache) can drop
stale entries immediately instead of waiting out their TTL.
Redis blacklist — the fast path every access-token verification checks
first (assert_token_usable, shared by /auth/validate and this service's
own get_current_user); downstream services run the identical check
locally against the same Redis instance via their own jwt_verify.py
(see omnibioai-control-center and
omnibioai-security-audit) rather than
calling back into this service on every request.
Logout — POST /auth/logout revokes the refresh token, blacklists the
access token's jti, publishes the invalidation event, and clears the
session cookie — all four in one call, fail-open on any individual step
(a Redis blip must never turn "log me out" into a stuck request).
Session cookies — see the dedicated section below.
HS256 is the production default (JWT_ALGORITHM=HS256, core/config.py) —
every token issued today is HS256, signed with the shared SECRET_KEY.
RS256 signing, verification, and a GET /.well-known/jwks.json endpoint
are fully implemented and available today, but not yet enabled — an
operator must explicitly set JWT_ALGORITHM=RS256 to switch issuance over:
- RS256 —
core/jwt.py::_signsigns with an RSA private key (core/rsa_keys.py) wheneverJWT_ALGORITHM=RS256, stamping akidheader on every token it mints. - JWKS endpoint —
GET /.well-known/jwks.jsonpublishes the public half of whatever key is configured (or, in dev/test with no key configured, an ephemeral process-local keypair — never safe for production), keyed bykid. kidsupport — the stable key identifier (a SHA-256 hash of the public key, not random) lets a verifier pick the right JWKS entry today and supports multi-key rotation in the future.- Dual verification —
core/jwt.py::decode_tokendispatches on each token's ownalgheader rather than on the currentJWT_ALGORITHMsetting, so HS256 tokens issued before a cutover keep validating for their full remaining lifetime after RS256 issuance is switched on. This service's own verification, and the localjwt_verify.pymodules in omnibioai-control-center and omnibioai-security-audit, all follow this same dispatch-by-header pattern (SSO Phase 2 PR15/PR16).
Production still defaults to HS256 until an operator deliberately flips
JWT_ALGORITHM=RS256 — RS256 readiness does not change any token issued
by a default deployment today. See the Deployment Notes
in the ecosystem root README for the rollout plan.
Alongside the existing JSON access_token/refresh_token response body
(unchanged, still returned on every login/refresh for API clients that
read it directly), /auth/login and /auth/refresh also set a
browser-oriented session cookie:
| Attribute | Value | Why |
|---|---|---|
| Name | omnibioai_session |
|
| Value | The current refresh token (same value the JSON body's refresh_token carries) |
No separate session store — the cookie IS the refresh token, in a form JavaScript never has to touch |
HttpOnly |
Yes | Not readable by page JavaScript — mitigates token theft via XSS |
Secure |
Yes | Never sent over plain HTTP |
SameSite |
Lax |
Sent on same-site top-level navigation and same-origin requests; blocks it from being attached to cross-site POSTs |
Domain |
.omnibioai.org (configurable via SESSION_COOKIE_DOMAIN) |
Shared across every first-party subdomain — webstudio., control., etc. — without any token ever passing through frontend JS |
How browsers refresh sessions — a browser holding only this cookie (no
token in localStorage/JS memory at all) can still call POST /auth/refresh
with an empty body: the endpoint falls back to the cookie when the request
body omits refresh_token, rotates it, and re-sets the cookie on the
response. Frontends that proxy this call through their own backend (e.g.
Control Center's routes_auth_proxy.py) must relay the Cookie request
header upstream and the Set-Cookie response header back downstream —
each hop is an independent HTTP request that doesn't forward cookies
automatically.
Browser
│
▼
Auth Service (omnibioai-auth)
│
├──▶ JWT access_token (15 min) + refresh_token (7 days),
│ returned in the JSON response body
│
├──▶ Refresh Cookie omnibioai_session (HttpOnly, Secure, SameSite=Lax,
│ Domain=.omnibioai.org) — same value as refresh_token
│
└──▶ JWKS GET /.well-known/jwks.json — RS256 public key set,
ready for downstream RS256 verification once
JWT_ALGORITHM=RS256 is enabled
Roles and permissions are DB-backed (fully dynamic, CRUD-managed — not a
fixed enum), embedded in the JWT payload at issuance, and enforced by
app/rbac.py via FastAPI dependency injection. Seeded roles:
| Role | Scope | Key permissions |
|---|---|---|
user |
Global | Default role on signup |
admin |
Global | manage_roles, manage_licenses, manage_config, override_sso_enforcement |
platform_admin |
Global, cross-tenant | manage_all_orgs — the only role that bypasses per-org isolation |
org_admin |
Per-organization | manage_org, manage_teams, manage_api_keys, manage_oauth_clients, manage_sso |
org_member |
Per-organization | Baseline organization membership |
Downstream services that can't reach the database to resolve a permission
from a role name (e.g. omnibioai-control-center, omnibioai-security-audit)
check for the specific seeded role name on the JWT instead (admin,
platform_admin) — see each service's own README.
The table above is the seeded baseline, not the whole story — PR13
("dynamic permission assignment and enterprise RBAC activation," main's
tip commit) activated org-scoped custom roles on top of it. Three separate
role-management APIs currently coexist (see
Roles & Permissions /
Organizations & Teams above for their endpoints):
/roles(routes_roles.py) — the original global role catalog,manage_roles-gated./platform/roles(routes_platform_roles.py) — the platform-wide catalog surfaced toomnibioai-control-center's Admin Console,manage_all_orgs-gated./organizations/{id}/roles(routes_organization_roles.py) — org-scoped custom roles anorg_admin(or platform admin) can define per-organization, on top of the seededorg_admin/org_memberbaseline.
app/rbac.py's enforcement doesn't care which of the three created a
role — a role's permissions array on the JWT is checked the same way
regardless of origin. assert_no_unregistered_permissions (app/main.py
startup, app/services/role_service.py) fails startup loud if any
Permission row in the database has drifted out of sync with
app/core/permission_names.py's registry — the single source of truth
for every permission name across all three surfaces. See
test_pr13_role_catalog_crud.py, test_pr13_role_org_scope.py, and
test_pr13_escalation_guards.py for the guarantees this activation adds
(an org admin cannot escalate a custom role beyond what the assigning
admin's own permissions already allow).
This service is not intended to run in isolation. Start it via the top-level
docker-compose.yml in the machine/ monorepo:
docker compose up omnibioai-authThe service depends on mysql and redis compose services being healthy
before it starts. An admin user is bootstrapped automatically on first startup.
| Variable | Default | Description |
|---|---|---|
DB_USER |
— | MySQL username |
DB_PASSWORD |
— | MySQL password |
DB_HOST |
localhost |
MySQL host (use mysql inside compose) |
DB_PORT |
3306 |
MySQL port |
DB_NAME |
omnibioai |
Database name |
SECRET_KEY |
— | HS256 JWT signing secret (required) |
JWT_ALGORITHM |
HS256 |
Signing algorithm for newly issued tokens. Verification always accepts both regardless of this setting — see JWT |
JWT_PRIVATE_KEY / JWT_PUBLIC_KEY |
(generated) | PEM-encoded RSA keypair for RS256 signing + JWKS. Unset in dev/test generates an ephemeral, process-local keypair — never safe for production |
ACCESS_TOKEN_EXPIRE_MINUTES |
15 |
Access token lifetime |
REFRESH_TOKEN_EXPIRE_DAYS |
7 |
Refresh token lifetime |
SESSION_COOKIE_DOMAIN |
.omnibioai.org |
Domain attribute on the omnibioai_session cookie |
GOOGLE_OAUTH_CLIENT_ID / _SECRET |
— | Google OAuth login; unset disables that provider (503) |
GITHUB_OAUTH_CLIENT_ID / _SECRET |
— | GitHub OAuth login; unset disables that provider (503) |
MICROSOFT_OAUTH_CLIENT_ID / _SECRET |
— | Microsoft OAuth login; unset disables that provider (503) |
OAUTH_REDIRECT_BASE_URL |
https://webstudio.omnibioai.org |
Must match each provider's registered redirect URI |
FRONTEND_BASE_URL |
https://webstudio.omnibioai.org |
Where the browser lands after an OAuth/SSO callback completes |
REQUIRE_HTTPS_FOR_SSO_ISSUER |
true |
Require HTTPS for an org's configured Enterprise OIDC issuer URL |
REDIS_URL |
redis://redis:6379 |
Redis connection URL (jti blacklist, policy:invalidate pub/sub) |
app/
├── main.py # FastAPI entrypoint — registers all 23 routers, admin
│ # bootstrap, Permission Registry drift check at startup,
│ # /metrics, /health
├── rbac.py # Role + permission dependency helpers (require_permission,
│ # require_org_permission_or_platform_admin, ...)
├── api/
│ ├── routes_auth.py # /auth/register, /login, /refresh, /logout, /validate
│ ├── routes_oauth.py # /auth/{provider}/login, /callback, /link/confirm
│ ├── routes_oauth_token.py # /oauth/token (client_credentials grant)
│ ├── routes_sso.py # /auth/sso/discover, /{org_slug}/login, /{org_slug}/callback
│ ├── routes_org_sso.py # /orgs/{org_id}/sso config CRUD + override
│ ├── routes_jwks.py # /.well-known/jwks.json
│ ├── routes_orgs.py # /orgs — org CRUD, invite, member roles
│ ├── routes_teams.py # /orgs/{org_id}/teams
│ ├── routes_organization_roles.py # /organizations/{id}/roles, /permissions, /members
│ ├── routes_roles.py # /roles — legacy global role catalog
│ ├── routes_mfa.py # /users/me/mfa — TOTP, devices, recovery codes, challenge
│ ├── routes_org_mfa.py # /orgs/{org_id}/mfa-policy
│ ├── routes_apikeys.py # /orgs/{org_id}/api-keys
│ ├── routes_oauth_clients.py # /orgs/{org_id}/oauth-clients
│ ├── routes_service_identity.py # /service/me, /platform/services/{client_id}
│ ├── routes_config.py # /auth/config
│ ├── routes_license.py # /license/*
│ ├── routes_identity.py # /me, /platform/users/{id}/identity
│ ├── routes_platform_admin.py # /platform/orgs
│ ├── routes_platform_users.py # /platform/users
│ ├── routes_platform_roles.py # /platform/roles
│ ├── routes_platform_permissions.py # /platform/permissions
│ ├── routes_platform_audit.py # /platform/audit-events
│ ├── deps.py # Shared FastAPI dependencies
│ └── services/
│ └── token_service.py # Shared token-issuance helper used by multiple routers
├── core/
│ ├── config.py # Settings from environment
│ ├── jwt.py # Token creation, decoding, dual HS256/RS256 dispatch
│ ├── security.py # Password hashing
│ ├── crypto.py # Low-level crypto helpers
│ ├── oauth_providers.py # Google/GitHub/Microsoft provider config
│ ├── permission_names.py # Permission Registry — single source of truth for every
│ │ # permission name (PR4)
│ ├── rsa_keys.py # RS256 keypair loading / ephemeral dev-key generation
│ └── token_revocation.py # Redis jti-blacklist + refresh-token-family revocation
├── db/
│ ├── models.py # User, RefreshToken, RevokedToken, Org, Role, ... models
│ ├── session.py # SQLAlchemy engine + session factory
│ ├── base.py # Declarative base
│ ├── init_admin.py # Admin user bootstrap
│ └── schema_guard.py # Startup schema-verification guard (main's latest
│ # commit — see "fix(auth): add schema guard and
│ # startup regression tests")
├── services/
│ ├── auth_service.py # authenticate, generate_tokens, revoke_token
│ ├── user_service.py # User CRUD helpers
│ ├── user_admin_service.py # Platform-admin user management
│ ├── service_tokens.py # Service-to-service token helpers
│ ├── service_identity.py # Service-identity resolution
│ ├── org_service.py # Organization CRUD
│ ├── team_service.py # Team CRUD/membership
│ ├── role_service.py # Role CRUD across all 3 role surfaces + registry drift check
│ ├── org_sso_service.py # Org SSO config CRUD
│ ├── org_oidc_service.py # Enterprise OIDC login flow
│ ├── sso_discovery_service.py # Domain → org SSO lookup
│ ├── mfa_service.py # TOTP/recovery-code logic
│ ├── apikey_service.py # API key issuance/revocation
│ ├── oauth_client_service.py # OAuth client_credentials client management
│ ├── oauth_service.py # OAuth 2.1/PKCE provider login flow
│ ├── platform_admin_service.py # Cross-tenant platform-admin queries
│ ├── audit_service.py # Audit event recording/query
│ ├── config_service.py # Global config get/set
│ ├── identity_service.py # Identity resolution
│ └── license_service.py # License validate/generate/status/revoke
└── schemas/
├── auth.py, user.py, orgs.py, teams.py, roles.py, organization_roles.py,
├── mfa.py, org_mfa.py, apikeys.py, oauth.py, oauth_client.py,
├── service_identity.py, config.py, license.py, identity.py,
└── permissions.py, platform_admin.py, role_admin.py, user_admin.py, org_sso.py
# Pydantic request/response models, one module per feature area above
pytest tests/701 tests across 52 files (pytest --collect-only). Core coverage —
auth flows (test_auth.py), RBAC (test_rbac.py), security primitives
(test_security.py), health check (test_health.py) — plus dedicated
suites per feature area:
| Area | Files |
|---|---|
| MFA | test_mfa.py, test_mfa_login_challenge.py, test_mfa_org_policy.py, test_mfa_recovery_codes.py |
| SSO / Enterprise OIDC | test_sso_login.py, test_sso_enforcement.py, test_org_sso.py, test_pkce.py |
| OAuth | test_oauth.py, test_oauth_clients.py, test_oauth_token.py |
| Organizations, teams, roles | test_orgs.py, test_teams.py, test_roles.py, test_organization_role_assignment_api.py |
| PR13 (dynamic RBAC activation) | test_pr13_role_catalog_crud.py, test_pr13_role_org_scope.py, test_pr13_escalation_guards.py, test_pr13_jwt_permission_merge.py |
| Platform administration | test_platform_admin.py, test_platform_admin_api.py, test_platform_users_api.py, test_platform_roles_api.py, test_platform_role_detail_api.py, test_platform_permissions_api.py |
| Audit | test_audit_ledger.py, test_pr11_identity_audit.py |
| JWT / tokens | test_jwt_iss_aud.py, test_jwt_org_context.py, test_rs256_jwks.py, test_refresh_rotation.py, test_token_revocation.py, test_session_cookie.py |
| API keys / service identity | test_apikeys.py, test_service_identity_api.py |
| License | test_license.py, test_legacy_license_import.py |
| Config | test_config.py |
| Authorization hardening | test_idor_org_scoping.py, test_permission_registry.py, test_registry_drift_detection.py, test_route_authorization_coverage.py |
| Schema / startup / migrations | test_schema_guard.py, test_startup_smoke.py, test_migrations.py, test_admin_bootstrap_schema_regression.py, test_backfill_default_org.py |
| Identity | test_identity_api.py |
| PR12 (enterprise auth flow) | test_pr12_auth_flow.py |
MFA design trail and other feature-specific audits live in docs/
(pr11-mfa-*.md); deployment and migration procedure in
docs/DEPLOYMENT_CHECKLIST.md and docs/MIGRATIONS.md.