Skip to main content

IDV Reconciliation API

When a wallet holder presents a verifiable credential but has no existing identity binding in the system, the Auth Bridge cannot resolve their institutional identity. The IDV (Identity Verification) Reconciliation API handles this gap by orchestrating an out-of-band verification flow -- typically an OIDC authentication against an institutional identity provider like SURFconext -- that establishes the link between the wallet credential and the institutional identity.

All IDV endpoints are scoped to a specific OID4VP session and live under the base path /auth/oid4vp/sessions/{sessionId}/idv on the Auth Bridge (port 8090). The one exception is the OIDC callback endpoint, which uses a fixed path that the external identity provider redirects to.

POST /auth/oid4vp/sessions/{sessionId}/idv/initiate

Starts the identity verification flow by generating an OIDC authorization request for the configured identity provider (e.g., SURFconext). The Auth Bridge creates a PKCE challenge, generates state and nonce parameters, and returns a fully formed authorization URL that the frontend should redirect the user's browser to.

Path Parameters

ParameterTypeDescription
sessionIdstring (UUID)The OID4VP session that requires identity verification. The session must be in VERIFIED or IDV_REQUIRED status.

Response (200 OK)

{
"reconciliationSessionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"authorizationUrl": "https://connect.test.surfconext.nl/oidc/authorize?client_id=portal-auth-bridge&redirect_uri=https%3A%2F%2Fauth-bridge.example.com%2Fauth%2Foid4vp%2Fidv%2Fcallback&response_type=code&scope=openid+profile+email+eduid&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&state=xyz123&nonce=abc456",
"providerId": "surf"
}
FieldTypeDescription
reconciliationSessionIdstring (UUID)A unique identifier for this reconciliation attempt. Used internally to correlate the OIDC callback with the correct OID4VP session.
authorizationUrlstringThe complete OIDC authorization URL. The frontend must redirect the user's browser to this URL. The user will authenticate at SURFconext (or whichever provider is configured), and the provider will redirect back to the Auth Bridge callback endpoint.
providerIdstringAn identifier for the identity provider being used. Currently always "surf" for the SURFconext integration.

What the Frontend Should Do

After receiving this response, the frontend should redirect the user's browser to the authorizationUrl. This takes the user away from the portal and into the SURFconext login flow. The frontend should persist the sessionId and reconciliationSessionId (e.g., in session storage or in the URL state) so it can resume polling after the user returns.

const initiateIdv = async (sessionId: string) => {
const response = await fetch(`/api/wallet/sessions/${sessionId}/idv/initiate`, {
method: 'POST',
});

const data = await response.json();

// Persist session info for after the redirect
sessionStorage.setItem('idv_session_id', sessionId);
sessionStorage.setItem('idv_reconciliation_id', data.reconciliationSessionId);

// Redirect the browser to the identity provider
window.location.href = data.authorizationUrl;
};

Internal PKCE and State Management

The Auth Bridge generates a fresh PKCE code_verifier and code_challenge pair for each IDV initiation. The code_verifier is stored server-side alongside the reconciliation session and is never exposed to the frontend. The state parameter is a signed, opaque token that encodes the OID4VP session ID and reconciliation session ID, ensuring that the callback can be matched to the correct session even if multiple reconciliations are in progress simultaneously.

GET /auth/oid4vp/idv/callback

This endpoint is called by the external identity provider (SURFconext) after the user has authenticated -- it is not called by the frontend. The identity provider redirects the user's browser here with an authorization code and state parameter.

Query Parameters

ParameterTypeDescription
codestringThe authorization code issued by the identity provider.
statestringThe state parameter that was sent in the authorization request. Contains the encrypted session correlation data.

Processing Steps

