How guests sign in
Every guest has one FINNS account, held in Keycloak (realm finns). The guest app, the booking website and any future channel sign in against that same account with standard OpenID Connect. No partner runs its own sign-up or stores passwords.
Identifiers
Section titled “Identifiers”A guest has four identifiers, and each party gets only the ones it needs.
| Identifier | Looks like | Issued by | Who sees it |
|---|---|---|---|
Database id (customer.id) |
Core’s row id, used in foreign keys | Core | Core only. It never leaves core: not in any API, token or log shared with others |
Subject (sub) |
Keycloak user id, permanent and internal | Keycloak | The app, the admin console, core, and the booking engine (it signs in with Keycloak itself). The one id internal apps address a guest by |
Session id (sid) |
Keycloak session id | Keycloak | Core and the booking engine, to sign one session out |
| FINNS ID | Keycloak username | Keycloak | The guest only, who can sign in with it instead of their email. It can change (rarely), so nothing uses it as a key |
External id (extId) |
lowercase UUID | Core | Partners. It is the only identifier a partner ever receives, so partners store nothing else, and the guest never sees it. Internal apps never address a guest by it |
Name and email are copied from Keycloak into core, and are given to the booking engine at sign-in only. The engagement partner never receives them.
Signing in from the app
Section titled “Signing in from the app”The app uses the authorization code flow with PKCE as a public client (trident-app). Registration, password reset and any second factor are Keycloak’s own pages, so they can change without an app release.
sequenceDiagram
autonumber
actor G as Guest
box rgb(227,244,243) FINNS platform
participant APP as Guest app
participant KC as Keycloak
participant CORE as Core
end
G->>APP: Tap "Sign in"
APP->>KC: Authorization request (PKCE, scope openid profile email)
KC->>G: Sign-in or registration page
G->>KC: Credentials
KC-->>APP: Redirect with authorization code
APP->>KC: Exchange code + PKCE verifier
KC-->>APP: Access token, refresh token, ID token
Note over APP: nowrap:Tokens kept in Keychain / Keystore (mobile)<br/>or browser storage (web)
par Keycloak tells core about the account
KC-)CORE: POST /api/v1/keycloak/user-changes {userId}
CORE->>KC: Read user (Admin API, service account)
Note over CORE: nowrap:Create or update the customer:<br/>public external id, name, email copy
and The app uses its token
APP->>CORE: GET /api/v1/app/points (Bearer access token)
CORE-->>APP: {balance}
end
If the notice from Keycloak is lost, core creates the customer on the app’s first call anyway, and a reconciliation every ten minutes repairs any drift.
On mobile, the app asks for the password at each sign-in (prompt=login, ephemeral browser session) and then keeps the guest signed in with an offline refresh token. On the web, the app relies on Keycloak’s browser session, which is also what lets the booking website sign the guest in silently.
Signing in to a partner: one-time codes
Section titled “Signing in to a partner: one-time codes”The app never gives a partner its access token. When a guest moves from the app to a partner (the booking website, or a widget inside the app), the app asks core for a one-time code and hands that over instead. The partner’s server exchanges it with core.
sequenceDiagram
autonumber
box rgb(227,244,243) FINNS platform
participant APP as Guest app
participant CORE as Core
end
box rgb(246,241,233) Partner
participant P as Partner server
end
APP->>CORE: Ask for a code (Bearer access token)
CORE-->>APP: {code, ttlSeconds: 60}
APP->>P: Hand over the code
P->>CORE: Redeem the code (Bearer partner token)
CORE-->>P: Who it is (identifiers this partner may see)
Note over P: nowrap:Start the partner's own short session.<br/>The code is now spent.
Signing out everywhere
Section titled “Signing out everywhere”When a guest signs out of the app, core tells every partner, so no session outlives the app’s.
sequenceDiagram
autonumber
actor G as Guest
box rgb(227,244,243) FINNS platform
participant APP as Guest app
participant CORE as Core
participant KC as Keycloak
end
box rgb(246,241,233) Partners
participant BOOK as Booking engine
participant PTS as Sota Platforms
end
G->>APP: Sign out
APP->>CORE: POST /api/v1/app/logout (Bearer access token)
Note over CORE: nowrap:Void this session's unused codes
CORE-->>APP: 204
par Core notifies partners (async, one retry)
CORE-)BOOK: POST /auth/revoke {sub, sid}
BOOK-->>CORE: 200 (sessions deleted)
and
CORE-)PTS: POST /auth/revoke {extId, signedOutAt}
PTS-->>CORE: 200 (sessions deleted)
end
APP->>KC: Revoke refresh token / end Keycloak session
opt Web sign-out ends the Keycloak session
KC-)BOOK: Back-channel logout (signed logout_token)
end
- Each partner hosts
POST /auth/revokeat the root of its origin and answers exactly200. Core retries once on anything else. - A partner must also refuse any later attempt to start a session from a code issued before the sign-out, because a code can still be in flight when the revoke arrives.
- Partners that sign guests in with Keycloak directly (the booking engine) also receive Keycloak’s own back-channel logout.
Keycloak clients
Section titled “Keycloak clients”| Client | Type | Used by | Purpose |
|---|---|---|---|
trident-app |
Public, PKCE | Guest app | Guest sign-in; core accepts only tokens issued to this client |
poc-booking |
Public, PKCE | Booking website | Direct sign-in, silent SSO, fresh sign-in before payment |
finns-core-sync |
Confidential, service account only | Core | Read users (view-users), nothing else |
A new partner that signs guests in itself gets its own client, with its own redirect URIs and back-channel logout URL.
Internal and confidential. Share partner sections only as partner builds (pnpm build:partner).

