Skip to main content

Reconciliation Sessions

When the reconciliation engine decides to run IDV (identity verification), it creates a reconciliation session to manage the OIDC flow state. The session tracks the authorization parameters (PKCE code verifier, state, nonce), the redirect to the external identity provider (SURFconext), and the resulting identity claims. Sessions are short-lived (default 5 minutes) and encrypted at rest.

A reconciliation session exists for exactly one purpose: to carry state across the asynchronous OIDC authorization code flow. The flow involves a browser redirect to an external identity provider, user interaction at that provider, and a callback to the portal. The session ensures that the callback can be securely correlated with the original request and that no state is lost during the redirect.

Session State Machine

The following diagram illustrates the session state transitions from creation through completion or failure.

Reconciliation Session State Machine

Each session passes through a well-defined sequence of states. Forward transitions are the only valid transitions -- a session never moves backward.

StateDescription
CREATEDSession initialized. PKCE code challenge, state parameter, and nonce have been generated. The authorization URL has been constructed but the user has not yet been redirected.
REDIRECTEDThe authorization URL has been returned to the frontend, and the user's browser is being redirected to the external identity provider. The session is now waiting for the callback.
CALLBACK_RECEIVEDThe identity provider has called back with an authorization code and the state parameter. The state has been validated against the session's stored value. Token exchange has not yet occurred.
COMPLETEDToken exchange succeeded. The ID token has been validated. Identity claims have been extracted. The identity_match and identity_link_binding records have been created. The resolved identity is encrypted and stored on the session. This is a terminal state.
EXPIREDThe session exceeded its TTL (default 300 seconds) without reaching COMPLETED. This typically happens when the user takes too long at the institutional login page or abandons the flow. This is a terminal state.
ERRORAn error occurred during processing. This could be a failed token exchange, an invalid ID token, a network error contacting the provider, or any other unrecoverable failure. The error_message field contains details. This is a terminal state.

COMPLETED, EXPIRED, and ERROR are terminal states -- no further transitions are possible once a session reaches one of these states.

Session Record Structure

Each reconciliation session is persisted as a database record with the following columns:

ColumnTypeDescription
idTEXT (UUID)Primary key. A randomly generated UUID that uniquely identifies this session. Used in the callback URL to correlate the response with the session.
tenant_idTEXTThe tenant that initiated the authentication. Used for multi-tenant isolation -- sessions from one tenant cannot interfere with sessions from another.
statusTEXTCurrent state of the session (CREATED, REDIRECTED, CALLBACK_RECEIVED, COMPLETED, EXPIRED, or ERROR).
identifier_hashTEXTHMAC hash of the authenticated user's identifier (typically the holder key fingerprint). Links the session to the wallet authentication that triggered it.
identifier_typeTEXTType of the identifier being reconciled (e.g., KEY for holder key, SUBJECT_ID for institutional ID).
provider_idTEXTWhich reconciliation provider is being used (e.g., "surf"). References the provider configuration in the application settings.
authorization_urlTEXTThe full OIDC authorization URL with all parameters (client_id, redirect_uri, response_type, scope, code_challenge, state, nonce). This is the URL the frontend redirects the user to.
stateTEXTThe OIDC state parameter. A cryptographically random string generated for this session, used for CSRF protection. Validated when the callback is received.
nonceTEXTThe OIDC nonce parameter. A cryptographically random string included in the authorization request and expected in the ID token. Used for replay protection.
code_verifierTEXTThe PKCE code verifier. A cryptographically random string generated for this session. Stored encrypted in the database. Sent to the token endpoint during code exchange to prove that the caller is the same entity that initiated the authorization request.
redirect_uriTEXTThe callback URL registered with the OIDC provider. The provider sends the authorization code to this URL after the user authenticates.
token_endpointTEXTThe provider's token endpoint URL, discovered from the OIDC discovery document. Used during the authorization code exchange.
encrypted_identityTEXTThe resolved identity, encrypted with AES-256-GCM using Key C. Only populated when the session reaches COMPLETED status. Contains the merged and normalized claim set that will be used to create the identity link binding.
encrypted_identity_key_versionINTEGERThe version of Key C that was used to encrypt the encrypted_identity field. Enables key rotation -- the system can decrypt using the correct key version even after rotation.
error_messageTEXTHuman-readable error details. Only populated when the session reaches ERROR status. Useful for debugging but never exposed to end users.
created_atTEXTISO 8601 timestamp of when the session was created. Used together with expires_at to determine session validity.
expires_atTEXTISO 8601 timestamp of when this session expires. Computed as created_at + TTL (default TTL is 300 seconds). After this time, the session is considered expired and the callback will be rejected.

Session Lifecycle Walkthrough

The following sections walk through each phase of the session lifecycle in detail, explaining what happens at each step and why.

Step 1: Creation (CREATED)

The Auth Bridge creates a reconciliation session when the reconciliation plan is RunIdv (or StepUp). This happens after the wallet credential has been verified and the holder key has been extracted, but before any external identity provider is contacted.

