Implementation of the MitID / OpenID login flow in the Self Service application.
All endpoints are under /api/{apiVersion}/{organisation}.
Scope: This document describes the MitID (OpenID) login provider. The application has other providers (a planned email-code login) that are out of scope here — they converge on the same final step (Establish session) and share the same session store, so downstream services authenticate uniformly regardless of provider.
Steps
1. Start
POST /auth/openid with a redirect_uri. Receive { ref, openUri }.
ref— an opaque handle for this auth session.openUri— where the user completes MitID.
Direct the browser to openUri.
2. Authenticate
User completes MitID. Bright binds the authenticated identity to the ref server-side (the CPR/identity is resolved from the ref; the client never handles it).
3. Poll status
Poll POST /accounts/status with { ref } on a fixed interval, branching on status:
| Status | Meaning | Action |
|---|---|---|
authProgress | Authentication still in progress | Transient — keep polling |
accountCreating | Account being created | Transient — keep polling |
exists | Identity is linked to an account | Proceed to Exchange for session |
notInBright | Identity not yet known to Bright | POST /accounts/create { ref }, then keep polling |
needsAssociation | Identity must be linked to a customer | Collect customerId + pinCode, POST /accounts/associate { ref, customerId, pinCode }, then keep polling |
notCustomer | Not a customer of this organisation | Terminate |
error | Authentication failed | Terminate — surface message |
Guards (one action per ref):
createis issued only once perref. Bright can lag in flipping offnotInBright; re-firingcreateagainst the now-existing account returns a409 ConflictError. After issuing, just keep polling for the terminal state.associate(prompt + submit) happens only once perref. If the association doesn't take (wrongcustomerId/pinCode), Bright keeps returningneedsAssociation. Surface the failure immediately rather than silently re-prompting and looping until timeout.
4. Loop bound
Cap polling with a max iteration count + delay. If no terminal state is reached, throw a timeout. (Currently: 60 iterations × 2 s ≈ 120 s ceiling.)
5. Exchange for session
POST /sessions/access-token with { ref }. Take the first token's accessToken / refreshToken / userId.
6. Establish session
GET /users/{userId} with the bearer token to resolve accountId (first account), then hand (accessToken, refreshToken, userId, accountId) to the shared session store. Downstream services authenticate via that store regardless of provider.
