Add ZITADEL-compatible client-credentials JWT issuance
Implement the production-compatible OAuth 2.0 client-credentials flow required by the Kaukus-to-Murmur service account before the Rust identity cutover. Verified production contract: - Token endpoint: `/oauth/v2/token` - Client authentication: `client_secret_basic` - Grant: `client_credentials` - Required request scope: `openid urn:zitadel:iam:org:project:id:{project_id}:aud` - Service account access-token type: JWT - Signing algorithm: RS256 through the existing JWKS signing-key lifecycle - JWT claims consumed by Murmur: exact `iss`, service-account `sub`, project ID in `aud`, `exp`, `nbf`, `iat`, `kid`, and `client_id` - No ID token or refresh token is required. Constraints: - Authenticate the existing machine-user client ID and hashed client secret using pinned-Go-compatible storage and hashing. - Preserve scope filtering, audience construction, token persistence/lifecycle events, failure responses, secret redaction, and same-database pinned-Go readability. - Advertise `client_credentials` only after the flow is implemented. - Murmur locally accepts tokens for at most one hour after `iat`, independent of the issuer's longer expiry. Acceptance criteria: - The production Kaukus service account can obtain a JWT access token with the verified request shape. - Wrong/missing credentials, inactive machine users, unsupported scopes/grants, and non-JWT service-account configuration fail compatibly without leaking secrets. - Issued tokens validate through the Rust JWKS and the Murmur resource server with the expected claims. - Differential tests compare the Rust flow with the pinned Go baseline on synthetic compatible fixtures. - Pinned Go reads/validates the Rust-written access-token lifecycle state after an exclusive-writer restart, and the schema fingerprint remains unchanged. - Discovery, unit/integration/security tests, formatting, strict Clippy, and relevant compatibility gates pass. Planning decisions: - Treat the OAuth `client_id` as the machine user's canonical username/login, not as an OIDC application client ID. - Resolve and authenticate the active machine user from `projections.login_names3`, `projections.users14`, and `projections.users14_machines`; verify its redacted bcrypt secret outside database locks and recheck the exact credential/state before committing. - Support Basic authentication only for this grant, require JWT machine-token configuration, return no ID or refresh token, and keep all authentication failures externally generic. - Derive token audience from validated `urn:zitadel:iam:org:project:id:{project_id}:aud` scopes. Persist one Go-readable `oidc_session` aggregate with an empty browser session ID and `client_credentials` token reason. - Reuse the current signing-key/JWKS lifecycle and access-token state replay, but split access-token signing from authorization-code ID-token issuance and add the required `nbf` claim. - Extend the Rust-owned synthetic bootstrap with a fresh private machine secret and JWT token type; never use the archived production-data fixture. - Update the compatibility manifest because it currently advertises only `authorization_code`. - The reviewed Rust-owned synthetic schema already contains the pinned-Go-required feature projections. The bidirectional test path now covers machine tokens, but execution remains pending because no attested pinned-Go image is configured locally. Implementation status: - The transport-independent service, strict Basic grant dispatch, RS256 access-only signing, `nbf` compatibility, instance-scoped credential lookup, transactional credential/lifecycle rechecks, Go-shaped events, discovery, bootstrap credentials, manifest, and local documentation are implemented. - Workspace tests, strict Clippy, and the full `cargo dev test` synthetic contract suite pass, including success, generic failure, stale-secret, inactive-user, non-JWT, JWKS, persisted-authority, event-shape, lifecycle, and schema-stability coverage. - The attested pinned-Go exclusive-writer gate passes bidirectionally: Go accepts Rust-issued machine JWT/state and Rust accepts Go-issued machine JWT/state with unchanged schema. This gate discovered and fixed JWT scope encoding to the Go-compatible space-delimited representation while retaining legacy Rust array decoding. - An isolated actual-Murmur control-plane smoke passes through discovery/JWKS: a valid Rust machine JWT reaches the protected resource and returns authenticated not-found, while wrong-audience and tampered JWTs return 401. A deployed production Kaukus-to-Murmur smoke remains outstanding. - `cargo deny check` remains red on repository dependency advisories unrelated to this feature.
Checklist
6 of 8 complete- Add transport-independent machine credential authentication and scope/audience orchestration
- Generate a fresh JWT machine credential in the Rust-owned synthetic bootstrap and private dev environment
- Update manifest, status, README, and run all repository validation gates
- Pass Murmur consumer validation and bidirectional pinned-Go exclusive-writer readback
- Verify success, negative, race, lifecycle, secret-redaction, JWKS, and schema-stability behavior
- Add instance-scoped machine credential lookup and atomic Go-readable token persistence
- Implement grant-specific token parsing, RS256 access-only issuance, and discovery metadata
- Freeze the pinned-Go HTTP, scope, JWT-claim, error, and event contracts with differential fixtures
- Area
- Kaukus
- Kind
- Feature
- Priority
- High
- Task ID
tsk_vz63mp3tdd- Updated