When the callback arrives, the Auth Bridge performs the following sequence:

  1. Validate state: Decrypts and verifies the state parameter to recover the OID4VP session ID and reconciliation session ID. Rejects the request if the state is invalid, expired, or has already been used (replay protection).

  2. Exchange code for tokens: Sends the authorization code to the identity provider's token endpoint, along with the stored PKCE code_verifier, the client_id, and the client_secret. This exchange happens server-to-server, never through the browser.

  3. Validate ID token: Verifies the signature of the returned ID token against the identity provider's JWKS endpoint. Checks the iss, aud, exp, nonce, and other standard claims.

  4. Extract claims: Reads the identity claims from the ID token (and optionally from the UserInfo endpoint). The critical claims for reconciliation are:

    • eduid -- the user's eduID identifier
    • eduperson_principal_name -- the institutional principal name
    • email -- the user's email address
    • sub -- the identity provider's subject identifier
  5. Create identity records: Stores the extracted claims as a new identity record and creates a binding that links the wallet credential's holder DID to this institutional identity.

  6. Encrypt resolved identity: Packages the resolved identity into an encrypted token that the session completion endpoint will use to construct the final claims.

  7. Redirect the user: After processing, the Auth Bridge redirects the user's browser back to the portal frontend, typically to a page that resumes the wallet authentication flow.

Callback Redirect

After successful processing, the browser is redirected to:

https://portal.example.com/wallet/callback?session={sessionId}&status=success

On error, the redirect includes an error indicator:

https://portal.example.com/wallet/callback?session={sessionId}&status=error&reason=token_exchange_failed

GET /auth/oid4vp/sessions/{sessionId}/idv/status

Checks the current status of the IDV reconciliation flow. The frontend polls this endpoint after the user returns from the identity provider to determine whether the reconciliation completed successfully.

Path Parameters

ParameterTypeDescription
sessionIdstring (UUID)The OID4VP session ID.

Response (200 OK)

{
"reconciliationStatus": "COMPLETED",
"errorMessage": null
}
FieldTypeDescription
reconciliationStatusstring (enum)The current status of the IDV reconciliation. See the status table below.
errorMessagestring or nullA human-readable error description when the status is ERROR. Null for all other statuses.

IDV Status Values

StatusDescription
CREATEDThe IDV reconciliation has been initiated but the user has not yet been redirected to the identity provider.
REDIRECTEDThe user's browser has been redirected to the identity provider's authorization endpoint. The Auth Bridge is waiting for the callback.
CALLBACK_RECEIVEDThe identity provider has redirected back to the callback endpoint and the Auth Bridge is processing the authorization code.
COMPLETEDThe identity verification succeeded. The claims were extracted, identity records were created, and the wallet-to-identity binding was established. The OID4VP session can now be completed.
ERRORThe identity verification failed. Check errorMessage for details. Common causes include token exchange failure, invalid ID token, or missing required claims.

Polling Example

After the user returns from SURFconext, the frontend should poll the IDV status before attempting to complete the session:

const waitForIdvCompletion = async (sessionId: string): Promise<void> => {
const MAX_ATTEMPTS = 30;
const POLL_INTERVAL_MS = 2000;

for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
const response = await fetch(`/api/wallet/sessions/${sessionId}/idv/status`);
const data = await response.json();

if (data.reconciliationStatus === 'COMPLETED') {
return; // IDV succeeded, session can be completed
}

if (data.reconciliationStatus === 'ERROR') {
throw new Error(`IDV failed: ${data.errorMessage}`);
}

await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
}

throw new Error('IDV polling timed out');
};

POST /auth/oid4vp/sessions/{sessionId}/idv/submit

Submits the reconciliation for final processing. This endpoint is used when manual confirmation is required -- for example, when the system detects a potential match but wants the user to confirm that the institutional identity belongs to them before creating the binding.

Path Parameters

ParameterTypeDescription
sessionIdstring (UUID)The OID4VP session ID.

Request Body

{
"confirmed": true,
"selectedMatchId": "optional-match-id-if-multiple-candidates"
}
FieldTypeRequiredDescription
confirmedbooleanYesWhether the user confirms the identity match. If false, the reconciliation is aborted and a new IDV attempt can be started.
selectedMatchIdstringNoWhen multiple potential identity matches are found, this field specifies which one the user has selected.

