Signing members in
A member can reach the booking engine in three ways. All three end with the same thing on your side: your own short session that knows the member’s sub.
| Arrives from | How you identify them | Can browse as member | Can pay |
|---|---|---|---|
| FINNS app on a phone | One-time handoff code, redeemed with core | ✓ | after a fresh sign-in |
| FINNS app on the web, or a returning visitor | Keycloak SSO (OIDC), silently if possible | ✓ | after a fresh sign-in |
| Your site directly | Login button (OIDC) | ✓ | after a fresh sign-in |
From the app: the handoff
Section titled “From the app: the handoff”The app can’t share its browser session with the system browser on a phone, so it hands over a one-time code instead.
sequenceDiagram
autonumber
actor G as Guest
box rgb(227,244,243) FINNS platform
participant APP as FINNS app
participant CORE as Core
end
box rgb(246,241,233) Booking engine
participant BE as Booking site
end
G->>APP: Tap "Book"
APP->>CORE: POST /api/v1/app/handoffs (Bearer access token)
CORE-->>APP: 201 {code, ttlSeconds: 60}
APP->>BE: Open browser: GET /auth/handoff?code=…
Note over BE: nowrap:Seal {code, returnTo} in a short cookie,<br/>so the code leaves the URL and history
BE-->>G: 303 → /auth/handoff
G->>BE: GET /auth/handoff
BE->>CORE: POST /api/v1/booking/handoffs/peek {code}
CORE-->>BE: 200 {displayName, email} (code not used up)
BE-->>G: "Continue as Ayu (ayu@example.com)?"
G->>BE: POST /auth/handoff (same-origin)
BE->>CORE: POST /api/v1/booking/handoffs/redeem {code}
CORE-->>BE: 200 {sub, sid, displayName, email} (code spent)
Note over BE: nowrap:New session for sub, source "handoff"
BE-->>G: 303 → returnTo, signed in
- Never redeem on GET. Link previews and prefetchers follow GET links.
- Move the code out of the URL before rendering anything (sealed cookie + redirect).
- Treat every failure the same: unknown, used, expired, revoked or malformed codes all return
404. Send the guest to your normal sign-in; don’t retry the redeem. - Check the code’s shape first (43 base64url characters) and rate-limit the endpoint per IP.
From the web: Keycloak SSO
Section titled “From the web: Keycloak SSO”Your site is an OpenID Connect client of the FINNS realm, so a guest already signed in to the FINNS web app (or anywhere else in the realm) is recognised without typing anything.
sequenceDiagram
autonumber
actor G as Guest
box rgb(246,241,233) Booking engine
participant BE as Booking site
end
box rgb(243,236,223) FINNS identity
participant KC as Keycloak
end
G->>BE: GET / (top-level navigation)
Note over BE: nowrap:No session, and no silent attempt<br/>in the last 10 h
BE-->>G: 303 → Keycloak, prompt=none, PKCE, nonce
G->>KC: Authorization request
alt Signed in to Keycloak
KC-->>G: 303 → /redirect?code=…
G->>BE: GET /redirect
BE->>KC: Exchange code
KC-->>BE: ID token (sub, sid, name, email)
Note over BE: nowrap:New session, source "oidc"
else Not signed in
KC-->>G: 303 → /redirect?error=login_required
Note over BE: nowrap:Stay a guest, and don't ask again for 10 h
endThe same flow without prompt=none: Keycloak shows its sign-in or registration page. The FINNS app’s web build opens /auth/login directly, which signs the member in through SSO with no page shown.
Keep only the ID token’s claims (sub, sid, name, email, auth_time); discard the tokens themselves.
Before payment: a fresh sign-in
Section titled “Before payment: a fresh sign-in”A session that came from a handoff, or from an old SSO session, proves less than a password typed just now. Paying always requires a fresh sign-in.
sequenceDiagram
autonumber
actor G as Guest
box rgb(246,241,233) Booking engine
participant BE as Booking site
end
box rgb(243,236,223) FINNS identity
participant KC as Keycloak
end
G->>BE: GET /checkout
Note over BE: nowrap:Session is "handoff", or reauth older than 5 min
BE-->>G: 303 → Keycloak, prompt=login, max_age=0
G->>KC: Enter password
KC-->>G: 303 → /redirect?code=…
G->>BE: GET /redirect
BE->>KC: Exchange code
KC-->>BE: ID token with auth_time
Note over BE: nowrap:Require auth_time ≥ start − 30 s.<br/>New session with reauth_at = now
G->>BE: GET /checkout → pay
Your sessions
Section titled “Your sessions”| Rule | Reference implementation |
|---|---|
| Session id: random, 256 bits; store only its hash | __Host- cookie, HttpOnly; Secure; SameSite=Lax |
| Short, absolute lifetime, never extended | 15 minutes |
| New id on every sign-in; delete the old one | ✓ |
| Remember sign-outs, so a late code or SSO response can’t recreate a session | signed_out_sub (10 h), revoked_sid (1 h) |
Internal and confidential. Share partner sections only as partner builds (pnpm build:partner).

