Files
ThothII/docs/architecture/authentication.md
T

8.3 KiB

Authentication architecture

ThothII supports local and generic oidc through protected auth.yaml, plus the trusted-proxy upstream path used by Omics. The host operator surface is one CLI, tht; there is no separate authentication executable. The backend owns authorization in all paths and opaque browser sessions only in local/OIDC. tht owns protected local/OIDC configuration and local-user files; upstream identity is supplied per request by the authenticated server proxy.

Presentation is separate: full/embedded rendering does not select authentication. The Mac uses full/local; Omics uses embedded/upstream; a standalone server can use full/OIDC. Do not configure a second ThothII OIDC login simply because Omics itself authenticates users through Authentik.

flowchart TB
    BROWSER["Browser"] --> BOUNDARY["Authentication boundary"]
    BOUNDARY --> LOCAL["Local users\nArgon2id hashes"]
    BOUNDARY --> OIDC["OIDC provider\nAuthorization Code PKCE"]
    BOUNDARY --> UPSTREAM["Trusted proxy\nVerified portal session"]
    OIDC --> GROUPS["Groups claim\nexact mapping"]
    LOCAL --> PRINCIPAL["Thoth principal"]
    GROUPS --> PRINCIPAL
    UPSTREAM --> PRINCIPAL
    PRINCIPAL --> ROLES["Roles"]
    ROLES --> PERMISSIONS["Permissions"]
    PERMISSIONS --> ROUTES["Protected routes"]
    SECRETS["Mounted secret bundle"] -.-> BOUNDARY

Configuration and trust boundaries

The following protected-file configuration applies to local/OIDC. Upstream uses AUTH_MODE=upstream without a mounted auth.yaml or authentication runtime projection. The backend refuses both authorities together. AUTH_MODE=none and mock are development/test modes, not production fallbacks. The exact upstream setup, header contract, proxy hops and origin checks are in the server integration guide.

The installation descriptor points to an operator-controlled authentication directory. It contains non-secret auth.yaml and, for local mode, users.yaml. POSIX installations use a private directory and owner-only regular files; Windows uses equivalent owner-only ACLs. Secret values are read from the mounted secret bundle and are never placed in YAML, command arguments, logs, JSON diagnostics, or browser storage.

Local users have Argon2id password hashes, stable IDs, enabled state, roles, and authRevision. The production role expansion from backend/src/auth/config.ts is exact:

Role Permissions
user session.use
admin session.use, session.read_all, session.manage_all, settings.manage, workspace.manage, workspace.secrets.manage, database.manage, memory.manage, evidence.manage, pi.manage, auth.diagnostics.read

admin therefore includes the ordinary session.use permission. No other role or permission label is part of the production catalog.

After validating a browser session, the backend expands its roles through the current permission catalog on every request. The permissions saved at login are a historical snapshot, so existing administrator sessions can use newly deployed administration features without signing in again. Session expiry, revocation and local-user role validation still apply before role expansion.

OIDC is provider-neutral at the browser protocol boundary. Authorization Code + PKCE, issuer, signature, audience, expiry, state, and nonce are validated before a principal is created. Authentik is the first certified group-catalog adapter, not a special browser login mode.

Group authorization

This section describes ThothII's direct OIDC login, not the embedded Omics path. Omics checks its own capability and administrator status and supplies normalized identity headers; ThothII does not repeat the OIDC groups exchange.

OIDC must return a direct, non-empty groups claim whose value is a JSON array of strings. Missing, malformed, indirect, or overage-style claims fail closed. The browser callback returns HTTP 401 with the generic code oidc_callback_failed; it does not expose the internal reason. Authentication diagnostics and interactive device-flow validation use oidc_groups_claim_invalid for invalid group-claim/identity results. Exact configured external group names map to Thoth roles and then to permissions. A user with no mapped group is authenticated but receives no role and gets 403 from protected routes. Unmapped upstream groups are ignored silently, without an error or warning.

Every configured mapping is checked by the configured group-catalog adapter. Authentik checks the exact group name and reports oidc_mapped_group_missing when it cannot find it. The dedicated Authentik API service account has group-view-only privilege; it is separate from the OIDC client.

Diagnostics and ordering

The closed production diagnostic-code union is:

auth_ready
auth_config_incomplete
auth_config_invalid
auth_session_store_invalid
local_user_registry_invalid
local_admin_missing
oidc_secret_missing
oidc_discovery_unreachable
oidc_issuer_mismatch
oidc_jwks_unreachable
oidc_group_catalog_unreachable
oidc_group_catalog_unauthorized
oidc_mapped_group_missing
oidc_mapped_group_ambiguous
oidc_groups_claim_invalid
oidc_device_flow_unavailable

Workspace Validate performs static authentication validation without provider connectivity. tht auth check performs live, non-interactive diagnosis: static safety plus OIDC discovery, issuer/JWKS, group-catalog authentication, and exact configured-group existence. Adding --interactive runs that same live diagnosis and then validates a device-flow identity when the provider supports Device Authorization. Aggregate live workspace and authentication validation is available through the installation diagnostics.

The ordered tht doctor report is exactly: descriptor, files, docker, compose, configuration, authentication, services, core-http, frontend-http, workspace-registry, workflow, pi. Its authentication entry is the live non-interactive diagnosis against the healthy running core; later checks may be skipped when an earlier prerequisite fails.

Browser sessions

This section applies only to local and direct OIDC. Upstream reuses the portal's authenticated session at the proxy boundary, not a ThothII cookie; its /me response has session: null and csrfToken: null.

The browser receives only an opaque HttpOnly, SameSite=Lax cookie named thothii_session. State-changing cookie requests require the in-memory CSRF token, same-origin Origin, and Fetch Metadata checks when present. The frontend never stores bearer tokens or session secrets in Web Storage.

  • Ordinary local sessions: 2-hour idle and 12-hour absolute expiry; closing the browser removes the non-persistent cookie.
  • Remember me local sessions: 7-day idle and 30-day absolute expiry; they survive browser and ThothII restarts.
  • OIDC sessions: at most 8 hours and never beyond the validated ID-token expiry.

Local password, role, enabled-state, and logout-all changes increment authRevision and invalidate affected sessions. Authentication configuration revision changes invalidate all sessions after reload. Logout deletes the server record. Backup restore excludes active sessions and OIDC state, recreates empty private auth-state directories, and therefore forces reauthentication.

Full local/OIDC logout calls POST /auth/logout, revokes the server session and clears its cookie. It does not call the identity provider's global logout. If the provider still has an SSO session, the next OIDC login can complete without another password prompt. Embedded has no ThothII logout control: use the portal.

Access revalidation

The frontend treats /me as the access authority. In embedded it does not fetch /auth/config or offer local/OIDC login. On focus, pageshow, visibility return and event-stream reconnection it rechecks access. A 401/403 from this probe clears protected state; a 403 on one operation is not automatically an app-wide logout. Neither the DOM adapter nor the proxy's initial SSE check guarantees instantaneous revocation of streams already open in other tabs.

See the local guide, generic OIDC guide, Authentik guide, upstream integration, and manual acceptance matrix.