Skip to main content
Proposed (STA-834, STA-835). It extends 0206, 0207 and 0278. Linear STA-834, STA-835.
Note 2026-10-26 (STA-1264), additive: IdpAdmin.listAuthMethods(userId) answers a user’s enrolled authentication methods as RFC 8176 amr codes (the ZITADEL driver maps its method types; the level each code reaches stays IdentityConfig.assuranceByMethod), for the security page’s MFA enrolment list. ApiKeyRecord gains lastUsedAt and ApiKeyStore gains recordUse(id, at) with a no-op default; ApiKeyVerifierService writes it after a passing verification when it is older than IdentityConfig.apiKeyLastUsedSeconds (60), and a failed write never refuses the key.
Note 2026-10-26 (STA-1263), additive: LoginPolicy gains externalProviderIds (the provider-side ids of the market pack’s sign-in providers, through IdentityConfig.externalProviders) and secondFactors (IdpSecondFactor: totp, u2f, otp_email, otp_sms). When a list is given, setLoginPolicy leaves the organisation offering exactly those (the ZITADEL driver adds the missing and removes the others, regional providers as system-owned); a list left out is not touched. This is decision 4 applied to sign-in methods per market (OS identity A3).

Context

Nothing in Stackra created a ZITADEL user or machine user, ended a session, ran the OAuth device flow or set an organisation’s login policy at run time, so device credentials, machine principals and revocations could not reach the provider. API keys were stored as hashes, but the gateway had no verification read.

Decision

  1. IdpAdmin port in @stackra/identity/contracts: createUser, createMachineUser (with its client secret, shown once), deactivateUser, removeUser, endSession (a gone session is fine), endSessions(user), startDeviceAuthorization and pollDeviceToken (RFC 8628), and setLoginPolicy.
  2. ZitadelIdpAdminDriver uses the v2 user and session services, the management API for machine users, secrets and the login policy, and the OAuth device endpoints. Managed or bring-your-own is configuration: IdpAdminConfig.url is the platform’s ZITADEL or the tenant’s own, and the token is the IDENTITY_IDP_ADMIN_TOKEN secret. A 409 is idp_user_exists; a timeout, network failure, 429 or 5xx is idp_admin_unavailable (retryable). MemoryIdpAdminDriver serves tests.
  3. API-key verification by hash. hashApiKey is SHA-256 of the key (unsalted; the key holds 178 random bits). ApiKeyVerifierService.verify(key, origin) refuses a malformed key with no lookup, reads the record through the ApiKeyStore port by hash, compares in constant time, and refuses a revoked or expired key, a publishable key from an unbound origin and a secret key from a browser, all as one api_key_rejected.
  4. Login policies are data. The values come from tenant settings and market-pack defaults and are pushed through the port; nothing in code picks them.

Consequences

  • The identity service binds the ZITADEL driver and implements ApiKeyStore over its key table.
  • Devices get credentials through createMachineUser or the device flow (device_credential.issued).

Enforcement

packages/security/identity/src/core/drivers/__tests__/zitadel-idp-admin.driver.test.ts, packages/security/identity/src/core/services/__tests__/api-key-verifier.service.test.ts.

Supersedes and sources

Supersedes: none. Sources: STA-834 (services/identity SPEC G2); STA-835 (G6); ADR 0235 decision 1.