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.
./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)
## 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)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
This is phase 6b of access tokens, QQQ's personal access token solution (DESIGN.md §13, owner decisions 23–30).
TOKENSfeature, on by default whenever writes are enabled. Opt out withtokensEnabled=falseorAUTHENTIK_QBIT_TOKENS=false. New permission:authentik.tokens.manage.authentik_token: Create Token (for yourself), Rotate Token, Revoke Token (single and bulk), Extend Expiry;authentik_user: Create Service Account.qqq-<appKey>-token-brokerprovider gives a 2-minute API token that is used for the one action and then dropped.service_account(create),set_keywith a qbit-generated key (rotate) andPATCH(extend).PATCHsendsintent,userandexpires, and a race guard re-reads the token afterwards.serviceAccountTokenLifetime, defaultdays=365).authentik Admins,identity-admins,tokenProtectedGroups,qqq.io/protectedand the qbit account. This is also enforced in Authentik by the broker's deny policy.AdminResult.toStringor process state. Usage text is generated from config.authentik_token_scope: non-secret metadata, record audit, no generic writes. Scope enforcement comes in 6c.Contract findings (in DESIGN.md)
service_accountneedsadd_user+add_token, andset_keyneedsadd_token(Authentik maps a detail POST toadd_*) plusset_token_key. Grantingadd_tokenis safe because a non-superuser's create always belongs to the caller.view_token_keyis never granted.redirect_uris: []. Fixed in KofTwentyTwo/cluster-gitops#103, which is the companion PR.Test plan
./mvnw verify -DskipITs: unit tests and the JaCoCo gateTokenContractIT, 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_tokenclaim, service-account create/set_key/extend, the detective alarm, and the qbit lifecycle with the key absent from audit rows and eventsSuperuserFloorITnow accepts 400 from/recovery/with the productionreset_user_password(the qbit never calls it)🤖 Generated with Claude Code