docs(auth): address authentication guide review

This commit is contained in:
2026-08-18 03:33:00 +02:00
parent f4f38717e1
commit 91925d64bf
12 changed files with 406 additions and 142 deletions
+52 -6
View File
@@ -14,8 +14,15 @@ read from the mounted secret bundle and are never placed in YAML, command argume
diagnostics, or browser storage. diagnostics, or browser storage.
Local users have Argon2id password hashes, stable IDs, enabled state, roles, and `authRevision`. Local users have Argon2id password hashes, stable IDs, enabled state, roles, and `authRevision`.
The stable roles are `user` and `admin`; `admin` inherits `user` and adds installation, session, The production role expansion from `backend/src/auth/config.ts` is exact:
Pi, workspace, secret-management, and diagnostic permissions.
| Role | Permissions |
|---|---|
| `user` | `session.use` |
| `admin` | `session.use`, `session.read_all`, `session.manage_all`, `settings.manage`, `workspace.manage`, `workspace.secrets.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.
OIDC is provider-neutral at the browser protocol boundary. Authorization Code + PKCE, issuer, 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. signature, audience, expiry, state, and nonce are validated before a principal is created.
@@ -24,15 +31,54 @@ Authentik is the first certified group-catalog adapter, not a special browser lo
## Group authorization ## Group authorization
OIDC must return a direct, non-empty `groups` claim whose value is a JSON array of strings. 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 with Missing, malformed, indirect, or overage-style claims fail closed. The browser callback returns
`oidc_groups_claim_invalid`. Exact configured external group names map to Thoth roles and then to HTTP 401 with the generic code `oidc_callback_failed`; it does not expose the internal reason.
permissions. A user with no mapped group is authenticated but receives no role and gets `403` from Authentication diagnostics and interactive device-flow validation use
protected routes. Unmapped upstream groups are ignored silently, without an error or warning. `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 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 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. 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:
```text
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. Workspace Test is the aggregate live workspace and
authentication validation.
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 ## Browser sessions
The browser receives only an opaque `HttpOnly`, `SameSite=Lax` cookie named `thothii_session`. The browser receives only an opaque `HttpOnly`, `SameSite=Lax` cookie named `thothii_session`.
+21 -10
View File
@@ -51,7 +51,10 @@ authorization:
The ID token must contain a direct, non-empty `groups` array of strings. ThothII does not follow The ID token must contain a direct, non-empty `groups` array of strings. ThothII does not follow
distributed claims, overage links, or indirect provider expansion. Missing or malformed claims fail distributed claims, overage links, or indirect provider expansion. Missing or malformed claims fail
closed with `oidc_groups_claim_invalid`. closed. A browser callback exposes only HTTP 401 `oidc_callback_failed`; it never reveals whether
the claim was missing, malformed, indirect, or overage-style. The redacted diagnostic and
interactive device-flow contract uses `oidc_groups_claim_invalid` for invalid group-claim or
device-flow identity results.
Configured group names are exact and case-sensitive. The union of matched mappings determines the Configured group names are exact and case-sensitive. The union of matched mappings determines the
Thoth roles. A token with no mapped group is authenticated but has no role and cannot use protected Thoth roles. A token with no mapped group is authenticated but has no role and cannot use protected
@@ -61,7 +64,16 @@ provider may authenticate while still failing installation readiness.
## Checks and diagnostics ## Checks and diagnostics
Run the static check first: The surfaces have distinct semantics and this order is recommended:
1. Workspace Validate performs static authentication validation without contacting the provider.
2. `tht auth check` performs live, non-interactive authentication diagnosis, including discovery,
issuer/JWKS, catalog credentials, and all configured mapped groups.
3. `tht auth check --interactive` repeats live diagnosis and additionally validates a device-flow
identity and its direct `groups` claim when Device Authorization is available.
4. Workspace Test performs aggregate live workspace and authentication validation.
The live CLI forms are:
```sh ```sh
tht auth check tht auth check
@@ -72,12 +84,11 @@ For a provider that advertises Device Authorization, `tht auth check --interacti
verification URI and one-time user code on the terminal, waits for completion, and validates a verification URI and one-time user code on the terminal, waits for completion, and validates a
real ID token including `groups`. It is an operator check, not a replacement for browser login. real ID token including `groups`. It is an operator check, not a replacement for browser login.
Static checks cover configuration, secret references, group mappings, file safety, and session `tht doctor` emits this exact ordered report: `descriptor`, `files`, `docker`, `compose`,
storage. Live checks then cover discovery, issuer/JWKS, provider access, and configured groups. `configuration`, `authentication`, `services`, `core-http`, `frontend-http`,
`tht doctor` runs the authentication check after configuration and before service/workspace checks. `workspace-registry`, `workflow`, `pi`. Its authentication entry is live and non-interactive.
Workspace validation includes static authentication readiness; workspace Test adds live OIDC and Any authentication failure makes Workspace Validate or Workspace Test non-activatable according
group-catalog checks. Any authentication failure makes the workspace non-activatable. to that surface's static or live scope.
The redacted diagnostic codes include `oidc_secret_missing`, `oidc_discovery_unreachable`, The complete closed diagnostic-code union and exact role-to-permission expansion are in the
`oidc_issuer_mismatch`, `oidc_jwks_unreachable`, `oidc_groups_claim_invalid`, [authentication architecture](../architecture/authentication.md).
`oidc_mapped_group_missing`, and `oidc_mapped_group_ambiguous`.
+6 -3
View File
@@ -12,7 +12,8 @@ generic OIDC; these steps configure the provider-specific group catalog only.
in the protected bundle under `THT_AUTHENTIK_API_TOKEN`. in the protected bundle under `THT_AUTHENTIK_API_TOKEN`.
4. Create or confirm the exact groups `TOT Users` and `TOT Admin`. Map them explicitly in 4. Create or confirm the exact groups `TOT Users` and `TOT Admin`. Map them explicitly in
`auth.yaml` to `user` and `admin`, respectively. Keep other upstream groups out of the mapping. `auth.yaml` to `user` and `admin`, respectively. Keep other upstream groups out of the mapping.
5. Run the static check and then the live interactive check: 5. Run Workspace Validate for static authentication validation. Then run live non-interactive
diagnosis, followed by the optional device-flow identity check:
```sh ```sh
tht auth check tht auth check
@@ -20,8 +21,10 @@ generic OIDC; these steps configure the provider-specific group catalog only.
tht doctor --json tht doctor --json
``` ```
6. Run workspace Validate and then workspace Test. Test must prove discovery/JWKS, catalog access, 6. Run Workspace Test for aggregate live workspace and authentication validation. It must prove
and every configured group. The diagnostic result must contain no secret values. discovery/JWKS, catalog access, and every configured group. The diagnostic result must contain
no secret values. `tht doctor --json` reports `authentication` after `configuration` and before
`services` in its exact ordered checklist.
Only configured exact group names are queried. Additional Authentik or directory groups are ignored Only configured exact group names are queried. Additional Authentik or directory groups are ignored
silently, without a warning. A mapped group absent from Authentik fails closed with silently, without a warning. A mapped group absent from Authentik fails closed with
+1 -1
View File
@@ -173,7 +173,7 @@ The canonical local Compose smoke uses the base file plus the local profile. Kee
base+profile command available for install verification: base+profile command available for install verification:
~~~sh ~~~sh
docker compose --env-file deploy/env/local.env + -f compose.yaml -f deploy/compose.local.yaml up --build -d docker compose --env-file deploy/env/local.env -f compose.yaml -f deploy/compose.local.yaml up --build -d
~~~ ~~~
After the stack is ready, configure and check authentication with the single host CLI tht; see After the stack is ready, configure and check authentication with the single host CLI tht; see
+2 -2
View File
@@ -2,8 +2,8 @@
Authentication acceptance is documented in the [manual authentication matrix](../testing/authentication-manual-acceptance.md). Authentication acceptance is documented in the [manual authentication matrix](../testing/authentication-manual-acceptance.md).
Use generic OIDC with Authentik as the certified group catalog, map only the exact TOT Users and Use generic OIDC with Authentik as the certified group catalog, map only the exact TOT Users and
TOT Admin groups, then run tht auth check, tht auth check --interactive, workspace Validate, TOT Admin groups, then run Workspace Validate, `tht auth check`, `tht auth check --interactive`,
and workspace Test in that order. Browser callback E2E, native Windows execution, approved PSD and Workspace Test in that order. Browser callback E2E, native Windows execution, approved PSD
manual identities, external L2, and the two parked restore-lock preconditions remain pending the manual identities, external L2, and the two parked restore-lock preconditions remain pending the
Task 15/release gates. Task 15/release gates.
+49 -52
View File
@@ -1,37 +1,50 @@
# Put ThothII behind Caddy # Put ThothII behind Caddy
This example assumes Caddy runs on the Linux host, ThothII `frontend` listens only on Choose exactly one authentication mode. Direct ThothII-managed OIDC and deprecated upstream
`127.0.0.1:8080`, public DNS points to the host, and a separate authentication gateway validates authentication are mutually exclusive proxy contracts; never combine their directives.
the user's real login/session. Replace the domain and auth-gateway address.
For ThothII-managed generic OIDC, preserve the configured public origin and the exact callback ## Direct ThothII-managed OIDC
`/api/auth/oidc/callback`; the browser and API remain one same-origin surface. Run
`tht auth check` and workspace Test after proxy changes.
## Trust boundary Use this mode when `auth.yaml` has `mode: oidc`. Caddy terminates TLS and proxies every request to
`frontend`; ThothII performs login, callback validation, session creation, and authorization.
Caddy must not apply `forward_auth` or another external authentication gateway.
Caddy is the only public listener. It provides automatic HTTPS, performs `forward_auth`, and The public `/api/auth/oidc/login` and `/api/auth/oidc/callback` paths pass unchanged through the
proxies only to `frontend`; frontend then sends same-origin `/api` requests to private `core`. same proxy as the rest of `/api`. The configured `publicUrl` must match the browser origin.
Never send the public reverse proxy to core port 8787.
The authentication gateway must return 2xx only after validating a real credential or session.
The ordered `route` below deletes every public/private Thoth identity request header before auth.
Only on auth success does `copy_headers` rename normalized response claims into the private
`X-Thoth-Trusted-*` headers consumed by frontend.
Forwarding identity headers alone does not authenticate a user.
Do not replace the authentication gateway with static `header_up` values, a network allowlist, or
browser-provided identity. The admin claim must come from reviewed identity-provider authorization.
## Example configuration
Save a reviewed site block in the Caddyfile. Caddy obtains and renews TLS certificates for the real
DNS name; use the organization's approved ACME issuer or certificate policy.
```caddyfile ```caddyfile
thoth.example.com { thoth.example.invalid {
reverse_proxy 127.0.0.1:8080 {
# No URI rewrite: OIDC login and callback paths reach frontend unchanged.
flush_interval -1
header_up Host {host}
header_up X-Forwarded-Proto https
header_up X-Forwarded-Host {host}
}
log {
output file /var/log/caddy/thoth-access.log
format json
}
}
```
After reload, run Workspace Validate for static validation, `tht auth check` for live,
non-interactive authentication diagnosis, and then Workspace Test for aggregate live validation.
## Deprecated upstream migration mode
Use this section only while the installation explicitly uses deprecated `upstream` mode. Do not
use it with `mode: oidc` or `mode: local`. Here an external authentication gateway owns login and
Caddy applies `forward_auth` before forwarding normalized private identity headers to `frontend`.
Forwarding identity headers alone does not authenticate a user. The authentication gateway returns
2xx only after validating its own credential or session. Clear browser-supplied public and trusted
headers before the subrequest, and map identity only from the successful auth response.
```caddyfile
thoth.example.invalid {
route { route {
# Remove untrusted browser claims before the auth subrequest.
request_header -X-Authenticated-User request_header -X-Authenticated-User
request_header -X-Thoth-Principal-Issuer request_header -X-Thoth-Principal-Issuer
request_header -X-Thoth-Principal-Subject request_header -X-Thoth-Principal-Subject
@@ -53,27 +66,23 @@ thoth.example.com {
} }
reverse_proxy 127.0.0.1:8080 { reverse_proxy 127.0.0.1:8080 {
# Disable response batching so SSE reaches the browser immediately.
flush_interval -1 flush_interval -1
header_up Host {host} header_up Host {host}
header_up X-Forwarded-Proto https header_up X-Forwarded-Proto https
} }
} }
log {
output file /var/log/caddy/thoth-access.log
format json
}
} }
``` ```
Configure log processing to remove cookies, authorization data, query strings, and identity ## Trust boundary
headers. Keep Caddy's private keys and state outside the ThothII source/operator directories.
Caddy is the only public listener and proxies only to loopback `frontend`, never directly to
`core`. Configure access logs to omit cookies, authorization data, query strings, and identity
headers. Keep Caddy keys and state outside ThothII source and operator directories.
## Validate and reload ## Validate and reload
Keep the public firewall rule closed while validating. Confirm ThothII responds only on loopback, Keep the public firewall closed while validating:
format a review copy if desired, validate the active file, then reload through the service manager:
```sh ```sh
curl --fail http://127.0.0.1:8080/health curl --fail http://127.0.0.1:8080/health
@@ -81,21 +90,9 @@ caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
sudo systemctl reload caddy sudo systemctl reload caddy
``` ```
Do not remove `forward_auth` if validation fails. Correct the Caddy version, adapter syntax,
authentication upstream, DNS, or certificate policy instead.
## Test authentication and SSE ## Test authentication and SSE
Open the firewall only after all of these pass: For direct OIDC, verify the login path redirects to the configured provider, the callback reaches
ThothII unchanged, forged identity headers grant nothing, and SSE is unbuffered. For deprecated
1. An unauthenticated HTTPS request is redirected to login or returns 401/403. upstream mode, additionally verify the external gateway rejects unauthenticated traffic and only
2. Supplying forged `X-Thoth-Principal-*`, `X-Thoth-Is-Admin`, or `X-Thoth-Trusted-*` request its 2xx response can create trusted identity headers.
headers does not grant access.
3. A real authenticated non-admin can use the application but cannot open Pi Management.
4. A real authenticated admin can use Pi Management.
5. A browser session receives live model updates without batching; a protected
`curl --no-buffer` request using a real short-lived login cookie is also acceptable. Delete the
cookie jar immediately afterward.
Re-run these checks after changing the identity provider, authentication gateway, Caddy, or
ThothII release.
+59 -52
View File
@@ -1,44 +1,66 @@
# Put ThothII behind Nginx # Put ThothII behind Nginx
This example assumes Nginx runs on the Linux host, ThothII `frontend` listens only on Choose exactly one authentication mode. Direct ThothII-managed OIDC and deprecated upstream
`127.0.0.1:8080`, and a separate authentication gateway validates the user's real login/session. authentication are mutually exclusive proxy contracts; never combine their locations or headers.
Replace the documentation domain, certificate paths, and auth-gateway address.
For ThothII-managed generic OIDC, preserve the configured public origin and the exact callback ## Direct ThothII-managed OIDC
`/api/auth/oidc/callback`; the browser and API remain one same-origin surface. Run
`tht auth check` and workspace Test after proxy changes.
## Trust boundary Use this mode when `auth.yaml` has `mode: oidc`. Nginx terminates TLS and proxies every request
to loopback `frontend`. ThothII owns OIDC login, callback validation, browser sessions, and
authorization. No external `auth_request` or authentication gateway belongs in this server.
Nginx is the only public listener. It terminates TLS, performs an `auth_request`, and proxies only The `location /` block below has a `proxy_pass` without a replacement URI, so public
to `frontend`; frontend then uses its private same-origin `/api` route to reach `core`. Never proxy `/api/auth/oidc/login` and `/api/auth/oidc/callback` are forwarded unchanged. The configured
the public listener directly to core port 8787. `publicUrl` must match the browser origin.
The authentication gateway must return 2xx only after validating a real credential or session. It
returns normalized `X-Thoth-Principal-*` and `X-Thoth-Is-Admin` response headers. Nginx clears all
client-supplied public and private-hop identity headers and copies only those successful auth
response values into `X-Thoth-Trusted-*` on the private hop to frontend.
Forwarding identity headers alone does not authenticate a user.
Do not substitute a static header, network allowlist, or client-provided header for the
authentication gateway. The admin value must come from reviewed identity-provider authorization,
not from a username supplied by the browser.
## Example configuration
Install an Nginx build that includes `ngx_http_auth_request_module`. Save a reviewed version of
this server block under the host's Nginx configuration directory:
```nginx ```nginx
server { server {
listen 80; listen 80;
server_name thoth.example.com; server_name thoth.example.invalid;
return 301 https://$host$request_uri; return 301 https://$host$request_uri;
} }
server { server {
listen 443 ssl; listen 443 ssl;
server_name thoth.example.com; server_name thoth.example.invalid;
ssl_certificate /etc/nginx/tls/thoth/fullchain.pem;
ssl_certificate_key /etc/nginx/tls/thoth/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
# No auth_request and no URI rewrite: ThothII receives OIDC paths unchanged.
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Connection "";
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
add_header X-Accel-Buffering no always;
}
}
```
After reload, run Workspace Validate for static validation, `tht auth check` for live,
non-interactive authentication diagnosis, and then Workspace Test for aggregate live validation.
## Deprecated upstream migration mode
Use this section only while the installation explicitly uses deprecated `upstream` mode. Do not
use it with `mode: oidc` or `mode: local`. In this mode an external authentication gateway owns
login, and Nginx applies `auth_request` before forwarding normalized private identity headers.
Forwarding identity headers alone does not authenticate a user. The authentication gateway returns
2xx only after validating its own credential or session.
```nginx
server {
listen 443 ssl;
server_name thoth.example.invalid;
ssl_certificate /etc/nginx/tls/thoth/fullchain.pem; ssl_certificate /etc/nginx/tls/thoth/fullchain.pem;
ssl_certificate_key /etc/nginx/tls/thoth/privkey.pem; ssl_certificate_key /etc/nginx/tls/thoth/privkey.pem;
@@ -51,8 +73,6 @@ server {
proxy_set_header Content-Length ""; proxy_set_header Content-Length "";
proxy_set_header X-Original-URI $request_uri; proxy_set_header X-Original-URI $request_uri;
proxy_set_header X-Original-Method $request_method; proxy_set_header X-Original-Method $request_method;
# The auth service derives identity from the real login/session, never these headers.
proxy_set_header X-Authenticated-User ""; proxy_set_header X-Authenticated-User "";
proxy_set_header X-Thoth-Principal-Issuer ""; proxy_set_header X-Thoth-Principal-Issuer "";
proxy_set_header X-Thoth-Principal-Subject ""; proxy_set_header X-Thoth-Principal-Subject "";
@@ -75,7 +95,6 @@ server {
auth_request_set $thoth_is_admin auth_request_set $thoth_is_admin
$upstream_http_x_thoth_is_admin; $upstream_http_x_thoth_is_admin;
# Discard browser claims. Carry only successful auth-subrequest values to frontend.
proxy_set_header X-Authenticated-User ""; proxy_set_header X-Authenticated-User "";
proxy_set_header X-Thoth-Principal-Issuer ""; proxy_set_header X-Thoth-Principal-Issuer "";
proxy_set_header X-Thoth-Principal-Subject ""; proxy_set_header X-Thoth-Principal-Subject "";
@@ -85,7 +104,6 @@ server {
proxy_set_header X-Thoth-Trusted-Principal-Subject $thoth_principal_subject; proxy_set_header X-Thoth-Trusted-Principal-Subject $thoth_principal_subject;
proxy_set_header X-Thoth-Trusted-Principal-Display-Name $thoth_principal_display_name; proxy_set_header X-Thoth-Trusted-Principal-Display-Name $thoth_principal_display_name;
proxy_set_header X-Thoth-Trusted-Is-Admin $thoth_is_admin; proxy_set_header X-Thoth-Trusted-Is-Admin $thoth_is_admin;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https; proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Host $host;
@@ -93,8 +111,6 @@ server {
proxy_set_header Connection ""; proxy_set_header Connection "";
proxy_pass http://127.0.0.1:8080; proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1; proxy_http_version 1.1;
# SSE must reach the browser without response buffering or cache delay.
proxy_buffering off; proxy_buffering off;
proxy_cache off; proxy_cache off;
proxy_read_timeout 3600s; proxy_read_timeout 3600s;
@@ -103,13 +119,15 @@ server {
} }
``` ```
Keep the certificate private key outside the ThothII tree. Do not log cookies, authorization ## Trust boundary
headers, auth response bodies, or trusted identity headers.
Nginx is the only public listener and proxies only to loopback `frontend`, never directly to
`core`. Keep private keys outside the ThothII tree. Do not log cookies, authorization headers,
OIDC callback query values, authentication bodies, or trusted identity headers.
## Validate and reload ## Validate and reload
First keep the public firewall rule closed. Confirm ThothII responds only on loopback, validate the Keep the public firewall closed while validating:
full Nginx configuration, then reload without stopping existing connections:
```sh ```sh
curl --fail http://127.0.0.1:8080/health curl --fail http://127.0.0.1:8080/health
@@ -117,20 +135,9 @@ sudo nginx -t
sudo systemctl reload nginx sudo systemctl reload nginx
``` ```
If `nginx -t` reports that `auth_request` is unknown, install the distribution package/module that
provides it. Do not remove authentication to make the syntax check pass.
## Test authentication and SSE ## Test authentication and SSE
Open the firewall only after all of these pass: For direct OIDC, verify login redirects to the configured provider, callback traffic reaches
ThothII unchanged, forged identity headers grant nothing, and SSE is unbuffered. For deprecated
1. An unauthenticated HTTPS request is redirected to login or returns 401/403. upstream mode, additionally verify the external gateway rejects unauthenticated traffic and only
2. Supplying forged `X-Thoth-Principal-*`, `X-Thoth-Is-Admin`, or `X-Thoth-Trusted-*` request its 2xx response can create trusted identity headers.
headers does not grant access.
3. A real authenticated non-admin can use the application but cannot open Pi Management.
4. A real authenticated admin can use Pi Management.
5. A browser session receives live model updates for longer than the default proxy timeout without
batching; a protected `curl --no-buffer` test using a real short-lived login cookie is also
acceptable. Delete the cookie jar immediately afterward.
Re-run these checks after changing the identity provider, auth gateway, Nginx, or ThothII release.
+3 -1
View File
@@ -3,7 +3,9 @@
Server authentication uses generic OIDC with the reverse proxy preserving the configured public Server authentication uses generic OIDC with the reverse proxy preserving the configured public
origin and callback path. Follow the [OIDC guide](authentication-oidc.md), [Authentik guide](authentik.md) origin and callback path. Follow the [OIDC guide](authentication-oidc.md), [Authentik guide](authentik.md)
when applicable, and the [authentication acceptance matrix](../testing/authentication-manual-acceptance.md). when applicable, and the [authentication acceptance matrix](../testing/authentication-manual-acceptance.md).
The host authentication CLI is tht; use tht auth check before workspace Validate/Test. The host authentication CLI is `tht`: Workspace Validate is static, `tht auth check` is live and
non-interactive, `--interactive` adds device-flow identity validation, and Workspace Test is the
aggregate live gate.
This guide is for an installer with basic Linux administration and very basic Docker knowledge. This guide is for an installer with basic Linux administration and very basic Docker knowledge.
It deploys the same Compose distribution used on a local PC: the mandatory application is exactly It deploys the same Compose distribution used on a local PC: the mandatory application is exactly
@@ -12,10 +12,13 @@ URLs, directory/LDAP details, tokens, passwords, hashes, cookies, or realistic s
that lock immediately before extraction; replace convention-only that lock immediately before extraction; replace convention-only
`createWithDependenciesLockHeld` with an opaque installation-bound transaction capability or `createWithDependenciesLockHeld` with an opaque installation-bound transaction capability or
closure so lock-held primitives cannot be called without the capability. closure so lock-held primitives cannot be called without the capability.
2. Run `tht auth check`, then `tht auth check --interactive` where Device Authorization is 2. Run Workspace Validate first; it is the static authentication gate. Run `tht auth check` for
available, then workspace Validate and workspace Test. Run `tht doctor --json` and confirm its live non-interactive diagnosis, then `tht auth check --interactive` where Device Authorization
`authentication` check precedes service and workspace checks. is available, then Workspace Test for aggregate live validation.
3. Confirm the exact direct `groups` claim for both identities and the mappings `TOT Users → user` 3. Run `tht doctor --json` and confirm this exact report order: `descriptor`, `files`, `docker`,
`compose`, `configuration`, `authentication`, `services`, `core-http`, `frontend-http`,
`workspace-registry`, `workflow`, `pi`.
4. Confirm the exact direct `groups` claim for both identities and the mappings `TOT Users → user`
and `TOT Admin → admin`. Confirm extra upstream groups are ignored without warning. and `TOT Admin → admin`. Confirm extra upstream groups are ignored without warning.
## Matrix ## Matrix
@@ -24,18 +27,19 @@ URLs, directory/LDAP details, tokens, passwords, hashes, cookies, or realistic s
|---|---| |---|---|
| Ordinary identity opens its own application/session routes | Allowed; admin-only routes return `403`. | | Ordinary identity opens its own application/session routes | Allowed; admin-only routes return `403`. |
| Admin identity opens admin routes | Allowed according to the `admin` permission set. | | Admin identity opens admin routes | Allowed according to the `admin` permission set. |
| Token omits `groups` | Authentication fails closed with `oidc_groups_claim_invalid`. | | Browser callback token omits `groups` | Callback returns HTTP 401 `oidc_callback_failed`; the internal reason is not exposed. |
| Token has malformed, indirect, or overage groups | Authentication fails closed with `oidc_groups_claim_invalid`. | | Browser callback token has malformed, indirect, or overage groups | Callback returns HTTP 401 `oidc_callback_failed`; the internal reason is not exposed. |
| Interactive diagnostic receives missing or invalid groups | Diagnostic fails with `oidc_groups_claim_invalid`. |
| Token has no mapped group | Principal has no role; protected routes return `403`; no warning is emitted. | | Token has no mapped group | Principal has no role; protected routes return `403`; no warning is emitted. |
| A configured group is absent from Authentik | Check fails with `oidc_mapped_group_missing`. | | A configured group is absent from Authentik | Check fails with `oidc_mapped_group_missing`. |
| Catalog token is wrong or lacks group-view-only access | Check fails redacted as catalog unavailable/unauthorized. | | Catalog token is wrong or lacks group-view-only access | Live check fails redacted with `oidc_group_catalog_unauthorized`. |
| Mapped group is renamed | The next check fails closed until configuration and provider agree. | | Mapped group is renamed | The next check fails closed until configuration and provider agree. |
| Token adds an unrelated group | Login and authorization are unchanged; no warning is emitted. | | Token adds an unrelated group | Login and authorization are unchanged; no warning is emitted. |
| Backend restarts with Remember me | Remembered local session survives within its TTL. | | Backend restarts with Remember me | Remembered local session survives within its TTL. |
| Password/role/enable revision changes | Affected local sessions are rejected and reauthentication is required. | | Password/role/enable revision changes | Affected local sessions are rejected and reauthentication is required. |
| CSRF or cross-origin mutation is attempted | Request is rejected. | | CSRF or cross-origin mutation is attempted | Request is rejected. |
| Logout | Cookie expires and the server session is deleted. | | Logout | Cookie expires and the server session is deleted. |
| Provider outage | Live check and OIDC login fail closed; no credential is exposed. | | Provider outage | Live check reports `oidc_discovery_unreachable`; browser login fails closed without exposing credentials. |
| Restore is completed | Sessions and OIDC state are absent; all users must reauthenticate. | | Restore is completed | Sessions and OIDC state are absent; all users must reauthenticate. |
## Status at Task 14 ## Status at Task 14
-1
View File
@@ -48,7 +48,6 @@ markdown_extensions:
nav: nav:
- Home: index.md - Home: index.md
- Guida utente: guida-utente.md - Guida utente: guida-utente.md
- Autenticazione: architecture/authentication.md
- Accettazione autenticazione: testing/authentication-manual-acceptance.md - Accettazione autenticazione: testing/authentication-manual-acceptance.md
- Setup Policlinico San Donato: install/psd-workspace-setup.md - Setup Policlinico San Donato: install/psd-workspace-setup.md
- ThothII (Documentazione Tecnica): - ThothII (Documentazione Tecnica):
+107 -6
View File
@@ -1,7 +1,15 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P) script_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)
root=$script_root
if [[ $# -gt 0 ]]; then
[[ $# -eq 2 && $1 == "--root" && -d $2 ]] || {
echo "usage: auth-docs-smoke.sh [--root DIRECTORY]" >&2
exit 2
}
root=$(cd "$2" && pwd -P)
fi
docs=( docs=(
"$root/docs/architecture/authentication.md" "$root/docs/architecture/authentication.md"
"$root/docs/install/authentication-local.md" "$root/docs/install/authentication-local.md"
@@ -18,6 +26,7 @@ docs=(
"$root/docs/index.md" "$root/docs/index.md"
"$root/README.md" "$root/README.md"
"$root/PROJECT_STATE.md" "$root/PROJECT_STATE.md"
"$root/mkdocs.yml"
) )
for path in "${docs[@]}"; do for path in "${docs[@]}"; do
@@ -36,21 +45,113 @@ required=(
"THT_AUTHENTIK_API_TOKEN" "THT_AUTHENTIK_API_TOKEN"
"Remember me" "Remember me"
"oidc_mapped_group_missing" "oidc_mapped_group_missing"
"oidc_callback_failed"
"session.read_all"
"workspace.secrets.manage"
"auth.diagnostics.read"
) )
for term in "${required[@]}"; do for term in "${required[@]}"; do
rg -Fq "$term" "$corpus" || { echo "auth docs smoke: missing required term: $term" >&2; exit 1; } rg -Fq "$term" "$corpus" || { echo "auth docs smoke: missing required term: $term" >&2; exit 1; }
done done
if rg -n -i 'thothii-admin|thothctl[[:space:]]+auth' "$corpus"; then canonical_compose='docker compose --env-file deploy/env/local.env -f compose.yaml -f deploy/compose.local.yaml up --build -d'
rg -Fxq "$canonical_compose" "$root/docs/install/local.md" || {
echo "auth docs smoke: missing canonical local Compose command" >&2
exit 1
}
if rg -n -F 'docker compose --env-file deploy/env/local.env +' "$corpus"; then
echo "auth docs smoke: noncanonical local Compose command" >&2
exit 1
fi
nav_count=$(awk 'index($0, "architecture/authentication.md") { count++ } END { print count + 0 }' "$root/mkdocs.yml")
[[ $nav_count == 1 ]] || {
echo "auth docs smoke: authentication navigation must appear exactly once" >&2
exit 1
}
python3 - "$root" <<'PY'
import pathlib
import re
import sys
root = pathlib.Path(sys.argv[1])
architecture = (root / "docs/architecture/authentication.md").read_text()
user_row = "| `user` | `session.use` |"
admin_row = (
"| `admin` | `session.use`, `session.read_all`, `session.manage_all`, `settings.manage`, "
"`workspace.manage`, `workspace.secrets.manage`, `pi.manage`, `auth.diagnostics.read` |"
)
if user_row not in architecture or admin_row not in architecture:
raise SystemExit("auth docs smoke: role-to-permission map is not exact")
diagnostic_heading = "## Diagnostics and ordering"
diagnostic_start = architecture.find(diagnostic_heading)
diagnostic_end = architecture.find("\n## ", diagnostic_start + len(diagnostic_heading))
diagnostic_section = architecture[diagnostic_start:diagnostic_end if diagnostic_end >= 0 else None]
match = re.search(r"```text\n([\s\S]*?)```", diagnostic_section)
expected_codes = [
"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",
]
actual_codes = [] if match is None else [line for line in match.group(1).splitlines() if line]
if actual_codes != expected_codes:
raise SystemExit("auth docs smoke: diagnostic code union is not exact")
for relative, language, forbidden, required in [
("docs/install/reverse-proxy-caddy.md", "caddyfile", "forward_auth", "forward_auth"),
("docs/install/reverse-proxy-nginx.md", "nginx", "auth_request", "auth_request"),
]:
source = (root / relative).read_text()
direct_start = source.find("## Direct ThothII-managed OIDC")
deprecated_start = source.find("## Deprecated upstream migration mode")
if direct_start < 0 or deprecated_start <= direct_start:
raise SystemExit(f"auth docs smoke: {relative} does not split direct and deprecated modes")
direct = source[direct_start:deprecated_start]
deprecated_end = source.find("\n## ", deprecated_start + 4)
deprecated = source[deprecated_start:deprecated_end if deprecated_end >= 0 else None]
blocks = re.findall(rf"```{language}\n([\s\S]*?)```", direct)
direct_code = "\n".join(blocks)
if "/api/auth/oidc/login" not in direct or "/api/auth/oidc/callback" not in direct:
raise SystemExit(f"auth docs smoke: {relative} omits unchanged public OIDC paths")
if re.search(rf"(?m)^\s*{forbidden}\b", direct_code):
raise SystemExit(f"auth docs smoke: {relative} applies external auth in direct OIDC mode")
deprecated_code = "\n".join(
re.findall(rf"```{language}\n([\s\S]*?)```", deprecated)
)
if not re.search(rf"(?m)^\s*{required}\b", deprecated_code):
raise SystemExit(f"auth docs smoke: {relative} omits scoped deprecated upstream auth")
PY
if rg -n -i --pcre2 '\bthothii-admin\b|\bthothctl\b[^\r\n]{0,256}\bauth\b' "$corpus"; then
echo "auth docs smoke: forbidden authentication CLI wording" >&2 echo "auth docs smoke: forbidden authentication CLI wording" >&2
exit 1 exit 1
fi fi
if rg -n -i 'password[[:space:]]*[:=][[:space:]]*(?!<|YOUR|REPLACE|CHANGE|FILE|PROMPT)' --pcre2 "$corpus"; then if rg -n -i --pcre2 -- '--password(?!-file)\b(?:[[:space:]]+|=)\S+' "$corpus"; then
echo "auth docs smoke: plaintext password example" >&2 echo "auth docs smoke: plaintext password option" >&2
exit 1 exit 1
fi fi
if rg -n -i 'unmapped[^.]{0,100}(generate|produce|emit|cause)[^.]{0,100}(warning|warn)|unmapped[^.]{0,100}warning(s)?[[:space:]]+(are|is)[[:space:]]+emitted' "$corpus"; then if rg -n -i --pcre2 '(?:^|[,{[:space:]])password[[:space:]]*:[[:space:]]*\S+|"password"[[:space:]]*:[[:space:]]*(?:"[^"]+"|[^,}[:space:]]+)' "$corpus"; then
echo "auth docs smoke: misleading warning claim for unmapped groups" >&2 echo "auth docs smoke: plaintext password field" >&2
exit 1
fi
if rg -n -i --pcre2 '(?:unmapped|additional|extra)[^.\r\n]{0,160}groups?[^.\r\n]{0,160}(?:generate|produce|emit|cause|trigger|raise|create|result)[^.\r\n]{0,160}(?:warnings?|alerts?|advisory|advisories|notices?|notifications?|noise)|(?:warnings?|alerts?|advisory|advisories|notices?|notifications?|noise)[^.\r\n]{0,160}(?:generate|produce|emit|cause|trigger|raise|create|result)[^.\r\n]{0,160}(?:unmapped|additional|extra)[^.\r\n]{0,160}groups?' "$corpus"; then
echo "auth docs smoke: misleading noise claim for unmapped groups" >&2
exit 1 exit 1
fi fi
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env bash
set -euo pipefail
root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)
tmp=$(mktemp -d "${TMPDIR:-/tmp}/thoth-auth-docs.XXXXXX")
trap 'rm -rf "$tmp"' EXIT
files=(
docs/architecture/authentication.md
docs/install/authentication-local.md
docs/install/authentication-oidc.md
docs/install/authentik.md
docs/testing/authentication-manual-acceptance.md
docs/architecture/overview.md
docs/install/local.md
docs/install/server.md
docs/install/psd-workspace-setup.md
docs/install/reverse-proxy-caddy.md
docs/install/reverse-proxy-nginx.md
docs/guida-utente.md
docs/index.md
README.md
PROJECT_STATE.md
mkdocs.yml
)
make_fixture() {
local name=${1:?fixture name required}
local fixture="$tmp/$name"
mkdir -p "$fixture"
for relative in "${files[@]}"; do
mkdir -p "$fixture/$(dirname "$relative")"
cp "$root/$relative" "$fixture/$relative"
done
printf '%s\n' "$fixture"
}
expect_rejected() {
local name=${1:?fixture name required}
local fixture_text=${2:?fixture text required}
local expected=${3:?expected error required}
local fixture output
fixture=$(make_fixture "$name")
printf '%s\n' "$fixture_text" >>"$fixture/README.md"
output="$tmp/$name.output"
if "$root/scripts/auth-docs-smoke.sh" --root "$fixture" >"$output" 2>&1; then
echo "auth docs fixture unexpectedly passed: $name" >&2
exit 1
fi
rg -Fq "$expected" "$output" || {
echo "auth docs fixture failed for the wrong reason: $name" >&2
sed -n '1,20p' "$output" >&2
exit 1
}
echo "auth docs negative fixture rejected: $name"
}
positive=$(make_fixture positive)
printf '%s\n' \
'Use --password-file <file>; never pass a password value.' \
'Additional unmapped groups are silently ignored without warnings, alerts, or advisories.' \
>>"$positive/README.md"
"$root/scripts/auth-docs-smoke.sh" --root "$positive" >/dev/null
echo "auth docs positive fixture passed"
expect_rejected thothctl-intervening \
'Run thothctl --installation <descriptor> --json auth check.' \
'forbidden authentication CLI wording'
expect_rejected alternate-admin \
'Run thothii-admin users list.' \
'forbidden authentication CLI wording'
expect_rejected password-option \
'Run tht auth user add demo --password example-value.' \
'plaintext password option'
expect_rejected yaml-password \
'password: example-value' \
'plaintext password field'
expect_rejected json-password \
'{"password": "example-value"}' \
'plaintext password field'
expect_rejected unmapped-warning \
'Unmapped OIDC groups generate warnings.' \
'misleading noise claim for unmapped groups'
expect_rejected unmapped-alert \
'Unmapped provider groups trigger operator alerts.' \
'misleading noise claim for unmapped groups'
expect_rejected unmapped-advisory \
'An advisory is emitted for every unmapped group.' \
'misleading noise claim for unmapped groups'
expect_rejected compose-plus \
'docker compose --env-file deploy/env/local.env + -f compose.yaml -f deploy/compose.local.yaml up --build -d' \
'noncanonical local Compose command'
echo "auth docs smoke fixture suite passed"