Response (200 OK)

{
"reconciliationStatus": "COMPLETED",
"message": "Identity binding created successfully"
}

After a successful submit, the OID4VP session status transitions to COMPLETED and the session's complete endpoint will return a 200 response with full identity claims.

Complete IDV Flow Walkthrough

The following sequence describes the entire IDV reconciliation flow from the frontend's perspective, from the moment the wallet session reports that IDV is required to the point where the user is fully authenticated.

const handleWalletLoginWithIdv = async () => {
// Step 1: Create OID4VP session and display QR code
const session = await createWalletSession('portal-eduid-vc');
displayQrCode(session.qrCodeDataUri);

// Step 2: Poll until session reaches a terminal status
const status = await pollSessionStatus(session.sessionId);

if (status.status === 'VERIFIED' && !status.idvRequired) {
// Happy path: known holder, complete immediately
const result = await completeSession(session.sessionId);
onAuthenticated(result);
return;
}

if (status.status === 'VERIFIED' && status.idvRequired) {
// Unknown holder: start IDV reconciliation
showMessage('Please authenticate with your institution to link your wallet.');

// Step 3: Initiate IDV and redirect to SURFconext
const idv = await initiateIdv(session.sessionId);
// This redirects the browser away from the portal
window.location.href = idv.authorizationUrl;
return; // Browser navigates away here
}

// Handle other terminal states
if (status.status === 'EXPIRED') {
showError('Session expired. Please try again.');
} else if (status.status === 'ERROR') {
showError('Wallet verification failed. Please try again.');
}
};

// This function runs when the user returns from SURFconext
const handleIdvCallback = async () => {
const sessionId = sessionStorage.getItem('idv_session_id');
if (!sessionId) {
showError('Session context lost. Please start over.');
return;
}

// Step 4: Wait for IDV to complete
try {
await waitForIdvCompletion(sessionId);
} catch (error) {
showError(`Identity verification failed: ${error.message}`);
return;
}

// Step 5: Complete the OID4VP session (now with resolved identity)
const result = await completeSession(sessionId);
onAuthenticated(result);
};

Error Handling

SURFconext Authentication Failure

If the user cancels the SURFconext login or the authentication fails at the identity provider, the callback endpoint receives an error response instead of an authorization code. The Auth Bridge sets the IDV status to ERROR with a descriptive message:

{
"reconciliationStatus": "ERROR",
"errorMessage": "Identity provider authentication failed: user_cancelled"
}

The frontend should display an appropriate message and offer the user the option to retry the IDV flow from the beginning.

Session Expiry During IDV

OID4VP sessions have a configurable timeout (default: 5 minutes). If the user takes too long at the SURFconext login, the underlying OID4VP session may expire. When this happens, the IDV callback will fail because the session is no longer valid:

{
"reconciliationStatus": "ERROR",
"errorMessage": "OID4VP session has expired. Please start a new wallet authentication."
}

To mitigate this, the session timeout should be configured generously enough to account for the time the user spends at the identity provider. A timeout of 10 to 15 minutes is recommended for deployments that use IDV reconciliation.

Attribute Mapping Failure

The Auth Bridge expects certain claims from the identity provider's ID token (at minimum, eduid or sub). If these claims are missing, the reconciliation fails:

{
"reconciliationStatus": "ERROR",
"errorMessage": "Required claim 'eduid' not present in identity provider response"
}

This typically indicates a misconfiguration in the OIDC scope or a change in the identity provider's claim mapping. The error should be escalated to an administrator.

Duplicate Binding Detection

If the institutional identity extracted during IDV is already bound to a different wallet, the Auth Bridge raises a conflict:

{
"reconciliationStatus": "ERROR",
"errorMessage": "Institutional identity is already bound to a different wallet holder"
}

This situation requires manual intervention or an explicit unbinding of the previous wallet before the new binding can be created.