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.
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,
Pi, workspace, secret-management, and diagnostic permissions.
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`, `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,
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
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
`oidc_groups_claim_invalid`. 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.
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:
```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
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
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
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
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
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
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
storage. Live checks then cover discovery, issuer/JWKS, provider access, and configured groups.
`tht doctor` runs the authentication check after configuration and before service/workspace checks.
Workspace validation includes static authentication readiness; workspace Test adds live OIDC and
group-catalog checks. Any authentication failure makes the workspace non-activatable.
`tht doctor` emits this exact ordered report: `descriptor`, `files`, `docker`, `compose`,
`configuration`, `authentication`, `services`, `core-http`, `frontend-http`,
`workspace-registry`, `workflow`, `pi`. Its authentication entry is live and non-interactive.
Any authentication failure makes Workspace Validate or Workspace Test non-activatable according
to that surface's static or live scope.
The redacted diagnostic codes include `oidc_secret_missing`, `oidc_discovery_unreachable`,
`oidc_issuer_mismatch`, `oidc_jwks_unreachable`, `oidc_groups_claim_invalid`,
`oidc_mapped_group_missing`, and `oidc_mapped_group_ambiguous`.
The complete closed diagnostic-code union and exact role-to-permission expansion are in the
[authentication architecture](../architecture/authentication.md).
+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`.
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.
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
tht auth check
@@ -20,8 +21,10 @@ generic OIDC; these steps configure the provider-specific group catalog only.
tht doctor --json
```
6. Run workspace Validate and then workspace Test. Test must prove discovery/JWKS, catalog access,
and every configured group. The diagnostic result must contain no secret values.
6. Run Workspace Test for aggregate live workspace and authentication validation. It must prove
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
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:
~~~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
+2 -2
View File
@@ -2,8 +2,8 @@
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
TOT Admin groups, then run tht auth check, tht auth check --interactive, workspace Validate,
and workspace Test in that order. Browser callback E2E, native Windows execution, approved PSD
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
manual identities, external L2, and the two parked restore-lock preconditions remain pending the
Task 15/release gates.
+49 -52
View File
@@ -1,37 +1,50 @@
# Put ThothII behind Caddy
This example assumes Caddy runs on the Linux host, ThothII `frontend` listens only on
`127.0.0.1:8080`, public DNS points to the host, and a separate authentication gateway validates
the user's real login/session. Replace the domain and auth-gateway address.
Choose exactly one authentication mode. Direct ThothII-managed OIDC and deprecated upstream
authentication are mutually exclusive proxy contracts; never combine their directives.
For ThothII-managed generic OIDC, preserve the configured public origin and the exact callback
`/api/auth/oidc/callback`; the browser and API remain one same-origin surface. Run
`tht auth check` and workspace Test after proxy changes.
## Direct ThothII-managed OIDC
## 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
proxies only to `frontend`; frontend then sends same-origin `/api` requests to private `core`.
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.
The public `/api/auth/oidc/login` and `/api/auth/oidc/callback` paths pass unchanged through the
same proxy as the rest of `/api`. The configured `publicUrl` must match the browser origin.
```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 {
# Remove untrusted browser claims before the auth subrequest.
request_header -X-Authenticated-User
request_header -X-Thoth-Principal-Issuer
request_header -X-Thoth-Principal-Subject
@@ -53,27 +66,23 @@ thoth.example.com {
}
reverse_proxy 127.0.0.1:8080 {
# Disable response batching so SSE reaches the browser immediately.
flush_interval -1
header_up Host {host}
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
headers. Keep Caddy's private keys and state outside the ThothII source/operator directories.
## Trust boundary
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
Keep the public firewall rule closed while validating. Confirm ThothII responds only on loopback,
format a review copy if desired, validate the active file, then reload through the service manager:
Keep the public firewall closed while validating:
```sh
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
```
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
Open the firewall only after all of these pass:
1. An unauthenticated HTTPS request is redirected to login or returns 401/403.
2. Supplying forged `X-Thoth-Principal-*`, `X-Thoth-Is-Admin`, or `X-Thoth-Trusted-*` request
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.
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
upstream mode, additionally verify the external gateway rejects unauthenticated traffic and only
its 2xx response can create trusted identity headers.
+59 -52
View File
@@ -1,44 +1,66 @@
# Put ThothII behind Nginx
This example assumes Nginx runs on the Linux host, ThothII `frontend` listens only on
`127.0.0.1:8080`, and a separate authentication gateway validates the user's real login/session.
Replace the documentation domain, certificate paths, and auth-gateway address.
Choose exactly one authentication mode. Direct ThothII-managed OIDC and deprecated upstream
authentication are mutually exclusive proxy contracts; never combine their locations or headers.
For ThothII-managed generic OIDC, preserve the configured public origin and the exact callback
`/api/auth/oidc/callback`; the browser and API remain one same-origin surface. Run
`tht auth check` and workspace Test after proxy changes.
## Direct ThothII-managed OIDC
## 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
to `frontend`; frontend then uses its private same-origin `/api` route to reach `core`. Never proxy
the public listener directly to core port 8787.
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:
The `location /` block below has a `proxy_pass` without a replacement URI, so public
`/api/auth/oidc/login` and `/api/auth/oidc/callback` are forwarded unchanged. The configured
`publicUrl` must match the browser origin.
```nginx
server {
listen 80;
server_name thoth.example.com;
server_name thoth.example.invalid;
return 301 https://$host$request_uri;
}
server {
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_key /etc/nginx/tls/thoth/privkey.pem;
@@ -51,8 +73,6 @@ server {
proxy_set_header Content-Length "";
proxy_set_header X-Original-URI $request_uri;
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-Thoth-Principal-Issuer "";
proxy_set_header X-Thoth-Principal-Subject "";
@@ -75,7 +95,6 @@ server {
auth_request_set $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-Thoth-Principal-Issuer "";
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-Display-Name $thoth_principal_display_name;
proxy_set_header X-Thoth-Trusted-Is-Admin $thoth_is_admin;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Host $host;
@@ -93,8 +111,6 @@ server {
proxy_set_header Connection "";
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
# SSE must reach the browser without response buffering or cache delay.
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
@@ -103,13 +119,15 @@ server {
}
```
Keep the certificate private key outside the ThothII tree. Do not log cookies, authorization
headers, auth response bodies, or trusted identity headers.
## Trust boundary
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
First keep the public firewall rule closed. Confirm ThothII responds only on loopback, validate the
full Nginx configuration, then reload without stopping existing connections:
Keep the public firewall closed while validating:
```sh
curl --fail http://127.0.0.1:8080/health
@@ -117,20 +135,9 @@ sudo nginx -t
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
Open the firewall only after all of these pass:
1. An unauthenticated HTTPS request is redirected to login or returns 401/403.
2. Supplying forged `X-Thoth-Principal-*`, `X-Thoth-Is-Admin`, or `X-Thoth-Trusted-*` request
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.
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
upstream mode, additionally verify the external gateway rejects unauthenticated traffic and only
its 2xx response can create trusted identity headers.
+3 -1
View File
@@ -3,7 +3,9 @@
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)
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.
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
`createWithDependenciesLockHeld` with an opaque installation-bound transaction capability or
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
available, then workspace Validate and workspace Test. Run `tht doctor --json` and confirm its
`authentication` check precedes service and workspace checks.
3. Confirm the exact direct `groups` claim for both identities and the mappings `TOT Users → user`
2. Run Workspace Validate first; it is the static authentication gate. Run `tht auth check` for
live non-interactive diagnosis, then `tht auth check --interactive` where Device Authorization
is available, then Workspace Test for aggregate live validation.
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.
## 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`. |
| Admin identity opens admin routes | Allowed according to the `admin` permission set. |
| Token omits `groups` | Authentication fails closed with `oidc_groups_claim_invalid`. |
| Token has malformed, indirect, or overage 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. |
| 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. |
| 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. |
| 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. |
| Password/role/enable revision changes | Affected local sessions are rejected and reauthentication is required. |
| CSRF or cross-origin mutation is attempted | Request is rejected. |
| 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. |
## Status at Task 14
-1
View File
@@ -48,7 +48,6 @@ markdown_extensions:
nav:
- Home: index.md
- Guida utente: guida-utente.md
- Autenticazione: architecture/authentication.md
- Accettazione autenticazione: testing/authentication-manual-acceptance.md
- Setup Policlinico San Donato: install/psd-workspace-setup.md
- ThothII (Documentazione Tecnica):
+107 -6
View File
@@ -1,7 +1,15 @@
#!/usr/bin/env bash
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=(
"$root/docs/architecture/authentication.md"
"$root/docs/install/authentication-local.md"
@@ -18,6 +26,7 @@ docs=(
"$root/docs/index.md"
"$root/README.md"
"$root/PROJECT_STATE.md"
"$root/mkdocs.yml"
)
for path in "${docs[@]}"; do
@@ -36,21 +45,113 @@ required=(
"THT_AUTHENTIK_API_TOKEN"
"Remember me"
"oidc_mapped_group_missing"
"oidc_callback_failed"
"session.read_all"
"workspace.secrets.manage"
"auth.diagnostics.read"
)
for term in "${required[@]}"; do
rg -Fq "$term" "$corpus" || { echo "auth docs smoke: missing required term: $term" >&2; exit 1; }
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
exit 1
fi
if rg -n -i 'password[[:space:]]*[:=][[:space:]]*(?!<|YOUR|REPLACE|CHANGE|FILE|PROMPT)' --pcre2 "$corpus"; then
echo "auth docs smoke: plaintext password example" >&2
if rg -n -i --pcre2 -- '--password(?!-file)\b(?:[[:space:]]+|=)\S+' "$corpus"; then
echo "auth docs smoke: plaintext password option" >&2
exit 1
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
echo "auth docs smoke: misleading warning claim for unmapped groups" >&2
if rg -n -i --pcre2 '(?:^|[,{[:space:]])password[[:space:]]*:[[:space:]]*\S+|"password"[[:space:]]*:[[:space:]]*(?:"[^"]+"|[^,}[:space:]]+)' "$corpus"; then
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
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"