Skip to content

feat: Add Twake Token Manager — inter-service token broker#9

Open
mmaudet wants to merge 78 commits intomainfrom
feat/token-manager
Open

feat: Add Twake Token Manager — inter-service token broker#9
mmaudet wants to merge 78 commits intomainfrom
feat/token-manager

Conversation

@mmaudet
Copy link
Copy Markdown
Member

@mmaudet mmaudet commented Apr 3, 2026

Summary

Twake Token Manager — inter-service token broker for Twake Workplace. Centralizes OAuth2/OIDC token lifecycle management for all services behind a unified API with console-style token management UI.

What's included

Backend API (token-manager-api.twake.local):

  • Granular token management (CRUD per service) with 4 connectors
  • Umbrella token facade (single opaque token for multiple services)
  • Transparent proxy with token injection
  • OAuth2 callbacks with DB-persisted pending auth state
  • BullMQ automatic token refresh (cron)
  • Admin endpoints: user list, bulk revocation, global audit log
  • 81 unit tests (Vitest)

4 Service Connectors (E2E tested with real tokens + browser consent):

  • TMail JMAP — OIDC Authorization Code (james client)
  • Calendar CalDAV — OIDC Authorization Code (openpaas client)
  • Matrix Chat — SSO redirect flow
  • Cozy Drive — OAuth2 PKCE flow

Frontend v2 (token-manager.twake.local) — cozy-ui inspired design:

  • My Tokens: token list (service + umbrella), "+ Create Token" dialog
  • Create Token dialog: Service/Umbrella toggle, one-time token display (OpenAI-style), Token/Usage tabs with curl examples
  • Dashboard: stats cards, recent activity
  • Audit Log: user's own token actions
  • Admin Users & Tokens: accordion per user, Type column (Service/Umbrella), bulk revocation with confirmation
  • Admin Global Audit Log: all users, filterable
  • Admin Configuration: per-service refresh settings
  • SSO auth: OIDC redirect to LemonLDAP + dev-token fallback
  • Unique umbrella token names enforced

SDK + CLI included.

E2E Curl Validation

Service Direct curl Via Umbrella Proxy
TMail JMAP 7 mailboxes via proxy with SHA-256 accountId
Calendar CalDAV PROPFIND principals via proxy
Matrix Chat joined_rooms via proxy
Cozy Drive root directory via proxy

