0061: User Lifecycle and Invitation Strategy
Date: 2025-12-23
Status: Accepted
Context
The user-directory-service needs to support "Adding" (Admin creates user) and "Inviting" (User signs up) workflows. A naive implementation might involve the service accepting a password in the POST /users payload to create the user in the upstream IdP.
However, handling passwords within the user-directory-service introduces significant security risks and architectural anti-patterns:
- Credential Exposure: The service would need to handle cleartext passwords, increasing the attack surface and compliance scope.
- IdP Duplication: It treats the IdP as a dumb database rather than delegating authentication lifecycle management to it.
- User Experience: Setting a temporary password for a user is an outdated practice compared to self-service activation flows.
We need a strategy that allows for user provisioning and invitation without the user-directory-service ever touching a password.
Decision
We will adopt a Delegated Credential Management strategy. The Identity Provider (IdP) is the sole authority for credentials. The user-directory-service manages the identity profile and lifecycle state, but never the secrets.
1. "Add User" Flow (Admin Provisioning)
When an administrator manually creates a user via the Admin Portal:
- Create Identity: The
admin-bff(acting on behalf of the admin) callsPOST /userson theuser-directory-serviceusing an S2S token. The service calls the adapter to create the user in the IdP with profile data (Email, Name, Phone) but no credentials. - Trigger Activation: The adapter instructs the IdP to execute a "Required Action" flow (specifically
UPDATE_PASSWORDandVERIFY_EMAIL).- For Keycloak, this maps to the
PUT /admin/realms/{realm}/users/{id}/execute-actions-emailendpoint.
- For Keycloak, this maps to the
- Result: The IdP sends a branded email to the user containing a secure link to set their own password.
2. "Invite User" Flow (Just-In-Time Provisioning)
For invitations, we will use a Just-In-Time (JIT) provisioning model to avoid polluting the directory with inactive users.
- Create Invite: The
admin-bffcallsPOST /inviteson theuser-directory-serviceusing an S2S token. The service stores a localInviterecord (token, email, tenant_id) and sends a Citadel-branded email via thenotification-service. - Redemption: The user clicks the link and lands on the Citadel UI.
- Authentication: The user authenticates with the IdP (logging in or registering a new account self-service).
- Linkage: The Citadel UI sends the Invite Token and the new Auth Token to the backend. The backend validates the invite and adds the authenticated user to the tenant.
Consequences
Positive
- Zero Trust / Security: The platform never handles or stores user passwords, significantly reducing liability.
- Native UX: Users utilize the IdP's native, secure flows for password management (reset, recovery, MFA setup).
- Clean Directory: The JIT invitation flow prevents "ghost users" (invited but never active) from cluttering the upstream IdP.
Negative
- Dependency on IdP Features: We rely on the upstream IdP supporting "Execute Actions" emails (Keycloak does, others like Auth0 have similar "Password Change Ticket" APIs). Adapters for simpler IdPs might need workarounds.
- Frontend Complexity: The JIT flow requires the frontend to handle the "Invite Token" state through the OIDC redirect dance.