feat(tokens): access-token management (DESIGN.md §13, phase 6b) #24

Merged
KofTwentyTwo merged 3 commits from feature/token-management into develop 2026-10-14 12:44:04 +00:00
KofTwentyTwo commented 2026-10-14 02:59:51 +00:00 (Migrated from github.com)

Summary

This is phase 6b of access tokens, QQQ's personal access token solution (DESIGN.md §13, owner decisions 23–30).

  • TOKENS feature, on by default whenever writes are enabled. Opt out with tokensEnabled=false or AUTHENTIK_QBIT_TOKENS=false. New permission: authentik.tokens.manage.
  • Actions. Standard streamed-ETL Review/Result actions, none of them in the nav:
    • on authentik_token: Create Token (for yourself), Rotate Token, Revoke Token (single and bulk), Extend Expiry;
    • on authentik_user: Create Service Account.
  • Mechanism (decision 30, verified on 2026.8.3).
    • People's tokens are created as that person: an RFC 8693 token exchange through the qqq-<appKey>-token-broker provider gives a 2-minute API token that is used for the one action and then dropped.
    • Service accounts use service_account (create), set_key with a qbit-generated key (rotate) and PATCH (extend).
    • Every PATCH sends intent, user and expires, and a race guard re-reads the token afterwards.
    • The service-account lifetime cap is a user attribute (serviceAccountTokenLifetime, default days=365).
  • Guardrails (§13.4):
    • (a) Protected owners are refused: superusers (including inherited), authentik Admins, identity-admins, tokenProtectedGroups, qqq.io/protected and the qbit account. This is also enforced in Authentik by the broker's deny policy.
    • (b) Self-only for people. Admins may also target service accounts, but never another human, except to revoke.
    • (c) An expiry is required and capped (90 days for people, 365 for service accounts).
    • (d) Optional two-person approval for service-account tokens.
    • (f) Every action is audited before the call.
  • One-time key. It is delivered through core one-time process output (QRun-IO/qqq#1174, on top of #1162). This is a STAND-IN, by reflection until the RC: when core can't deliver, Review refuses, so nothing is created. The key never reaches tables, the audit, Authentik events, logs, AdminResult.toString or process state. Usage text is generated from config.
  • Scope table. authentik_token_scope: non-secret metadata, record audit, no generic writes. Scope enforcement comes in 6c.

Contract findings (in DESIGN.md)

  • service_account needs add_user + add_token, and set_key needs add_token (Authentik maps a detail POST to add_*) plus set_token_key. Granting add_token is safe because a non-superuser's create always belongs to the caller. view_token_key is never granted.
  • The broker provider needs redirect_uris: []. Fixed in KofTwentyTwo/cluster-gitops#103, which is the companion PR.

Test plan

  • ./mvnw verify -DskipITs: unit tests and the JaCoCo gate
  • TokenContractIT, 9/9 on Authentik 2026.8.3 with the production blueprints: caller-owned create, PATCH defaults, expiry cap, broker exchange as the user and owner isolation, deny policy, qqq_token claim, service-account create/set_key/extend, the detective alarm, and the qbit lifecycle with the key absent from audit rows and events
  • All other contract ITs green (53 total). SuperuserFloorIT now accepts 400 from /recovery/ with the production reset_user_password (the qbit never calls it)
  • CI

🤖 Generated with Claude Code

## Summary This is phase 6b of access tokens, QQQ's personal access token solution (DESIGN.md §13, owner decisions 23–30). - **`TOKENS` feature, on by default** whenever writes are enabled. Opt out with `tokensEnabled=false` or `AUTHENTIK_QBIT_TOKENS=false`. New permission: `authentik.tokens.manage`. - **Actions.** Standard streamed-ETL Review/Result actions, none of them in the nav: - on `authentik_token`: *Create Token* (for yourself), *Rotate Token*, *Revoke Token* (single and bulk), *Extend Expiry*; - on `authentik_user`: *Create Service Account*. - **Mechanism (decision 30, verified on 2026.8.3).** - People's tokens are created **as that person**: an RFC 8693 token exchange through the `qqq-<appKey>-token-broker` provider gives a 2-minute API token that is used for the one action and then dropped. - Service accounts use `service_account` (create), `set_key` with a qbit-generated key (rotate) and `PATCH` (extend). - Every `PATCH` sends `intent`, `user` and `expires`, and a race guard re-reads the token afterwards. - The service-account lifetime cap is a user attribute (`serviceAccountTokenLifetime`, default `days=365`). - **Guardrails (§13.4):** - (a) Protected owners are refused: superusers (including inherited), `authentik Admins`, `identity-admins`, `tokenProtectedGroups`, `qqq.io/protected` and the qbit account. This is also enforced in Authentik by the broker's deny policy. - (b) Self-only for people. Admins may also target service accounts, but never another human, except to revoke. - (c) An expiry is required and capped (90 days for people, 365 for service accounts). - (d) Optional two-person approval for service-account tokens. - (f) Every action is audited before the call. - **One-time key.** It is delivered through core one-time process output (QRun-IO/qqq#1174, on top of #1162). This is a STAND-IN, by reflection until the RC: when core can't deliver, Review refuses, so nothing is created. The key never reaches tables, the audit, Authentik events, logs, `AdminResult.toString` or process state. Usage text is generated from config. - **Scope table.** `authentik_token_scope`: non-secret metadata, record audit, no generic writes. Scope enforcement comes in 6c. ## Contract findings (in DESIGN.md) - `service_account` needs `add_user` + `add_token`, and `set_key` needs `add_token` (Authentik maps a detail POST to `add_*`) plus `set_token_key`. Granting `add_token` is safe because a non-superuser's create always belongs to the caller. `view_token_key` is never granted. - The broker provider needs `redirect_uris: []`. Fixed in KofTwentyTwo/cluster-gitops#103, which is the companion PR. ## Test plan - [x] `./mvnw verify -DskipITs`: unit tests and the JaCoCo gate - [x] `TokenContractIT`, 9/9 on Authentik 2026.8.3 with the production blueprints: caller-owned create, PATCH defaults, expiry cap, broker exchange as the user and owner isolation, deny policy, `qqq_token` claim, service-account create/set_key/extend, the detective alarm, and the qbit lifecycle with the key absent from audit rows and events - [x] All other contract ITs green (53 total). `SuperuserFloorIT` now accepts 400 from `/recovery/` with the production `reset_user_password` (the qbit never calls it) - [ ] CI 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.