docs(auth): address authentication guide review
This commit is contained in:
@@ -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`.
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
Executable
+94
@@ -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"
|
||||
Reference in New Issue
Block a user