Skip to content

Verified data for applications (verified_claims)

An application can receive what the user's EU Digital Identity Wallet verified in the current sign-in — PID claims, derived age thresholds such as age_over_18, and the claims of other credentials — the standard OIDC way, as OpenID Connect for Identity Assurance verified_claims. The data can come from a wallet sign-in or a verify-only step-up. Nothing is released unless an administrator opts the application in. (Preview, not yet tested with a real national wallet.)

Status: Preview — not yet tested with a real national wallet

New in 1.1.0-rc.1. Tested with the project's own test wallet.

Per-client policy

Console: Applications → the application → Verified data (wallet) (the application needs an OIDC client). Terraform: helix_client_verified_claims.

Field Meaning
enabled the opt-in. No policy, or enabled: false, releases nothing
allowedClaims 1–100 entries: PID claims (given_name, family_name, birthdate, nationalities, place_of_birth, address, their dotted members, issuing_country, issuing_authority), age_over_NN (1–150, derived from the verified birth date, which is not released unless listed), a credential id (qualification: all its claims) or one of its claims (qualification.profession). An entry allows everything below it. The administrative number is never releasable
deliverUserinfo default on: the userinfo response
deliverIdToken default off: the ID token of the code exchange only (never a refreshed one)
deliverAccessToken default off: every access token of the authorization (code and refresh). Every API the token reaches can read them; never a BSN
maxAgeSeconds optional, 60–2592000: how long after the wallet verification the claims may still be released (lifetime)
apiVerification, apiPredicateClaims for the verification API only
useStoredAttributes default off: when the sign-in has no wallet evidence, release from the user's verified attributes kept on the profile (marked "verification_process": "helix:stored"). Terraform use_stored_attributes
curl -s -b cookies.txt -c cookies.txt "$HELIX_URL/admin/realms/$REALM/verified-claims-catalog"
CSRF=$(awk '($6=="__Host-XSRF-TOKEN"||$6=="XSRF-TOKEN")&&$7~/^r1\./{t=$7} END{print t}' cookies.txt)

curl -s -b cookies.txt -X PUT -H "Content-Type: application/json" -H "X-XSRF-TOKEN: $CSRF" \
  -d '{"enabled": true,
       "allowedClaims": ["given_name", "age_over_18", "qualification.profession"],
       "deliverUserinfo": true, "maxAgeSeconds": 900}' \
  "$HELIX_URL/admin/realms/$REALM/verified-claims/citizen-portal"

The catalogue (GET …/verified-claims-catalog) lists what the realm's wallet connections verify: the PID claims, each connection's other credentials and their claims. GET …/verified-claims/{clientId} reads a policy (view-clients), GET …/verified-claims lists them, and DELETE …/verified-claims/{clientId} stops the release (console: Remove policy, confirmed and audited; the dialog names the application).

Who may write a policy. Writing one needs realm-admin: releasing wallet-verified personal data is a privacy decision on the level of the BSN release. It also passes the client-change guards: the caller must hold the client's service-account privileges, a client with a BSN grant needs realm-admin for any change, and the built-in helix-console / helix-cli clients never get a policy. Deleting a client (or its application) deletes its policy, so a client re-created under the same client_id inherits nothing. Policy changes are audited as VERIFIED_CLAIMS_POLICY_UPDATED / VERIFIED_CLAIMS_POLICY_DELETED.

Channels and how an application asks

  • Scope verified_claims (seeded in every realm; register it on the client): everything the policy allows, in every channel the policy enables.
  • The claims parameter (OIDC Core §5.5), for userinfo and/or id_token, wins over the scope for that channel. Released is what the policy allows and the request names:
{"userinfo": {"verified_claims": {
   "verification": {"trust_framework": null},
   "claims": {"given_name": null,
              "age_over_18": {"essential": true, "purpose": "To check you may buy this"},
              "qualification": {"profession": {"value": "doctor"}}}}}}
  • value / values release a claim only when the value that would be released matches (the boolean of an age threshold, a member of a list). A mismatch looks exactly like a claim that is not available, so a birth date cannot be probed through age_over_18.
  • A request whose trust_framework excludes eidas gets nothing.
  • A malformed claims parameter, an empty claims object, a purpose outside 3–300 characters, or more than 16 KiB, 100 claims or 20 values is refused with invalid_request.
  • Essential claims: when an essential claim the policy allows is not in the session's wallet verification and the application's login flow has the wallet verification step, the user signs in again once for that request and confirms with the wallet. Otherwise (also with prompt=none, for an impersonation, a PAR request, or a claim requested only in a channel the policy does not deliver) it is omitted.