During creation, the following cryptographic parameters are generated:

  • State parameter: A cryptographically random string (typically 32 bytes, base64url-encoded) used for CSRF protection. This value is included in the authorization URL as the state query parameter and is validated when the callback is received. If the callback's state does not match the session's state, the callback is rejected.
  • Nonce: A cryptographically random string used for ID token replay protection. This value is included in the authorization URL as the nonce query parameter and is expected to appear in the nonce claim of the returned ID token. If the ID token's nonce does not match, the token is rejected.
  • PKCE code verifier: A cryptographically random string (43-128 characters per RFC 7636) used for Proof Key for Code Exchange. The code verifier is stored (encrypted) in the session record. A SHA-256 hash of the verifier (the "code challenge") is included in the authorization URL. During token exchange, the original verifier is sent to the token endpoint, which verifies it against the stored challenge.

The provider's OIDC discovery document is fetched (or retrieved from cache) to determine the authorization endpoint and token endpoint URLs. The full authorization URL is constructed with the following parameters:

  • client_id -- the registered client identifier for this provider
  • redirect_uri -- the portal's callback URL
  • response_type=code -- authorization code flow
  • scope -- the configured scopes (e.g., openid profile email eduid)
  • code_challenge -- S256 hash of the code verifier
  • code_challenge_method=S256 -- indicates PKCE with SHA-256
  • state -- the session's state parameter
  • nonce -- the session's nonce

Step 2: Redirect (REDIRECTED)

The authorization URL is returned to the frontend as part of the authentication response. The frontend redirects the user's browser to this URL. At this point, control passes entirely to the external identity provider -- the portal has no visibility into what happens at the provider's login page.

The session transitions to REDIRECTED status, and the session timer is effectively running. The user must complete authentication at the identity provider and return via the callback before the session's TTL expires.

From the user's perspective, this is the moment they see their institution's login page (via SURFconext). They authenticate with their institutional credentials (username/password, multi-factor, etc.) and consent to sharing their identity attributes with the portal.

Step 3: Callback (CALLBACK_RECEIVED)

After the user authenticates at their institution, the identity provider redirects the browser back to the portal's callback URL with two query parameters:

  • code -- the authorization code, a one-time-use token that can be exchanged for identity tokens
  • state -- the state parameter that was included in the original authorization URL

The Auth Bridge receives the callback and performs the following validation:

  1. State validation: The state parameter from the callback is compared against the session's stored state value. If they do not match, the callback is rejected (this would indicate a CSRF attack or a session confusion error).
  2. Session lookup: The session is retrieved from the database using the session ID embedded in the callback URL.
  3. Expiry check: The session's expires_at timestamp is compared against the current time. If the session has expired, the callback is rejected with an appropriate error.
  4. Status check: The session must be in REDIRECTED status. If it is already COMPLETED, EXPIRED, or ERROR, the callback is rejected (preventing replay of the authorization code).

If all validations pass, the session transitions to CALLBACK_RECEIVED and the authorization code is retained for the next step.

Step 4: Completion (COMPLETED)

The Auth Bridge exchanges the authorization code for tokens by sending a POST request to the provider's token endpoint. The request includes:

  • grant_type=authorization_code
  • code -- the authorization code from the callback
  • redirect_uri -- the same redirect URI used in the authorization request
  • client_id -- the registered client identifier
  • client_secret -- the client secret (retrieved from the configured secret reference)
  • code_verifier -- the PKCE code verifier stored in the session (this proves the token request comes from the same entity that initiated the authorization)

The token endpoint responds with an access token, an ID token, and optionally a refresh token. The Auth Bridge then validates the ID token:

  1. Signature verification: The ID token's JWS signature is verified against the provider's published JWK set (from the discovery document).
  2. Issuer validation: The iss claim must match the expected issuer URL.
  3. Audience validation: The aud claim must contain the portal's client ID.
  4. Expiry validation: The exp claim must be in the future.
  5. Nonce validation: The nonce claim must match the session's stored nonce value.

If user-info-enabled is true for the provider, the Auth Bridge also calls the userinfo endpoint using the access token to retrieve additional claims that may not be present in the ID token.

Claims from the ID token and userinfo response are merged and then mapped through the material profile's canonical attribute rules. The merge mode for each attribute determines which source takes precedence when both provide a value.

With the merged claim set in hand, the Auth Bridge creates the identity records:

  • identity_match records: One for each material in the profile (e.g., a KEY-type match for the holder key fingerprint, a SUBJECT_ID-type match for the institutional sub claim).
  • identity_link_binding: The binding record that ties the matches together, containing the encrypted attribute envelope (encrypted with Key C via AES-256-GCM).

The resolved identity is encrypted and stored in the session's encrypted_identity field. The session transitions to COMPLETED. From this point forward, the wallet holder key is bound to the institutional identity, and all subsequent authentications will follow the UseExistingBinding fast path.

Session TTL

