Skip to content

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

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
  1. Never redeem on GET. Link previews and prefetchers follow GET links.
  2. Move the code out of the URL before rendering anything (sealed cookie + redirect).
  3. 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.
  4. Check the code’s shape first (43 base64url characters) and rate-limit the endpoint per IP.

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
  end

Keep only the ID token’s claims (sub, sid, name, email, auth_time); discard the tokens themselves.

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
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).