What the application receives

"verified_claims": [
  {"verification": {"trust_framework": "eidas", "assurance_level": "high",
                    "time": "2026-10-19T09:12:44Z",
                    "evidence": [{"type": "electronic_record", "check_details": [{"check_method": "vcrypt"}],
                                  "time": "2026-10-19T09:12:44Z",
                                  "record": {"type": "eudi_wallet_pid", "source": {"name": "CN=PID Issuer,C=DE"}}}]},
   "claims": {"given_name": "Erika", "age_over_18": true}},
  {"verification": {"trust_framework": "eidas", "time": "2026-10-19T09:12:44Z",
                    "evidence": [{"type": "electronic_record", "check_details": [{"check_method": "vcrypt"}],
                                  "time": "2026-10-19T09:12:44Z", "record": {"type": "eudi_wallet_eaa"}}]},
   "claims": {"qualification": {"profession": "doctor"}}}
]

One element per credential; a credential's claims sit under its id. assurance_level is the IDA value for the eidas trust framework (low, substantial, high). Dotted PID names are nested (address.country → "address": {"country": …}), and nationalities is a list. Read it from userinfo with the access token:

curl -s -H "Authorization: Bearer $ACCESS_TOKEN" "$HELIX_URL/realms/$REALM/userinfo"

Lifetime

Only the current session's verification is released, and only until that session ends:

  • maxAgeSeconds limits how long after the wallet verification the claims may be released. Unset: the realm's SSO session maximum lifetime, else 10 hours.
  • Independently, nothing is released once the SSO session that verified it has ended: logout, revocation, idle or maximum-lifetime expiry, or a new sign-in in that browser. A later wallet step-up replaces the verification.
  • Every delivery applies the application's current policy: removing a claim or a channel takes effect at the next userinfo call or token refresh; a removed or disabled policy releases nothing.
  • The verification waits in the session and on the authorization, sealed with AES-GCM under a key derived from DB_ENCRYPTION and bound to its realm, client, authorization and channel. It is never written to the account. The stored authorization keeps no verified_claims in its token claims, so token introspection does not return them; the signed token values the application holds still carry them.
  • Only for the authorization-code grant or its refresh, only to the client the authorization belongs to, never for an impersonation. Token exchange (an agent acting for the user), client credentials, device, CIBA and every other grant get none.

BSN rules

A released claim counts as a BSN when its name contains bsn, burgerservicenummer, citizen_service_number or sofi or names a national person identifier, or when a value contains the DigiD sector form, an eIDAS NL/xx/<bsn>, or any 8- or 9-digit sequence (per token, raw, and without spaces, dots or hyphens) that passes the 11-proef — only a valid ISO date is skipped.

Such a claim goes only to userinfo, only while the client has a BSN grant (the application's BSN release setting), never into a token. The check fails closed: long numbers such as IBANs or phone numbers may be withheld from tokens.

Discovery

The realm's OpenID configuration advertises claims_parameter_supported, verified_claims_supported, trust_frameworks_supported (eidas), evidence_supported (electronic_record), electronic_records_supported, claims_in_verified_claims_supported (the PID claims and age_over_18), acr_values_supported (the eIDAS levels; high is the wallet and wallet step-up), and acr / verified_claims in claims_supported:

curl -s "$HELIX_URL/realms/$REALM/.well-known/openid-configuration" \
  | jq '{verified_claims_supported, trust_frameworks_supported, claims_in_verified_claims_supported}'

Changed for verify-only connections

A client with an enabled policy no longer gets the verify-only connection's verify.releasedClaims ID-token release; its policy governs, in userinfo by default. Clients without a policy are unchanged, and the verify-only release now also uses the IDA value high as assurance_level instead of the LoA URI.

Audit

Event When Detail
VERIFIED_CLAIMS_RELEASED (actor: the user) each delivery: the ID token, an access token, a userinfo response clientId, channel, claims (names), credentials, legalBasis (when a BSN was released) — never a value
VERIFIED_CLAIMS_POLICY_UPDATED / _DELETED a policy change clientId, enabled, allowedClaims, channels

Not in this release

Showing purpose on the consent page; the essential-claim step-up for PAR requests; requesting individual verification / evidence sub-elements; verified claims over SAML; listing a realm's credential claims in discovery; verified-claims policies in realm export / import.

See also