Skip to main content

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:

  1. Credential Exposure: The service would need to handle cleartext passwords, increasing the attack surface and compliance scope.
  2. IdP Duplication: It treats the IdP as a dumb database rather than delegating authentication lifecycle management to it.
  3. 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:

  1. Create Identity: The admin-bff (acting on behalf of the admin) calls POST /users on the user-directory-service using an S2S token. The service calls the adapter to create the user in the IdP with profile data (Email, Name, Phone) but no credentials.
  2. Trigger Activation: The adapter instructs the IdP to execute a "Required Action" flow (specifically UPDATE_PASSWORD and VERIFY_EMAIL).
    • For Keycloak, this maps to the PUT /admin/realms/{realm}/users/{id}/execute-actions-email endpoint.
  3. 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.

  1. Create Invite: The admin-bff calls POST /invites on the user-directory-service using an S2S token. The service stores a local Invite record (token, email, tenant_id) and sends a Citadel-branded email via the notification-service.
  2. Redemption: The user clicks the link and lands on the Citadel UI.
  3. Authentication: The user authenticates with the IdP (logging in or registering a new account self-service).
  4. 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.