IdentityLinkBinding Record
While identity_match is the index, identity_link_binding is the full dossier. It stores the encrypted canonical attributes captured during reconciliation, assurance metadata about how the identity was verified, and version tracking fields for staleness detection. This is the record that enables the wallet fast-path: when a known holder returns, the system decrypts the cached attributes from the binding and projects them directly into the OIDC token without contacting any external provider.
The binding is created during the reconciliation flow, when the system links a wallet holder identity to an institutional identity. At that moment, the canonical attributes selected by the attribute rules (name, email, affiliation, eduID, and others) are serialized to JSON, encrypted as a single AES-256-GCM blob using Key C, and stored in the persisted_attributes_envelope field. From that point forward, the system can serve returning wallet users entirely from its local database.
Why Bindings Exist
The primary motivation for bindings is the fast-path optimization. Without bindings, every wallet login would require a real-time round-trip to SURFconext (or another OIDC provider) to fetch the user's institutional attributes. This introduces several problems:
- Latency: A round-trip to an external OIDC provider adds multiple seconds to the authentication flow. Wallet users expect near-instant authentication, especially on mobile devices where network conditions may be poor.
- Availability dependency: If SURFconext is down or experiencing degraded performance, wallet authentication would fail entirely. This creates a runtime dependency on an external system that the portal does not control.
- User experience: The reconciliation flow requires user interaction (logging in through the institutional IdP). Requiring this on every wallet login would defeat the purpose of wallet-based authentication.
With bindings, the first reconciliation caches everything needed. The user logs in through SURFconext once, the system captures and encrypts the canonical attributes, and all subsequent wallet logins are resolved entirely locally. The result is sub-second identity resolution with no external dependencies after the initial reconciliation.
Record Structure
The identity_link_binding table contains the following columns. Each record represents a complete holder-to-institution mapping with all associated metadata.
| Column | Type | Encryption | Description |
|---|---|---|---|
id | TEXT (UUID) | -- | Primary key. A unique identifier for this binding record. |
tenant_id | TEXT | -- | Multi-tenant isolation key. All queries include a tenant filter. |
match_id | TEXT (FK) | -- | Foreign key to identity_match.id. Links this binding to the match record that serves as the lookup index. |
holder_identifier_hash | TEXT | HMAC (Key A) | HMAC-SHA256 hash of the wallet holder's key fingerprint (JWK thumbprint). Computed using Key A. |
holder_hash_key_version | INTEGER | -- | Version of Key A used to compute holder_identifier_hash. Used for key rotation tracking. |
institution_identifier_hash | TEXT | HMAC (Key B) | HMAC-SHA256 hash of the institutional identifier (e.g., SURFconext sub claim). Computed using Key B. |
institution_hash_key_version | INTEGER | -- | Version of Key B used to compute institution_identifier_hash. Used for key rotation tracking. |
encrypted_institution_id | TEXT | AES (Key C) | The plaintext institutional identifier (e.g., the actual eduID), encrypted with AES-256-GCM using Key C. This is the only place where the reversible institutional ID is stored. |
encrypted_institution_id_key_version | INTEGER | -- | Version of Key C used to encrypt encrypted_institution_id. Used for key rotation tracking. |
persisted_attributes_envelope | TEXT | AES (Key C) | The canonical claims bundle, serialized to JSON and encrypted as a single AES-256-GCM blob using Key C. This is the primary data payload of the binding. |
provider_id | TEXT | -- | Identifies which reconciliation provider was used to create this binding. For example, "surf" for SURFconext-based reconciliation. Enables the system to support multiple reconciliation providers per tenant. |
institution_id_label | TEXT | -- | A human-readable label for the institution, such as "University of Amsterdam" or "TU Delft". Used for display purposes in administrative interfaces and audit logs. Not encrypted because it does not contain personally identifiable information. |
assurance_summary | TEXT (JSON) | -- | Authentication assurance metadata captured during reconciliation. Contains the wallet assurance level, OIDC ACR and AMR values, and evidence references. Stored as a JSON blob. |
canonical_schema_version | TEXT | -- | Version identifier for the canonical attribute schema that was in effect when this binding was created. When the attribute rules change, this version is compared against the current configuration to detect stale bindings. |
material_profile_version | TEXT | -- | Version identifier for the material profile configuration used during reconciliation. Material profiles define which attributes are requested and how they are mapped. |
selector_rule_version | TEXT | -- | Version identifier for the selector rule that triggered the reconciliation. Selector rules determine when and how reconciliation is initiated based on the attributes available in the VP. |
material_fingerprints | TEXT | -- | A hash of the material inputs (attribute values, rule configurations) that were used to create this binding. If the same inputs would produce a different fingerprint today, the binding is stale. |
created_at | TEXT | -- | Timestamp of when this binding was first created. |
updated_at | TEXT | -- | Timestamp of the most recent modification. |
last_used_at | TEXT | -- | Timestamp of the most recent access. Drives inactivity-based cleanup, similar to identity_match.last_used_at. |
reconcile_time | TEXT | -- | Timestamp of the original reconciliation event that created or last refreshed this binding. Distinct from created_at because a binding may be refreshed (re-reconciled) without being recreated. |
Dual-Hash Design
The binding stores both holder_identifier_hash (computed with Key A) and institution_identifier_hash (computed with Key B). This dual-hash design enables bidirectional lookup:
Wallet key to institution binding (forward path). When a wallet holder authenticates, the system computes the HMAC of their key fingerprint with Key A and looks up the binding by holder_identifier_hash. This is the primary fast-path used during wallet login. The system does not need to know anything about the user's institutional identity -- the wallet key alone is sufficient to find the binding and decrypt the cached attributes.
Institution ID to wallet binding (reverse path). When an external system (such as a student information system or a credential issuer) queries the REST API with an institutional identifier, the system computes the HMAC of that identifier with Key B and looks up the binding by institution_identifier_hash. This enables third-party systems to discover whether a given institutional user has a wallet binding, and to retrieve the associated identity information.
Both lookup paths are supported by database indexes and return the same binding record. The dual-hash approach avoids the need for a join through the identity_match table when looking up by institution identifier, keeping the reverse-lookup path fast.
Persisted Attributes Envelope
The persisted_attributes_envelope is the core data payload of the binding. It contains the canonical claims that were selected by the attribute rules during reconciliation, specifically those marked with persist: true in the rule configuration.
Typical contents include:
- eduid: The user's eduID identifier
- eduperson_principal_name: The scoped institutional identifier (e.g.,
user@university.nl) - email: The user's institutional email address
- given_name: The user's first name
- family_name: The user's surname
- eduperson_affiliation: The user's role within the institution (e.g.,
student,employee) - schac_home_organization: The institution's domain identifier
These attributes are serialized to a JSON object, then encrypted as a single AES-256-GCM blob using Key C. The encryption uses a unique nonce for each write, ensuring that two bindings with identical attributes produce different ciphertext.
On read, the system decrypts the blob using Key C, deserializes the JSON, and projects the individual attributes into the OIDC token according to the project rules in the attribute configuration. Not all persisted attributes are necessarily projected into every token -- the projection rules determine which attributes appear in the final response based on the requested scopes and the relying party's configuration.
The canonical_schema_version field tracks which version of the attribute rules was in effect when the envelope was created. When the rules change (for example, when a new attribute is added or an existing attribute's mapping is modified), the schema version is incremented. On read, the system compares the binding's schema version against the current configuration. If they differ, the binding is flagged as stale and will be refreshed on the next reconciliation opportunity.
Assurance Summary
The assurance_summary field is a JSON blob containing authentication assurance metadata captured at the time of reconciliation. This metadata describes the strength and method of the authentication that established the binding.
{
"walletAssuranceLevel": "high",
"oidcAcr": "urn:mace:surfnet.nl:assurance:loa2",
"oidcAmr": ["pwd"],
"executionId": "550e8400-e29b-41d4-a716-446655440000",
"evidenceReferenceHash": "sha256-a1b2c3d4e5f6..."
}
The fields in the assurance summary serve the following purposes:
- walletAssuranceLevel: The assurance level of the wallet itself, as determined by the wallet attestation or the VP metadata. Values such as
"high","substantial", or"low"correspond to eIDAS assurance levels. - oidcAcr: The Authentication Context Class Reference from the OIDC token issued by SURFconext. This indicates the level of assurance of the institutional authentication (e.g., single-factor vs. multi-factor).
- oidcAmr: The Authentication Methods Reference from the OIDC token. This is an array listing the authentication methods used (e.g.,
"pwd"for password,"mfa"for multi-factor authentication). - executionId: A unique identifier for the specific reconciliation execution that created this binding. Useful for audit trail correlation.
- evidenceReferenceHash: A SHA-256 hash of the evidence that was evaluated during reconciliation. This provides a tamper-evident reference to the original evidence without storing the evidence itself.
This metadata enables step-up authentication decisions. If a relying party requires a higher assurance level than what is recorded in the binding's assurance_summary, the system can request re-verification rather than serving the cached attributes. For example, a financial service might require loa3 while the binding was established at loa2, triggering a step-up flow.
Version Tracking and Staleness Detection
The binding includes three version fields and a fingerprint field that together enable the system to detect when a binding's cached data may be outdated.
canonical_schema_version
This version tracks the attribute rules configuration. When the rules that define which claims are canonical, how they are mapped, and which are persisted are updated, the schema version is incremented. A binding whose canonical_schema_version does not match the current configuration may be missing newly added attributes or may contain attributes that have been remapped.
material_profile_version
This version tracks the material profile configuration. Material profiles define the full set of parameters used during reconciliation, including which OIDC scopes to request, which claims to extract, and how to handle optional vs. required attributes. When the material profile changes, bindings created under the old profile may need to be refreshed.
selector_rule_version
This version tracks the reconciliation selector rules. These rules determine the conditions under which reconciliation is triggered (for example, "reconcile when the VP contains a PID credential but no institutional affiliation"). When the rules change, existing bindings may have been created under different triggering conditions and may need re-evaluation.
material_fingerprints
This is a hash of the material inputs (attribute values, rule configurations, profile settings) that were used to create the binding. Unlike the version fields, which track configuration changes, the fingerprint detects changes in the actual data. If the same reconciliation were performed today with the same configuration but different input data (because the user's institutional attributes have changed), the fingerprint would differ.
When any of these versions or the fingerprint do not match the current configuration or expected values, the binding is considered stale. Stale bindings continue to function -- the cached attributes are still served to avoid disrupting the user experience -- but the system flags the binding for refresh on the next reconciliation opportunity. This lazy refresh approach avoids the need for bulk re-materialization when configurations change.