The default session TTL is 300 seconds (5 minutes), configurable via the session-ttl-seconds property:

sphereon:
app:
identity:
reconciliation:
session-ttl-seconds: 300

The TTL covers the entire round-trip duration: session creation, user redirect to SURFconext, user authentication at their institution (which may involve multi-factor authentication), callback to the portal, and token exchange. If the user takes too long at the institutional login page -- perhaps they need to look up a password, complete an MFA challenge, or step away from their computer -- the session expires and they must restart the reconciliation flow.

Five minutes is generally sufficient for the round-trip, but institutions with slow MFA flows or complex authentication chains may need a longer TTL. The TTL should be kept as short as practical to limit the window for session-related attacks.

A background cleanup job runs at a configurable interval (default every 5 minutes, set via session-cleanup.interval-minutes) and deletes expired sessions from the database. This prevents the accumulation of abandoned sessions and reduces the database's storage footprint. The cleanup job is idempotent and safe to run concurrently across multiple application instances.

Reconciliation Provider Configuration

Reconciliation providers define the external identity providers that can be used for identity verification. Each provider wraps an OIDC client configuration with additional reconciliation-specific settings.

sphereon:
app:
identity:
reconciliation:
oidc-clients:
surf-oidc:
discovery-url: "https://connect.test.surfconext.nl/.well-known/openid-configuration"
client-id-ref:
key: "IDENTITY_RECONCILIATION_PROVIDERS_SURF_CLIENT_ID"
client-secret-ref:
key: "IDENTITY_RECONCILIATION_PROVIDERS_SURF_CLIENT_SECRET"
provider-id: "env"
scopes:
- "openid"
- "profile"
- "email"
- "eduid"
user-info-enabled: true

providers:
- id: "surf"
name: "SURFconext"
oidc-client-id: "surf-oidc"
identifier-attribute-name: "sub"
enabled: true

Configuration breakdown

OIDC client (oidc-clients.surf-oidc):

  • discovery-url: The OIDC discovery endpoint for SURFconext. The portal fetches this document to discover the authorization endpoint, token endpoint, JWK set URL, and supported features. The discovery document is cached to avoid repeated network calls.
  • client-id-ref: A reference to the client ID. The key field names an environment variable that contains the actual client ID. This keeps credentials out of the configuration file.
  • client-secret-ref: A reference to the client secret. The provider-id: "env" indicates that the secret is stored in an environment variable named by the key field. Other provider types (e.g., vault, secrets manager) could be supported.
  • scopes: The OIDC scopes to request during the authorization flow. openid is mandatory. profile and email provide standard identity claims. eduid is a SURFconext-specific scope that provides the eduID identifier.
  • user-info-enabled: When true, the portal calls the userinfo endpoint after token exchange to retrieve additional claims. Some providers include all claims in the ID token; others require a separate userinfo call for certain attributes.

Provider (providers[0]):

  • id: The provider identifier referenced by selector rules' plan templates (e.g., provider-id: "surf").
  • name: A human-readable display name for the provider.
  • oidc-client-id: References the OIDC client configuration defined above.
  • identifier-attribute-name: Which claim from the provider's response is used as the institutional identifier. This claim is hashed with Key B to create the SUBJECT_ID-type identity_match record (if the material profile includes a provider_subject material). For SURFconext, this is typically "sub".
  • enabled: Whether this provider is active. Disabled providers cannot be used by selector rules.

Security Properties

Reconciliation sessions incorporate multiple layers of security to protect against common attacks on OIDC authorization code flows.

PKCE (Proof Key for Code Exchange)

The code verifier is generated on the server side and stored encrypted in the session record. The code challenge (S256 hash of the verifier) is sent to the authorization endpoint. During token exchange, the original verifier is sent to the token endpoint. This prevents authorization code interception attacks: even if an attacker intercepts the authorization code from the callback URL, they cannot exchange it for tokens without the code verifier, which never leaves the server.

State parameter (CSRF protection)

The state parameter is a cryptographically random string unique to each session. It is included in the authorization URL and validated on the callback. This prevents cross-site request forgery attacks where an attacker tricks a user's browser into completing an authorization flow initiated by the attacker. Without a matching state parameter, the callback is rejected.

Nonce (ID token replay protection)

The nonce is a cryptographically random string included in the authorization request and expected in the returned ID token's claims. This prevents ID token replay attacks: an attacker cannot reuse an ID token from a previous session because the nonce will not match the current session's expected value.

Session isolation

Each reconciliation session has its own state, nonce, and code verifier. There is no shared state between sessions. This prevents session confusion attacks where an attacker attempts to mix parameters from different sessions. The session ID, state parameter, and nonce are all independently generated random values, making it computationally infeasible to correlate or substitute them.

Short TTL

The default 5-minute TTL limits the window during which a session is valid. An abandoned session (where the user started but did not complete the flow) becomes unusable after 5 minutes, reducing the risk of stale authorization codes being replayed or sessions being hijacked after the user has walked away.