Skip to content

Zitadel

Zitadel is the OIDC identity provider for the portal, the RADIUS SPA and the Monsieur Wifi app. One instance serves every environment:

  • Hostname: auth.digilan-edge.com
  • Runtime: Scaleway serverless container (rg.fr-par.scw.cloud/digilan/zitadel) with its own managed PostgreSQL instance (zitadel, backups enabled), separate from the shared platform database.
  • Source: infra/terraform/environments/shared/zitadel.tf, security.tf, email.tf (SMTP through Scaleway Transactional Email).

A person has one Zitadel account whichever environment they sign in to.

Projects and applications

Each environment has its own Zitadel project, digilan-<env>, holding that environment's applications. scripts/zitadel-apps.sh creates and reconciles them on tofu apply (it runs when the env's URLs or the script change) and records the ids in the environment directory.

Identifier Used by
Project id (zitadel_project_id output) NEXUS_ZITADEL_PROJECT_ID, VITE_ZITADEL_PROJECT_ID, radius Zitadel__Audience
Portal client id (portal_client_id) VITE_ZITADEL_CLIENT_ID, supplied to portal CI as DEV_ / STG_ / PRD_ZITADEL_CLIENT_ID
Monsieur Wifi client (mrwifi_client_id, mrwifi_client_secret) Monsieur Wifi web app
Radius client id (radius_client_id) RADIUS SPA

Nexus validates the token audience against its own environment's project id, so a token issued for one environment is refused by every other environment's nexus.

The nexus service user

environments/shared creates one machine user, nexus-service, with ORG_USER_MANAGER (scripts/zitadel-nexus-service.sh; output nexus_service_pat). Each environment passes its PAT in nexus_zitadel_service_token, which becomes NEXUS_ZITADEL_SERVICE_TOKEN on nexus and Zitadel__ServiceToken on the radius API. Nexus uses it to create invited users, resend invites, resolve an exact email to an account, and look up names and emails.

Login policy

The live login policy (instance and the one org):

  • Registration is closed. allowRegister and allowExternalIdp are false. The login page offers no sign-up. zitadel_registration_closed in security.tf enforces this on the instance policy and on every org with a policy of its own, changing only those two fields.
  • A second factor is optional. forceMfa is false. The enabled factors are OTP (authenticator app) and U2F (security keys); passwordless login with passkeys is allowed.

security.tf does not match the live MFA setting

null_resource.zitadel_login_policy sends a full policy with forceMfa: true. It runs only when its triggers change, so the live policy still has MFA optional. An apply that re-runs that resource would start forcing MFA for every user.

User provisioning

Accounts are created only by an invite from the portal: Team > Invite Member calls PortalUsersService.Invite, and nexus creates the user through the Zitadel Management API (or resends the invite if the address already has an account). The person sets a password from the invite email; adding a second factor is optional. Who may invite is described in Authz.

Signing in creates nothing in nexus. A new account has no organization and no role until it is granted one; until then the portal shows No organization yet.

Portal OIDC

The portal uses oidc-client-ts with the authorization code flow and PKCE. Redirect URI: https://<portal-host>/callback. Tokens are renewed silently before they expire.

Nexus token validation

At startup nexus loads the JWKS from ${NEXUS_ZITADEL_ISSUER}/oauth/v2/keys. Every portal RPC passes through PortalAuthInterceptor, which checks:

  1. the signature against the JWKS,
  2. iss equals NEXUS_ZITADEL_ISSUER,
  3. aud contains NEXUS_ZITADEL_PROJECT_ID.

The interceptor only authenticates. Each handler performs its own authorization check through authz.