Known Limitations (next sprint)

  • Migrate to cozy-ui native components (currently inline styles matching cozy-ui palette)
  • Token refresh requires real OIDC session (502 with dev-token — expected)
  • WebDAV PROPFIND via umbrella proxy needs Fastify method registration
  • React hydration warning (#418) from localStorage in SSR

Test plan

  • Start infrastructure: ./wrapper.sh up -d
  • Build: cd token_manager && docker compose --env-file ../.env build && docker compose --env-file ../.env up -d
  • /etc/hosts: 127.0.0.1 token-manager.twake.local token-manager-api.twake.local
  • Health: curl -sk https://token-manager-api.twake.local/health
  • Frontend: https://token-manager.twake.local/tokens?dev_user=user1
  • Unit tests: cd token_manager && npm test (81 tests)

🤖 Generated with Claude Code

mmaudet and others added 30 commits April 3, 2026 08:06
- Rewrite README with project description, features, table of contents,
  quick start guide, and troubleshooting section
- Add CONTRIBUTING.md with guidelines for issues, PRs, and code style
- Add AGPL-3.0 LICENSE file

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Credit LINAGORA as Twake.ai developer in introduction
- Replace " -- " dashes with colons throughout
- Add ~20 GB disk space requirement in prerequisites

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- cozy_stack → drive_app
- meet_app → visio_app
- tmail_app → mail_app

Update all references in wrapper.sh, docker-compose.yaml,
.gitignore, README.md, and CLAUDE.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Group DNS entries by service with comments for clarity.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Each component now uses a descriptive project name visible in
Docker Desktop: twake-db, twake-auth, twake-drive, twake-visio,
twake-calendar, twake-chat, twake-mail, twake-onlyoffice, twake-linshare.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace declare -A associative arrays with case-based lookup
functions. macOS ships with Bash 3.2 which does not support
associative arrays.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Generate homeserver.yaml directly instead of homeserver-postgres.yaml
and remove the redundant file-level volume mount. The directory mount
(./synapse:/data) combined with the file mount was causing Docker to
create an empty homeserver.yaml in the source directory, which then
took precedence.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Design spec for the inter-service token broker covering v0.1 (MVP) and
v0.2 (Token Umbrella). Defines architecture, data model, service connectors,
API routes, BullMQ refresh, SDK/CLI, Next.js admin frontend, and Docker
integration into the existing twake-workplace infrastructure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
24-task plan covering scaffolding, Prisma schema, crypto, 4 service
connectors (Cozy PKCE, OIDC, Matrix), Fastify API with token/umbrella/proxy
routes, BullMQ refresh worker, TypeScript SDK, CLI, Next.js admin frontend,
and Docker Compose integration into the existing infrastructure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements the ServiceConnector interface for Cozy Drive using OAuth2
PKCE. Registers per-user OAuth2 apps on Cozy instances, handles the
full authorize/callback/refresh lifecycle, and includes unit tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements OidcBaseConnector (abstract) that uses the LemonLDAP OIDC
token directly for authentication, with refresh/revoke via the OIDC
issuer endpoints. TmailConnector and CalendarConnector extend it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements MatrixConnector for the twake-chat service, supporting token-based
login via m.login.token, native Matrix refresh/logout endpoints, and full unit
test coverage (9 tests).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements validateOidcToken and authHook with injectable verify function for testing. Extracts user info (sub, email, groups, isAdmin) from Bearer tokens and returns 401 on invalid/missing tokens.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…logic

Implements TokenService as core business logic between API routes and
connectors/Prisma layer, with full TDD test coverage (5 test cases).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements opaque umbrella token lifecycle: raw token generation with
twt_ prefix, SHA-256 hash for storage, DB-backed introspection with
active/revoked/expired status, and hash-based revocation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements Task 13: creates server.ts (buildApp export) that wires all
connectors, services, middleware hooks, and health route into a Fastify
app, plus the health.ts route returning {status, service}.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements all token management REST endpoints under /api/v1 in a
protected Fastify plugin: create, refresh, list, detail, single and
bulk revoke, plus admin routes for tenant token listing, config
view/update, and paginated audit log querying.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements POST/DELETE umbrella token endpoints (protected scope), a transparent
proxy route using umbrella token auth, and the public OAuth callback for Cozy
Drive consent flow.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements background token auto-refresh using BullMQ + Valkey: getTokensNeedingRefresh queries expiring active tokens, processRefreshJob decrypts/refreshes/re-encrypts per token with audit logging, and startRefreshScheduler wires a cron-driven Queue+Worker into the Fastify server bootstrap.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… found

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implements TwakeTokenManager SDK class with thin fetch-based wrappers for
all REST API endpoints, ConsentRequiredError / TwakeTokenManagerError classes,
and full unit test coverage (8 tests) using vi.stubGlobal fetch mocking.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements a thin CLI wrapper over the SDK with global options
(--api-url, --token, --tenant, --format), token commands (create,
list, status, refresh, revoke), umbrella subcommands, and admin
subcommands (list-tokens, audit via direct fetch).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add Next.js App Router project skeleton: next.config.js (standalone output),
tailwind.config.ts, tsconfig.json, postcss.config.js, globals.css,
lib/api.ts (typed apiFetch wrapper), and lib/auth.ts (OIDC token store).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add root layout with sidebar nav, root redirect to /admin, StatsBar
component (active/expired/umbrella counts), TokenTable with status badges
and revoke/refresh actions, and the admin dashboard page that polls the
API every 30s and drives both components.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
mmaudet and others added 30 commits April 3, 2026 15:27
…consent flow

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove old admin/user pages and legacy components (stats-bar, token-table,
refresh-config, user-access-list). Add app/tokens/page.tsx with fetch,
revoke, and refresh handlers wired to TokenList and CreateTokenDialog.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ivity

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…API endpoints

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ulk revoke

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…client in LemonLDAP

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Matrix authenticate tests now expect the SSO redirect flow (type: 'redirect') instead of a direct login call. Tmail revoke test now expects no throw since revoke is best-effort (errors are silently caught).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Map access_token/umbrella_token from API to normalized token field for display.
Handle 202 consent_required from service token endpoint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…r (removed in v2)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add GET /umbrella-tokens endpoint to list user's umbrella tokens
- My Tokens page fetches both service + umbrella tokens and merges them
- Revoke handles umbrella tokens via DELETE /umbrella-token/:id
- TokenItem now includes id field for umbrella revocation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The frontend sends the database ID (cuid) but the API expected the raw
token (twt_...). Now tries revoke by ID first, falls back to raw token.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ce labels

Umbrella tokens now show human-readable names (e.g., "TMail JMAP, Calendar CalDAV")
instead of raw service IDs or blank. Also fix React key uniqueness for token rows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…admin umbrella listing

- Fix: dialog sends 'scopes' (not 'services') for umbrella creation
- Add 'name' field to UmbrellaToken Prisma model — stored and displayed
- Token list shows custom name when set, falls back to scope labels
- Remove dark mode toggle from sidebar (light only)
- Admin Users page: fetch and display umbrella tokens in expanded view

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ish Service/Umbrella

Also add name, type, scopes fields to TokenData interface for umbrella display.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ella count, button styles

- Admin: hide Refresh for umbrella tokens, revoke by ID with umbrella: prefix
- Admin: expand button with Show/Hide label + arrow icon (more visible)
- Admin: confirmation dialog before revoke
- Dashboard: fetch umbrella tokens for correct Umbrella Tokens count
- My Tokens: replace underlined links with bordered buttons for Refresh/Revoke

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rror message

- New admin page /admin/audit with all users' audit entries and email filter
- Add "Global Audit Log" to admin sidebar navigation
- Improve refresh error message to explain re-authorization needed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…d dialog

Shows service endpoint, OIDC scope, and a ready-to-use curl command
with the actual token. Dark terminal-style code block with copy button.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…D in curl

- Modal widened to 680px
- Two tabs: Token (token + copy) and Usage (endpoint, scope, curl example)
- YOUR_ACCOUNT_ID replaced with hex-encoded user email (JMAP accountId)
- Curl example uses result.service to survive consent flow redirect

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
TMail JMAP uses SHA-256(email) as accountId, not hex-encoded email.
Computed via Web Crypto API when entering display step.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Token tab: "Done — I've copied it" (closes dialog)
Usage tab: "Copy curl" → "✓ Curl copied!" on click (copies curl to clipboard)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ialog

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…PI curl

Umbrella tokens cannot be used directly against service APIs — they must
go through the Token Manager proxy. The Usage tab now shows:
- Info banner explaining proxy requirement
- Curl example via /api/v1/proxy/{service}/... endpoint
- Lists other available services for multi-scope umbrellas

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Returns 409 Conflict if an active umbrella token with the same name
already exists for the user. Only non-revoked tokens are checked.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…r type

- Detect umbrella by twt_ prefix in token (survives state reset)
- Use result.scopes instead of selectedScopes (survives consent flow)
- Proxy URL correctly generated for all umbrella service types

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Each service gets a complete curl example via the proxy, including
method, headers, and body (JMAP POST, CalDAV PROPFIND, etc.)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Shows 'An active umbrella token named "All" already exists'
instead of raw JSON error response.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant