57 KiB
ThothII Authentication Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add secure local authentication with remembered sessions and generic OIDC authentication with certified Authentik group validation, all administered through tht and included in workspace diagnostics.
Architecture: Fastify owns authentication, opaque file-backed browser sessions, CSRF protection, OIDC, and permission enforcement. The Go tht CLI owns protected local-user/configuration writes and delegates live provider checks to a container-local backend diagnostic command. React becomes a same-origin authenticated shell and never handles passwords beyond login submission or stores bearer/session tokens.
Tech Stack: Node 24.16.0, Fastify 5, TypeScript, React 18, openid-client 6.8.5, @fastify/cookie 11.1.2, @fastify/rate-limit 11.2.0, Go 1.26, golang.org/x/crypto/argon2 0.55.0, golang.org/x/term 0.45.0, Vitest, Playwright, Docker Compose.
Spec: docs/superpowers/specs/2026-08-16-thothii-authentication-design.md
Global Constraints
- The only host CLI is
tht; do not add another executable or revivethothctl. - Production modes are
localandoidc;noneandmockare development/test only, whileupstreamremains a deprecated migration adapter. - Authentik is the first-release certified OIDC provider; the browser OIDC protocol layer must not contain Authentik-specific login logic.
- The ID token must contain direct claim
groups: string[]; absent, malformed, indirect, or overage claims fail closed. - Only configured groups are checked and mapped. Unmapped provider groups are ignored silently and never produce a warning.
- Every configured group must be proven to exist through the configured group-catalog adapter; first release provides
authentik. - Local passwords use Argon2id v19 with
m=65536,t=3,p=1, 16-byte random salt, and 32-byte output. - A remembered local session has a seven-day idle timeout and thirty-day absolute timeout and survives browser/backend restarts.
- No raw session cookie, password, CSRF token, OIDC token, client secret, Authentik API token, or password hash may enter logs or diagnostic output.
- Browser authentication uses an opaque
HttpOnly,SameSite=Lax, path-scoped cookie;Secureis conditional on an HTTPS public URL so loopback HTTP remains functional. - Every cookie-authenticated state-changing route requires a CSRF token and same-origin browser checks.
- Authentication configuration is installation-global, but static checks appear in workspace validation and live checks appear in workspace connection tests.
- Workspace document content remains in its workspace language; application chrome and new authentication UI strings are English.
- JSON CLI stdout is pristine. Prompts, progress, and human guidance go to stderr.
- Node is exactly
24.16.0; Docker usessha256:40ad9f3064e67d6860b4bc3fe1880b2953934fd6320ada990e45fe0efa6badd7. - Existing session artifacts, workflow persistence, workspace ownership, and Pi RPC behavior must not change.
- Preserve unrelated user changes and the existing untracked
.playwright-cli/and.thothctl/paths.
Target file structure
New backend files are split by responsibility:
backend/src/auth/
types.ts roles, permissions, principal and diagnostic contracts
config.ts strict auth.yaml parser and canonical revision
authorization.ts permission expansion and route guards
local-registry.ts bounded/safe users.yaml reader and revision lookup
password.ts PHC parsing and Node Argon2id verification
session-store.ts opaque durable session and OIDC-state files
csrf.ts CSRF token and browser-origin enforcement
oidc-client.ts provider-neutral OIDC protocol adapter
group-catalog.ts catalog interface
authentik-group-catalog.ts Authentik read-only group existence adapter
diagnostics.ts shared static/live authentication diagnostics
routes.ts local/OIDC login, callback, logout, config and /me
diagnostic-command.ts machine/device-flow checker invoked by tht
New frontend files are:
frontend/src/auth/
AuthGate.tsx
LoginPage.tsx
authState.ts
frontend/src/api/auth.ts
New Go files are:
tools/tht/internal/authconfig/
types.go
password.go
store.go
users.go
commands.go
Focused tests use matching *.test.ts, *.test.tsx, and *_test.go files. Avoid adding auth
logic to backend/src/app.ts, frontend/src/shell/AppShell.tsx, or
tools/tht/cmd/tht/main.go beyond dependency wiring and command dispatch.
Task 1: Align the Runtime on Node 24 and Install Authentication Dependencies
Files:
- Modify:
docker/core.Dockerfile - Modify:
docker/frontend.Dockerfile - Modify:
docker/smoke/core-smoke.sh - Modify:
backend/package.json - Modify:
backend/package-lock.json - Modify:
frontend/package-lock.jsononly ifnpm installnormalizes lock metadata under Node 24 - Test:
backend/test/health.test.ts
Interfaces:
-
Produces: Node
24.16.0in backend build, frontend build, Pi build, and final core runtime. -
Produces: backend imports for
openid-client,@fastify/cookie, and@fastify/rate-limit. -
Preserves: the Pi package engine floor
>=22.19.0and all existing image/runtime contracts. -
Step 1: Pin the failing runtime expectation
Add an assertion to backend/test/health.test.ts that the build/runtime contract exposes major
version 24, and update docker/smoke/core-smoke.sh to reject Node 22/23. The accepted shell case is:
case "$node_version" in
v24.16.*) ;;
*) echo "Node 24.16 required, found $node_version" >&2; exit 1 ;;
esac
- Step 2: Run the focused checks and observe the old runtime failure
Run:
cd backend && npx vitest run test/health.test.ts
docker build --target backend-build -f docker/core.Dockerfile .
Expected: the source/runtime assertion or smoke inspection still reports Node 22.
- Step 3: Update all official Node stages and package metadata
Replace every Node stage in docker/core.Dockerfile and docker/frontend.Dockerfile with:
FROM node:24.16.0-bookworm@sha256:40ad9f3064e67d6860b4bc3fe1880b2953934fd6320ada990e45fe0efa6badd7
Update comments and smoke messages from Node 22 to Node 24.16. Then install exact backend versions:
cd backend
npm install --save-exact openid-client@6.8.5 @fastify/cookie@11.1.2 @fastify/rate-limit@11.2.0
npm install --save-dev --save-exact @types/node@24.13.3
- Step 4: Run the complete Node compatibility gate
Run:
cd backend && npx tsc --noEmit -p . && npx vitest run && npm run build
cd ../frontend && npx tsc -b && npx vitest run && npm run build
cd .. && docker build -f docker/core.Dockerfile -t thothii-core:auth-node24 .
docker run --rm --entrypoint /bin/sh thothii-core:auth-node24 -c 'node --version && /app/docker/smoke/core-smoke.sh'
Expected: Node reports v24.16.0; backend/frontend gates and the core smoke pass. Any Pi or native
dependency regression blocks this task and is fixed before proceeding.
- Step 5: Commit the runtime baseline
git add docker/core.Dockerfile docker/frontend.Dockerfile docker/smoke/core-smoke.sh \
backend/package.json backend/package-lock.json backend/test/health.test.ts frontend/package-lock.json
git commit -m "build: align authentication runtime on Node 24"
Task 2: Define and Validate Authentication Configuration
Files:
- Create:
backend/src/auth/types.ts - Create:
backend/src/auth/config.ts - Create:
backend/test/auth-config.test.ts - Modify:
backend/src/config.ts - Modify:
backend/test/config.test.ts
Interfaces:
- Produces:
export type AuthMode = "local" | "oidc" | "upstream" | "none" | "mock";
export type Role = "user" | "admin";
export type Permission =
| "session.use" | "session.read_all" | "session.manage_all"
| "settings.manage" | "workspace.manage" | "workspace.secrets.manage"
| "pi.manage" | "auth.diagnostics.read";
export interface LoadedAuthConfig {
value: AuthenticationConfig;
revision: string;
sourcePath: string;
}
export function loadAuthenticationConfig(path: string): LoadedAuthConfig;
export interface AuthenticationConfigProvider { current(): LoadedAuthConfig }
export function createAuthenticationConfigProvider(path: string): AuthenticationConfigProvider;
export function rolesToPermissions(roles: readonly Role[]): readonly Permission[];
-
Consumes: existing
yamlandzodbackend dependencies. -
Step 1: Write strict parser and permission tests first
Cover at least these fixtures in backend/test/auth-config.test.ts:
test.each([
["unknown root key", "unexpected"],
["relative users file", "../users.yaml"],
["OIDC without groups claim", undefined],
["OIDC without admin mapping", {}],
["HTTP non-loopback public URL", "http://thoth.example"],
])("rejects %s", (_label, mutation) => {
expect(() => loadAuthenticationConfig(writeFixture(mutation))).toThrow();
});
test("canonical group map order produces one stable revision", () => {
expect(loadAuthenticationConfig(first).revision).toBe(loadAuthenticationConfig(reordered).revision);
});
Also assert admin expands to every admin permission plus session.use, duplicate roles collapse,
and unknown roles fail parsing.
- Step 2: Run tests and verify missing-module failures
Run: cd backend && npx vitest run test/auth-config.test.ts test/config.test.ts
Expected: failure because the new parser/types and AppConfig fields do not exist.
- Step 3: Implement strict configuration types and canonical revision
Implement loadAuthenticationConfig() with bounded 1 MiB reads, yaml.parseDocument, explicit
duplicate-key rejection, z.strictObject, and a canonical SHA-256 revision over sorted JSON.
createAuthenticationConfigProvider() caches by inode/size/mtime and reloads after the CLI's atomic
replacement so mapping revisions invalidate sessions without a process restart. Add these fields
to AppConfig:
authMode: AuthMode;
authConfigFile: string;
authStateRoot: string;
authentication?: AuthenticationConfigProvider;
THT_AUTH_CONFIG_FILE defaults to /run/thothii-auth/auth.yaml and
THT_AUTH_STATE_ROOT defaults to /data/auth. A present config file is the sole source for
local or oidc; reject a simultaneous AUTH_MODE to prevent split-brain configuration. When the
file is absent, accept AUTH_MODE=none|mock|upstream only for development or migration. Public
exposure accepts config-derived oidc or deprecated upstream, never local, none, or mock.
- Step 4: Add complete local and OIDC fixture coverage
Assert the exact default lifetimes from the spec, loopback HTTP exception, the fixed references
THT_OIDC_CLIENT_SECRET and THT_AUTHENTIK_API_TOKEN, exact group-name matching, one admin
mapping, and no secret values in thrown messages.
- Step 5: Run focused and compile gates
Run:
cd backend
npx vitest run test/auth-config.test.ts test/config.test.ts
npx tsc --noEmit -p .
Expected: all tests pass and TypeScript is clean.
- Step 6: Commit the configuration contract
git add backend/src/auth/types.ts backend/src/auth/config.ts backend/src/config.ts \
backend/test/auth-config.test.ts backend/test/config.test.ts
git commit -m "feat(auth): define strict installation authentication config"
Task 3: Centralize Roles, Permissions, and Route Authorization
Files:
- Create:
backend/src/auth/authorization.ts - Create:
backend/test/authorization.test.ts - Modify:
backend/src/auth/principal.ts - Modify:
backend/src/auth/auth.ts - Modify:
backend/src/app.ts - Modify:
backend/src/routes/pi-management.ts - Modify:
backend/src/routes/settings.ts - Modify:
backend/src/routes/sessions.ts - Modify:
backend/src/routes/workspaces.ts - Modify:
backend/src/tht/tht-runner.ts - Modify: relevant
backend/test/routes-*.test.ts,backend/test/auth.test.ts, andbackend/test/tht-runner.test.ts
Interfaces:
- Produces:
export interface PrincipalContext {
issuer: string;
subject: string;
displayName?: string;
roles: readonly Role[];
permissions: readonly Permission[];
isAdmin: boolean;
}
export function hasPermission(principal: PrincipalContext, permission: Permission): boolean;
export function requirePermission(
request: FastifyRequest,
reply: FastifyReply,
permission: Permission,
): PrincipalContext | FastifyReply;
-
Preserves:
issuer + subjectownership andTHT_PRINCIPAL_IS_ADMINfor the harness transition. -
Step 1: Write a route authorization matrix that fails under scattered
isAdminchecks
In backend/test/authorization.test.ts, create principals for no role, user, and admin; assert
every permission in the spec. Add focused route cases:
expect(await requestAs(user, "PUT", "/settings", body)).toHaveStatus(403);
expect(await requestAs(admin, "PUT", "/settings", body)).toHaveStatus(200);
expect(await requestAs(user, "POST", "/pi-management/test")).toHaveStatus(403);
expect(await requestAs(admin, "POST", "/pi-management/test")).toHaveStatus(200);
Add session cases proving a user can mutate an owned session, cannot request scope=all, and an
admin can do both.
- Step 2: Run focused route tests and record the expected failures
Run:
cd backend
npx vitest run test/authorization.test.ts test/routes-settings.test.ts \
test/routes-pi-management.test.ts test/routes-sessions.test.ts test/routes-workspaces.test.ts
Expected: failures expose missing permissions and existing mode-specific authorization branches.
- Step 3: Implement one authorization guard and migrate every route
Implement requirePermission() to return one stable response:
{"code":"auth_forbidden","error":"This operation is not permitted"}
Remove managementAllowed() and route-local admin booleans. Apply the route table in the spec.
Keep object ownership inside the session locator/authorizer, with admin bypass based on
session.read_all or session.manage_all rather than isAdmin.
For compatibility adapters, derive roles as follows:
none -> admin only when publicExposure is false
mock -> user, or admin when the explicit mock-admin test header is true
upstream -> user plus admin when X-Thoth-Is-Admin is true
- Step 4: Preserve the harness principal environment contract
Continue exporting issuer, subject, display name, and derived admin status. Add a new trusted
comma-separated THT_PRINCIPAL_PERMISSIONS value containing only catalog values, and clear it in
clearPrincipalEnvironment().
- Step 5: Run all backend authorization and type gates
Run:
cd backend
npx vitest run test/auth.test.ts test/authorization.test.ts test/routes-pi-management.test.ts \
test/routes-settings.test.ts test/routes-sessions.test.ts test/routes-workspaces.test.ts \
test/tht-runner.test.ts
npx tsc --noEmit -p .
- Step 6: Commit the permission boundary
git add backend/src/auth backend/src/app.ts backend/src/routes backend/src/tht/tht-runner.ts backend/test
git commit -m "feat(auth): centralize ThothII permission enforcement"
Task 4: Build the Safe Go Authentication Store and Argon2id Registry
Files:
- Create:
tools/tht/internal/authconfig/types.go - Create:
tools/tht/internal/authconfig/password.go - Create:
tools/tht/internal/authconfig/store.go - Create:
tools/tht/internal/authconfig/users.go - Create:
tools/tht/internal/authconfig/password_test.go - Create:
tools/tht/internal/authconfig/store_test.go - Create:
tools/tht/internal/authconfig/users_test.go - Modify:
tools/tht/internal/safeio/files.go - Create:
tools/tht/internal/safeio/replace_unix.go - Create:
tools/tht/internal/safeio/replace_windows.go - Modify:
tools/tht/internal/safeio/files_unix_test.go - Modify:
tools/tht/internal/safeio/files_windows_test.go - Modify:
tools/tht/go.mod - Modify:
tools/tht/go.sum
Interfaces:
- Produces:
type Role string
const (RoleUser Role = "user"; RoleAdmin Role = "admin")
type User struct {
ID string `yaml:"id" json:"id"`
Username string `yaml:"username" json:"username"`
DisplayName string `yaml:"displayName,omitempty" json:"displayName,omitempty"`
PasswordHash string `yaml:"passwordHash" json:"-"`
Roles []Role `yaml:"roles" json:"roles"`
Enabled bool `yaml:"enabled" json:"enabled"`
AuthRevision uint64 `yaml:"authRevision" json:"authRevision"`
}
func HashPassword(password []byte, random io.Reader) (string, error)
func VerifyPassword(password []byte, encoded string) bool
func Load(directory string) (Config, Registry, error)
func MutateUsers(directory string, mutate func(*Registry) error) error
func ReplaceCanonicalRegular(path string, contents []byte, mode os.FileMode) error
-
Consumes:
golang.org/x/crypto/argon20.55.0 and existinggofrs/flock. -
Step 1: Write fixed Argon2 and unsafe-filesystem tests
Use one fixed salt and password to assert an exact PHC string. Cover wrong password, malformed PHC, oversized parameters, short/long passwords, symlinked directory/file, hard link, duplicate YAML key, unknown field, concurrent mutation, and last-admin refusal.
The shared vector file is backend/test/fixtures/argon2id-vectors.json with:
[{"password":"correct horse battery staple","saltHex":"000102030405060708090a0b0c0d0e0f","memoryKiB":65536,"passes":3,"parallelism":1,"keyLength":32,"phc":"$argon2id$v=19$m=65536,t=3,p=1$AAECAwQFBgcICQoLDA0ODw$DRo8ZSPI8G5OCvnFFapbVEjP69aDjy1Sw9i2743cPC4"}]
The committed literal is the cross-language oracle; both Go and Node independently derive it from the password and salt and compare it byte-for-byte.
- Step 2: Run Go tests and verify missing implementation failures
Run: cd tools/tht && go test ./internal/authconfig ./internal/safeio
- Step 3: Implement PHC parsing and bounded Argon2id hashing
Accept only the PHC grammar demonstrated by the committed vector, with decimal m, t, and p
fields and unpadded base64 salt/digest fields. Reject memory above 256 MiB, passes above ten,
parallelism above four, salt outside 16–64 bytes, and digest outside 16–64 bytes.
Use subtle.ConstantTimeCompare for verification.
- Step 4: Implement safe atomic replacement and locked mutations
Write a same-directory exclusive temporary file, set 0600, fsync file and directory, then replace
the regular single-link target. Unix uses rename; Windows uses
MoveFileEx(MOVEFILE_REPLACE_EXISTING|MOVEFILE_WRITE_THROUGH). Never follow a symlinked path
component. Hold <configDirectory>/.auth.lock for the complete read-check-write transaction.
- Step 5: Implement registry invariants
Require the spec's ASCII username grammar and use ASCII lowercase for lookup while preserving
display spelling. Reject control characters in Unicode display names. Generate UUIDv4 IDs with
crypto/rand. Increment authRevision on every security mutation and reject any result with no
enabled admin.
- Step 6: Run Go race and platform compilation gates
Run:
cd tools/tht
go test -race ./internal/authconfig ./internal/safeio
GOOS=windows GOARCH=amd64 go test -c ./internal/authconfig
GOOS=windows GOARCH=amd64 go test -c ./internal/safeio
Delete only the two generated test binaries after recording successful compilation.
- Step 7: Commit the safe local registry
git add tools/tht/internal/authconfig tools/tht/internal/safeio tools/tht/go.mod tools/tht/go.sum \
backend/test/fixtures/argon2id-vectors.json
git commit -m "feat(auth): add safe Argon2id local user registry"
Task 5: Add tht auth configure, User Management, and Status
Files:
- Create:
tools/tht/internal/authconfig/commands.go - Create:
tools/tht/internal/authconfig/commands_test.go - Modify:
tools/tht/internal/config/installation.go - Modify:
tools/tht/internal/config/installation_test.go - Modify:
tools/tht/internal/setup/files.go - Modify:
tools/tht/internal/setup/files_test.go - Modify:
tools/tht/cmd/tht/main.go - Modify:
tools/tht/cmd/tht/main_test.go - Modify:
tools/tht/go.mod - Modify:
tools/tht/go.sum
Interfaces:
- Produces all
tht authcommands in the spec except livecheck, which Task 12 wires to the backend diagnostic command. - Adds to
config.Installation:
Authentication struct { ConfigDirectory string }
func (i Installation) AuthenticationDirectory() string
func Run(ctx context.Context, installation config.Installation, args []string, stdin io.Reader, stdout, stderr io.Writer) int
-
Consumes:
golang.org/x/term0.45.0 for echo-free TTY password reads. -
Step 1: Write CLI grammar and pristine-JSON tests
Assert root help contains exactly one auth subtree and still rejects the retired CLI name. Test
local configure, OIDC configure, list JSON redaction, add/set-password/enable/disable/grant/revoke,
last-admin refusal, logout-all revision increment, non-TTY password refusal, and OIDC-mode refusal
for auth user.
The local non-interactive configure grammar is:
tht auth configure --mode local --public-url URL --admin-user USER \
[--admin-display-name NAME] --password-file FILE
TTY mode may omit the admin flags and prompts for them. Password-file reads are bounded to 1025 bytes and remove one trailing CRLF/LF only.
- Step 2: Run CLI tests and observe unknown-command failures
Run: cd tools/tht && go test ./cmd/tht ./internal/config ./internal/setup ./internal/authconfig
- Step 3: Extend the strict installation descriptor
Add:
authentication:
configDirectory: /absolute/operator-controlled/thothii-auth
Require an absolute canonical path and verify THT_AUTH_CONFIG_ROOT in the env file equals the
descriptor. setup and auth configure may create a missing final directory with private
permissions; start, doctor, status, user commands, and Compose operations require it to exist,
be private, and contain no symlinked component. Known-fields decoding must reject misspellings.
- Step 4: Implement command dispatch without growing
main.gobusiness logic
main.go parses the first-level auth verb and delegates to the exact authconfig.Run signature
above. All mutation logic stays in internal/authconfig. status --json returns mode, public URL,
user counts by role, and config revision; it never returns password hashes, secret refs' values, or
session data.
- Step 5: Implement protected bootstrap behavior
tht auth configure --mode local creates auth.yaml and users.yaml atomically with an initial
enabled admin. OIDC configuration writes the two exact group mappings supplied by
--user-group and --admin-group; if both names are equal, the command refuses the ambiguous
configuration.
- Step 6: Run CLI, race, and root-identity gates
Run:
cd tools/tht
go test -race ./internal/authconfig ./internal/config ./internal/setup ./cmd/tht
go build ./cmd/tht
./tht --help
Expected: only tht is named; JSON outputs parse with jq; no password/hash appears in captured
stdout/stderr.
- Step 7: Commit the host operator surface
git add tools/tht
git commit -m "feat(auth): add local and OIDC management to tht"
Task 6: Read Local Users and Verify Go-Generated Argon2 Hashes in Node
Files:
- Create:
backend/src/auth/password.ts - Create:
backend/src/auth/local-registry.ts - Create:
backend/test/auth-password.test.ts - Create:
backend/test/local-registry.test.ts - Consume:
backend/test/fixtures/argon2id-vectors.json
Interfaces:
- Produces:
export interface LocalUserRecord {
id: string;
username: string;
normalizedUsername: string;
displayName?: string;
passwordHash: string;
roles: readonly Role[];
enabled: boolean;
authRevision: number;
}
export interface LocalUserRegistry {
findByUsername(username: string): Promise<LocalUserRecord | undefined>;
findBySubject(id: string): Promise<LocalUserRecord | undefined>;
verify(user: LocalUserRecord | undefined, password: string): Promise<boolean>;
}
export function createLocalUserRegistry(usersPath: string): LocalUserRegistry;
-
Consumes: Node 24 native
crypto.argon2and the exact Go PHC format from Task 4. -
Step 1: Write cross-language vector and safe-file tests
Assert Node accepts every committed Go vector, rejects a one-byte password change, and refuses
oversized PHC parameters before allocating Argon2 memory. Registry tests cover known fields,
duplicate names/IDs, symlinks, hard links, mode wider than 0600, file larger than 1 MiB, and a
same-size atomic replacement with changed mtime.
- Step 2: Run focused tests and observe missing exports
Run: cd backend && npx vitest run test/auth-password.test.ts test/local-registry.test.ts
- Step 3: Implement Node PHC verification and dummy verification
Parse the same bounded PHC parameters as Go, derive exactly 32 bytes, and compare with
timingSafeEqual. Construct one process-local dummy hash at startup so unknown and disabled users
perform an indistinguishable Argon2 verification path.
- Step 4: Implement safe registry reload
Read only the configured users.yaml, reject unsafe metadata before/after read, parse strictly,
and cache by inode/size/mtime. Reload after an atomic host replacement. Error messages expose only
local_user_registry_invalid, not usernames, hashes, paths, or YAML content.
- Step 5: Run focused tests, typecheck, and Go/Node round trip
Run:
cd backend
npx vitest run test/auth-password.test.ts test/local-registry.test.ts
npx tsc --noEmit -p .
cd ../tools/tht && go test ./internal/authconfig
- Step 6: Commit the backend local identity reader
git add backend/src/auth/password.ts backend/src/auth/local-registry.ts \
backend/test/auth-password.test.ts backend/test/local-registry.test.ts
git commit -m "feat(auth): verify local ThothII users in the backend"
Task 7: Implement Durable Opaque Sessions and Revision Invalidation
Files:
- Create:
backend/src/auth/session-store.ts - Create:
backend/test/auth-session-store.test.ts - Modify:
backend/src/auth/types.ts
Interfaces:
- Produces:
export interface SessionCreateInput {
principal: PrincipalContext;
method: "local" | "oidc" | "upstream";
remembered: boolean;
userAuthRevision?: number;
authConfigRevision: string;
idleTtlMs: number;
absoluteTtlMs: number;
}
export interface CreatedAuthSession {
token: string;
csrfToken: string;
record: AuthSessionRecord;
}
export interface AuthSessionStore {
create(input: SessionCreateInput, now?: Date): Promise<CreatedAuthSession>;
resolve(token: string, now?: Date): Promise<AuthSessionRecord | undefined>;
touch(token: string, now?: Date): Promise<void>;
revoke(token: string): Promise<void>;
prune(now?: Date): Promise<number>;
}
export function createFileAuthSessionStore(root: string): AuthSessionStore;
export function deriveCsrfToken(sessionToken: string): string;
-
Produces: matching bounded OIDC-state create/consume methods with ten-minute expiry and single-use semantics.
-
Step 1: Write lifecycle, restart, and attack-path tests
Use two store instances against the same temporary directory to prove a remembered session survives a backend restart. Cover 256-bit token entropy/format, digest-only filenames, no raw token in file content, idle and absolute expiry, five-minute touch throttling, logout deletion, prune, single-use OIDC state, symlink/hard-link refusal, malformed/oversized records, and concurrent resolve/revoke.
- Step 2: Run the focused test and observe missing store failures
Run: cd backend && npx vitest run test/auth-session-store.test.ts
- Step 3: Implement one-file-per-session storage
Generate tokens with randomBytes(32).toString("base64url"); derive filenames with SHA-256 and
derive the frontend CSRF token with HKDF-SHA-256 using context thothii-csrf-v1. Persist neither
raw value. Create root/subdirectories as 0700 and files as 0600. Use exclusive create for new
records and same-directory write/fsync/rename for touches. Validate filename and record schema
before use.
- Step 4: Implement session validity hooks
Add a resolver callback that compares authConfigRevision on every request and, for local
sessions, looks up subject, enabled, and authRevision. Revoke on any mismatch before returning
a principal.
- Step 5: Run focused tests and a restart simulation
Run:
cd backend
npx vitest run test/auth-session-store.test.ts --repeat 3
npx tsc --noEmit -p .
Expected: repeated runs pass without timing flakes; the second store instance resolves the first instance's token.
- Step 6: Commit durable sessions
git add backend/src/auth/types.ts backend/src/auth/session-store.ts \
backend/test/auth-session-store.test.ts
git commit -m "feat(auth): persist opaque remembered sessions"
Task 8: Add Local Login, Cookies, Logout, Rate Limits, and CSRF
Files:
- Create:
backend/src/auth/csrf.ts - Create:
backend/src/auth/routes.ts - Create:
backend/test/auth-csrf.test.ts - Create:
backend/test/auth-routes-local.test.ts - Modify:
backend/src/auth/auth.ts - Modify:
backend/src/app.ts - Modify:
backend/src/server.ts - Modify:
backend/test/auth.test.ts - Modify: every state-changing route test that now needs a CSRF header
Interfaces:
- Produces public routes
GET /auth/config,POST /auth/local/login, OIDC route placeholders, authenticatedPOST /auth/logout, and authenticatedGET /me. - Produces:
export function registerAuthRoutes(app: FastifyInstance, deps: AuthRouteDependencies): void;
export function authenticateSession(deps: AuthDependencies): preHandlerHookHandler;
export function requireCsrf(request: FastifyRequest, reply: FastifyReply): true | FastifyReply;
-
Consumes: Task 6 local registry and Task 7 session store.
-
Step 1: Write end-to-end Fastify injection tests before route code
Cover successful ordinary and remembered login, generic failure for unknown/disabled/wrong
password, missing/wrong Origin, cookie attributes under HTTP and HTTPS public URLs, /me response,
logout, restart persistence, rate-limit response, concurrent Argon2 cap, and CSRF rejection for
every non-GET API family.
The remembered cookie must contain Max-Age=2592000; the ordinary cookie must not contain
Max-Age. Both contain HttpOnly, SameSite=Lax, and Path=/.
- Step 2: Run tests and observe missing route/plugin failures
Run: cd backend && npx vitest run test/auth-csrf.test.ts test/auth-routes-local.test.ts
- Step 3: Register cookie and bounded login protection
Register @fastify/cookie and @fastify/rate-limit before auth routes. Set conservative default
limits of ten failed login attempts per normalized username and twenty per source address per ten
minutes. Bound Argon2 verification to two concurrent jobs; excess requests return 429 without
queuing unbounded work.
- Step 4: Implement session authentication and CSRF once at the app boundary
Replace the current global authPreHandler with a hook that explicitly allows /health and
protocol endpoints, then resolves the opaque cookie for protected routes. For state-changing
methods compare X-ThothII-CSRF in constant time with deriveCsrfToken(cookieToken), require
Origin to equal the configured publicUrl origin, and require Sec-Fetch-Site: same-origin when
that header exists. Keep test helpers that generate a session+CSRF pair so individual route tests
do not bypass production hooks.
- Step 5: Return a frontend-safe
/merepresentation
Return only:
{
issuer, subject, displayName, roles, permissions, isAdmin,
csrfToken,
session: { method, remembered, idleExpiresAt, absoluteExpiresAt }
}
Never return cookie tokens, hashes, auth revisions, config revisions, or file paths.
- Step 6: Run backend auth, route, and build gates
Run:
cd backend
npx vitest run test/auth*.test.ts test/routes-*.test.ts test/sse-route.test.ts
npx tsc --noEmit -p .
npm run build
- Step 7: Commit the secure local web session
git add backend/src/auth backend/src/app.ts backend/src/server.ts backend/test
git commit -m "feat(auth): add local login and CSRF-protected sessions"
Task 9: Gate the React Application and Expose Remember Me
Files:
- Create:
frontend/src/api/auth.ts - Create:
frontend/src/auth/authState.ts - Create:
frontend/src/auth/AuthGate.tsx - Create:
frontend/src/auth/LoginPage.tsx - Create:
frontend/src/auth/AuthGate.test.tsx - Create:
frontend/src/auth/LoginPage.test.tsx - Modify:
frontend/src/api/client.ts - Modify:
frontend/src/api/sessions.ts - Modify:
frontend/src/api/types.ts - Modify:
frontend/src/App.tsx - Modify:
frontend/src/shell/AppShell.tsx - Modify:
frontend/src/stream/useSessionStream.ts - Modify:
frontend/src/api/client.test.ts - Modify: relevant
frontend/src/shell/*.test.tsx
Interfaces:
- Produces:
export interface AuthenticatedUser {
issuer: string;
subject: string;
displayName?: string;
roles: readonly ("user" | "admin")[];
permissions: readonly string[];
isAdmin: boolean;
csrfToken: string;
session: { method: "local" | "oidc" | "upstream"; remembered: boolean; idleExpiresAt: string; absoluteExpiresAt: string };
}
export function getAuthConfig(): Promise<AuthPublicConfig>;
export function loginLocal(username: string, password: string, remember: boolean): Promise<AuthenticatedUser>;
export function logout(): Promise<void>;
-
Consumes: same-origin
/apiand the backend CSRF contract. -
Step 1: Write shell-state and network-boundary tests
Test loading, local login form, invalid credentials, remembered checkbox, OIDC button, authenticated
shell, logout, expired-session 401, 403 presentation, and admin-only visibility. Assert neither
password nor any token is written to localStorage/sessionStorage.
- Step 2: Run focused frontend tests and observe missing UI failures
Run:
cd frontend
npx vitest run src/auth/AuthGate.test.tsx src/auth/LoginPage.test.tsx src/api/client.test.ts
- Step 3: Add an in-memory auth state and automatic CSRF header
apiFetch keeps default same-origin credentials and, for POST|PUT|PATCH|DELETE, reads the current
in-memory CSRF token and adds X-ThothII-CSRF. It never sends credentials to a cross-origin base
URL; extend the runtime URL policy to reject such a production configuration.
- Step 4: Build the authentication gate and login page
AuthGate calls /me; 200 renders AppShell, 401 renders LoginPage, and transient 503 renders a
retryable provider-unavailable state. The password input is uncontrolled beyond submission and is
cleared after every attempt. Remember me is unchecked by default and is shown only in local
mode.
- Step 5: Apply permission-aware chrome without relying on it for security
Hide Pi management and workspace mutation/test controls unless the relevant permission exists.
Hide scope=all unless session.read_all exists. Keep backend 403 handling because frontend
visibility is not authorization.
- Step 6: Preserve cookie-authenticated SSE
Keep EventSource same-origin under /api. On an authentication-generation change, close the old
EventSource and reset live session state before reconnecting. Do not add query-string tokens.
- Step 7: Run frontend unit, type, and build gates
cd frontend
npx vitest run
npx tsc -b
npm run build
- Step 8: Commit the authenticated frontend
git add frontend/src
git commit -m "feat(auth): add remembered local login to the frontend"
Task 10: Implement Provider-Neutral OIDC Authorization Code Flow
Files:
- Create:
backend/src/auth/oidc-client.ts - Create:
backend/test/oidc-client.test.ts - Create:
backend/test/auth-routes-oidc.test.ts - Modify:
backend/src/auth/routes.ts - Modify:
backend/src/auth/session-store.ts - Modify:
backend/src/app.ts
Interfaces:
- Produces:
export interface OidcIdentity {
issuer: string;
subject: string;
displayName?: string;
groups: readonly string[];
tokenExpiresAt: Date;
}
export interface OidcProtocol {
authorizationUrl(input: { state: string; nonce: string; codeVerifier: string }): Promise<URL>;
callback(input: { currentUrl: URL; state: string; nonce: string; codeVerifier: string }): Promise<OidcIdentity>;
diagnose(signal: AbortSignal): Promise<void>;
verifyDeviceFlow?(signal: AbortSignal, present: (uri: string, code: string) => void): Promise<OidcIdentity>;
}
-
Consumes:
openid-client6.8.5 and Task 7 single-use OIDC state storage. -
Step 1: Write protocol-validation and callback tests
Cover discovery issuer mismatch, missing HTTPS, state mismatch, replay, nonce mismatch, wrong
audience, expired token, invalid signature, missing sub, absent groups, non-array groups, empty
group item, distributed/overage groups, extra groups, unmapped groups, and a successful callback.
Tests inject a deterministic OidcProtocol; one concrete-adapter test supplies a local in-process
discovery/JWKS/token fixture through the library's custom fetch hook and never contacts the network.
- Step 2: Run focused tests and observe missing OIDC adapter failures
Run: cd backend && npx vitest run test/oidc-client.test.ts test/auth-routes-oidc.test.ts
- Step 3: Implement discovery and Authorization Code + PKCE
Use PKCE S256, random state, random nonce, exact configured callback, and exact issuer validation.
Store verifier/nonce/return target under the digest of state for ten minutes and consume it once.
Allow only the fixed return target /; do not accept arbitrary returnTo URLs.
- Step 4: Enforce the mandatory groups contract and map roles
Read groupsClaim from the verified ID-token claims. Require a direct array of unique non-empty
strings. Map exact strings through authorization.groupRoles, union roles, and ignore all other
strings without logging. A valid identity with no mapped role reaches the authenticated-but-
forbidden state.
- Step 5: Create an OIDC session without persisting tokens
Set absolute expiry to min(now + oidcTtlSeconds, ID-token exp). Persist only the derived principal
and authorization/config revision. Clear the state record after both successful and terminal
failed callbacks.
- Step 6: Run OIDC, auth-route, type, and build gates
cd backend
npx vitest run test/oidc-client.test.ts test/auth-routes-oidc.test.ts test/auth-session-store.test.ts
npx tsc --noEmit -p .
npm run build
- Step 7: Commit generic OIDC login
git add backend/src/auth backend/test/oidc-client.test.ts backend/test/auth-routes-oidc.test.ts
git commit -m "feat(auth): add generic OIDC login with mandatory groups"
Task 11: Add the Authentik Group Catalog and Shared Auth Diagnostics
Files:
- Create:
backend/src/auth/group-catalog.ts - Create:
backend/src/auth/authentik-group-catalog.ts - Create:
backend/src/auth/diagnostics.ts - Create:
backend/test/authentik-group-catalog.test.ts - Create:
backend/test/auth-diagnostics.test.ts - Modify:
backend/src/config/secret-bundle.ts - Modify:
backend/test/secret-bundle.test.ts
Interfaces:
- Produces:
export interface GroupCatalog {
verifyConfiguredGroups(names: readonly string[], signal: AbortSignal): Promise<readonly AuthDiagnostic[]>;
}
export interface AuthDiagnoser {
inspect(options: { live: boolean; interactive?: boolean; signal?: AbortSignal }): Promise<AuthDiagnostics>;
}
export function createAuthDiagnoser(deps: AuthDiagnoserDependencies): AuthDiagnoser;
-
Consumes fixed secret references
THT_OIDC_CLIENT_SECRETandTHT_AUTHENTIK_API_TOKENfrom the existing literal secret bundle parser. -
Step 1: Write exact Authentik request and comparison tests
Assert each mapped group produces one request with include_users=false&page_size=2, bearer auth,
five-second abort, redirect: "error", and encoded exact name. Test 0/1/2 results, pagination/body
overflow, 401/403, invalid JSON, redirect, timeout, and secret redaction.
Add the defining negative assertion:
expect(report.checks).not.toContainEqual(expect.objectContaining({ level: "warning" }));
expect(JSON.stringify(report)).not.toContain("Unmapped Corporate Group");
- Step 2: Run focused tests and observe missing adapter failures
Run: cd backend && npx vitest run test/authentik-group-catalog.test.ts test/auth-diagnostics.test.ts
- Step 3: Implement the least-privilege Authentik adapter
Use native fetch, a fixed operator-configured base origin, no redirects, bounded 1 MiB response,
and an AbortSignal. Return only stable codes and configured group names; discard upstream response
bodies and never enumerate unrelated groups.
- Step 4: Implement static and live diagnosis
Static mode validates configuration, file safety, secret presence, local enabled admin, role names,
group map, URL policy, and session root. Live OIDC mode additionally validates discovery/JWKS and
all Authentik mapped groups. Codes are exactly the closed AuthDiagnosticCode union in the spec;
auth_ready is the sole success code.
- Step 5: Verify redaction under every upstream failure
Seed tests with unique client-secret, API-token, cookie, password-hash, and file-path sentinels. Assert none appears in thrown errors, Fastify logs, human diagnostics, or JSON diagnostics.
- Step 6: Run focused and full auth gates
cd backend
npx vitest run test/auth*.test.ts test/oidc-client.test.ts test/secret-bundle.test.ts
npx tsc --noEmit -p .
- Step 7: Commit Authentik certification logic
git add backend/src/auth backend/src/config/secret-bundle.ts backend/test
git commit -m "feat(auth): validate mapped groups through Authentik"
Task 12: Integrate Auth Diagnostics with Workspaces, tht doctor, and tht auth check
Files:
- Create:
backend/src/auth/diagnostic-command.ts - Create:
backend/test/auth-diagnostic-command.test.ts - Modify:
backend/src/routes/workspaces.ts - Modify:
backend/test/routes-workspaces.test.ts - Modify:
backend/src/workspaces/diagnostics.ts - Modify:
frontend/src/api/workspaces.ts - Modify:
frontend/src/api/workspaces.test.ts - Modify:
frontend/src/shell/WorkspaceManager.tsx - Modify:
frontend/src/shell/WorkspaceManager.test.tsx - Modify:
tools/tht/internal/authconfig/commands.go - Modify:
tools/tht/internal/authconfig/commands_test.go - Modify:
tools/tht/internal/doctor/report.go - Modify:
tools/tht/internal/doctor/report_test.go - Modify:
tools/tht/cmd/tht/main_test.go
Interfaces:
- Extends workspace results with:
interface WorkspaceDiagnostics {
activatable: boolean;
diagnostics: Diagnostic[];
authentication: AuthDiagnostics;
}
-
Produces
tht auth check [--json|--interactive]and a doctor check namedauthentication. -
Step 1: Write workspace aggregation and CLI delegation tests
Assert /workspaces/validate uses inspect({live:false}), /workspaces/:id/test uses
inspect({live:true}), and activatable is false on auth failure even when workspace connectors
pass. Assert configured missing groups appear as errors and unrelated provider groups never appear.
In Go, assert exact Compose invocation and redaction for both stopped/running stacks. JSON stdout
must decode as the backend AuthDiagnostics contract with no banner.
- Step 2: Run focused backend/frontend/Go tests and observe failures
Run:
cd backend && npx vitest run test/routes-workspaces.test.ts test/auth-diagnostic-command.test.ts
cd ../frontend && npx vitest run src/api/workspaces.test.ts src/shell/WorkspaceManager.test.tsx
cd ../tools/tht && go test ./internal/authconfig ./internal/doctor ./cmd/tht
- Step 3: Add one backend diagnostic command
node dist/auth/diagnostic-command.js --json prints only the redacted report. --interactive
requires OIDC mode and the discovery document's device authorization endpoint, prints the
verification URI and user code to stderr, validates the resulting ID token and groups, and prints
one final success/failure report. No token is persisted.
- Step 4: Delegate host checks through Compose
tht auth check invokes a one-shot core command with the installation's normal mounts and secrets;
tht doctor uses exec -T when core is healthy. Add authentication after configuration and
before remote workspace/Pi checks. Sanitize both process output streams with all installation
secret values.
- Step 5: Aggregate workspace diagnostics without duplicating auth logic
Inject AuthDiagnoser into workspace routes. Static draft validation includes the static report;
installation test includes the live report. Preserve all existing connector diagnostic ordering
and messages, and compute overall activatable from both components.
- Step 6: Render authentication results in Workspace Manager
Show one Authentication section with Passed/Failed and configured-group errors. Do not render or
calculate a list of unmapped groups. Restrict Validate/Test actions to workspace.manage.
- Step 7: Run all diagnostic gates
cd backend && npx vitest run test/routes-workspaces.test.ts test/auth-diagnostic-command.test.ts
cd ../frontend && npx vitest run src/api/workspaces.test.ts src/shell/WorkspaceManager.test.tsx
cd ../tools/tht && go test -race ./internal/authconfig ./internal/doctor ./cmd/tht
- Step 8: Commit unified diagnostics
git add backend/src backend/test frontend/src tools/tht
git commit -m "feat(auth): include authentication in workspace and tht diagnostics"
Task 13: Wire Authentication into Compose, Setup, Backup, and Restore
Files:
- Modify:
compose.yaml - Modify:
deploy/compose.local.yaml - Modify:
deploy/compose.server.yaml - Modify:
deploy/compose.session-server.yaml.example - Modify:
deploy/env/local.env.example - Modify:
deploy/env/server.env.example - Modify:
deploy/psd/operator.env.example - Modify:
deploy/psd/thothii-installation.yaml.example - Modify:
docs/install/examples/thothii-installation.local.yaml - Modify:
docs/install/examples/thothii-installation.server.yaml - Modify:
deploy/secrets/thothii.secrets.example - Modify:
deploy/secrets/README.md - Modify:
tools/tht/internal/setup/files.go - Modify:
tools/tht/internal/setup/files_test.go - Modify:
tools/tht/internal/backup/create.go - Modify:
tools/tht/internal/backup/create_test.go - Modify:
tools/tht/internal/backup/restore.go - Modify:
tools/tht/internal/backup/restore_test.go - Modify:
tools/tht/internal/doctor/report.go - Modify:
tools/tht/internal/doctor/report_test.go - Modify:
scripts/unified-deployment-smoke.sh
Interfaces:
-
Produces mount
${THT_AUTH_CONFIG_ROOT}:/run/thothii-auth:roand volumeauth-state:/data/auth. -
Produces core env
THT_AUTH_CONFIG_FILE=/run/thothii-auth/auth.yamlandTHT_AUTH_STATE_ROOT=/data/auth. -
Preserves server whole-
/databind semantics. -
Step 1: Write Compose render and setup tests before YAML changes
Assert both profiles render the auth config directory read-only, core alone can access auth state,
the local profile declares auth-state, server uses ${THT_DATA_ROOT}/auth through its /data
bind, and workspace-maintenance receives neither user files nor auth state.
Update doctor volume expectations from seven to eight local persistent volumes.
- Step 2: Run render/setup/doctor tests and observe missing bindings
Run:
cd tools/tht && go test ./internal/setup ./internal/doctor ./internal/backup
Expected: tests fail until auth paths and the volume are declared. Compose rendering remains inside the existing fixture-safe setup/doctor tests so it never depends on an operator's uncommitted env or secret files.
- Step 3: Add mounts, volume, environment, and examples
Remove AUTH_MODE from the normal local and server profiles so auth.yaml is authoritative, and
keep an explicitly commented deprecated upstream migration example that is valid only when no
auth config exists. Add the two fixed secret-bundle keys:
THT_OIDC_CLIENT_SECRET=
THT_AUTHENTIK_API_TOKEN=
Never place example real-looking values in committed files.
- Step 4: Make setup configure authentication before startup
The ordered setup workflow becomes: create/validate installation files, configure local/OIDC auth, validate auth statically, render Compose, build/start, then run aggregate doctor. Non-interactive setup requires complete auth flags and password-file input for local mode.
- Step 5: Define backup and restore custody
Without --include-secrets, backup records the auth configuration path but excludes users.yaml
and secret values. With --include-secrets --yes, include auth.yaml and users.yaml under the
encrypted/custody-warning secret section. Never include active session or OIDC-state files.
Restore recreates /data/auth with private ownership and no active sessions, so every browser must
authenticate again.
- Step 6: Extend deployment smoke assertions
Add local bootstrap/login/remember/restart/logout checks and server static OIDC diagnostics with a
fake provider fixture. Assert workspace-maintenance cannot read /run/thothii-auth or /data/auth.
Assert final cleanup removes only test-scoped containers/volumes.
- Step 7: Run setup, backup, Compose, and smoke gates
cd tools/tht && go test -race ./internal/setup ./internal/config ./internal/backup ./internal/doctor ./cmd/tht
cd ../..
bash scripts/unified-deployment-smoke.sh
- Step 8: Commit deployment integration
git add compose.yaml deploy tools/tht scripts/unified-deployment-smoke.sh docs/install/examples
git commit -m "feat(auth): integrate authentication with installation lifecycle"
Task 14: Document Local Auth, Generic OIDC, Authentik, Groups, and PSD Acceptance
Files:
- Create:
docs/architecture/authentication.md - Create:
docs/install/authentication-local.md - Create:
docs/install/authentication-oidc.md - Create:
docs/install/authentik.md - Create:
docs/testing/authentication-manual-acceptance.md - Modify:
docs/architecture/overview.md - Modify:
docs/install/local.md - Modify:
docs/install/server.md - Modify:
docs/install/psd-workspace-setup.md - Modify:
docs/install/reverse-proxy-caddy.md - Modify:
docs/install/reverse-proxy-nginx.md - Modify:
docs/guida-utente.md - Modify:
docs/index.md - Modify:
README.md - Modify:
PROJECT_STATE.mdonly after automated and manual status is known - Modify:
mkdocs.ymlif navigation is explicit there
Interfaces:
-
Documents the exact YAML, CLI, group claim, group-role mapping, session lifetime, invalidation, diagnostic codes, Authentik service-account privileges, and PSD acceptance flow from the spec.
-
Step 1: Add a documentation contract test
Extend the existing docs smoke or add scripts/auth-docs-smoke.sh to require:
tht auth
groups
TOT Admin
THT_OIDC_CLIENT_SECRET
THT_AUTHENTIK_API_TOKEN
Remember me
oidc_mapped_group_missing
Also fail on thothii-admin, host-facing thothctl auth, plaintext-password examples, or wording
that claims unmapped OIDC groups generate warnings.
- Step 2: Run the docs smoke and observe missing-document failures
Run: bash scripts/auth-docs-smoke.sh
- Step 3: Write local and generic OIDC guides
Document bootstrap, initial admin, password recovery, remembered/ordinary expiry, logout-all,
restart behavior, all commands, JSON use, same-origin browser requirement, callback URL, mandatory
direct groups array, fail-closed unmapped-user behavior, and provider-adapter boundary.
- Step 4: Write the Authentik and PSD guide
Include exact operator steps: create OAuth2/OIDC application/provider, register callback, include
openid profile email, verify the groups claim, create dedicated service account/API token with
group-view permission only, create/confirm TOT Users and TOT Admin, map them in auth.yaml, run
tht auth check, run tht auth check --interactive, then run workspace Test. State explicitly
that additional Authentik/LDAP groups are ignored silently.
- Step 5: Write the manual acceptance matrix
Require one ordinary and one admin PSD test identity. Record expected results for ordinary/admin route access, missing claim, missing mapped group, wrong API token, group rename, extra unmapped group, session restart, password/role invalidation, CSRF rejection, logout, and provider outage. Never record real names, tokens, passwords, LDAP details, or internal URLs in committed evidence.
- Step 6: Run docs build and smoke
bash scripts/auth-docs-smoke.sh
python -m mkdocs build --strict
- Step 7: Commit operator and user documentation
git add docs README.md mkdocs.yml scripts/auth-docs-smoke.sh
git commit -m "docs(auth): document local OIDC and Authentik operation"
Task 15: Execute Full Automated and Manual Release Gates
Files:
- Create:
backend/test/fixtures/oidc-provider.mjs - Create:
scripts/authentication-smoke.sh - Create:
frontend/e2e/auth.spec.ts - Modify:
.github/workflows/deployment.yml - Modify:
docs/testing/authentication-manual-acceptance.md - Modify:
PROJECT_STATE.mdafter evidence is retained
Interfaces:
-
Produces one retained test report containing commit SHA, image IDs, Node/Pi versions, individual gate status, and no secret values.
-
Consumes every interface and acceptance condition from Tasks 1–14.
-
Step 1: Add a deterministic fake OIDC provider and browser E2E
The fixture exposes discovery, JWKS, authorization, token, device authorization, and Authentik-like group-list endpoints on loopback only. It issues signed short-lived ID tokens for ordinary, admin, missing-groups, malformed-groups, and unmapped-group identities.
Playwright covers local ordinary/remembered login, backend restart, logout, admin chrome, OIDC redirect/callback, 403 for an unmapped user, and expired session recovery.
- Step 2: Run every language-level gate
cd tools/tht && go test -race ./... && go build ./cmd/tht
cd ../../backend && npx tsc --noEmit -p . && npx vitest run && npm run build
cd ../frontend && npx tsc -b && npx vitest run && npm run build && npm run e2e
cd ../harness && .venv/bin/ruff check . && .venv/bin/pytest -q
- Step 3: Run Docker, installation, and security smoke gates
cd ..
bash scripts/authentication-smoke.sh
bash scripts/unified-deployment-smoke.sh
bash scripts/auth-docs-smoke.sh
Verify the built core reports Node v24.16.0, Pi starts, auth state survives only the intended
restart, and no sentinel secret appears in logs or artifacts.
- Step 4: Run the opt-in L2 live-session smoke
Use the repository's configured PSD/L2 secret layout without copying it into the worktree:
cd harness
.venv/bin/pytest -q -m l2
Then create one browser session through the real stack and complete a live session workflow as an ordinary authenticated user. Confirm principal ownership remains stable after resume.
- Step 5: Execute Authentik PSD manual acceptance
Follow docs/testing/authentication-manual-acceptance.md. Run tht auth check --interactive,
workspace Validate/Test, ordinary/admin authorization checks, extra-group no-warning check, and
mapped-group rename failure. Store only sanitized pass/fail evidence under a task-scoped
.artifacts/manual-acceptance/authentication/ directory.
- Step 6: Update project state with actual evidence
Record exact pass counts, retained artifact digest, source commit, Node version, Authentik version, and whether manual PSD acceptance is PASS or PENDING. Do not mark the feature complete while any required gate is pending.
- Step 7: Commit final gates and state
git add backend/test/fixtures/oidc-provider.mjs frontend/e2e/auth.spec.ts \
scripts/authentication-smoke.sh .github/workflows/deployment.yml \
docs/testing/authentication-manual-acceptance.md PROJECT_STATE.md
git commit -m "test(auth): gate local and Authentik authentication release"
Final acceptance checklist
tht --helpexposes one CLI and the completeauthsubtree.- A fresh local setup creates one admin without plaintext credentials.
- Ordinary local login and Remember me behave with the exact configured lifetimes.
- A remembered session survives browser and core restart.
- Password change, role change, disable, logout-all, logout, config change, and restore revoke the expected sessions.
- Every state-changing cookie-authenticated route rejects missing/invalid CSRF.
- Ordinary and admin permission matrices pass in backend and browser tests.
- OIDC Authorization Code + PKCE validates issuer, signature, audience, expiry, state, nonce,
and mandatory direct
groups. - Authentik group validation fails for configured missing/ambiguous groups.
- Unmapped Authentik/token groups produce no error, warning, or log entry.
- Workspace static validation and live Test include authentication and combine
activatable. tht auth check, interactive device check, and aggregatetht doctorare redacted and machine-readable.- Frontend stores no token/session secret in Web Storage and SSE uses only the session cookie.
- Node 24.16, Pi, backend, frontend, harness, Compose, backup/restore, L2, and PSD gates pass.
- Documentation explains the mandatory
groupsclaim and exact group-to-role mapping.