# 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](../federation/wallet-credentials.md) — 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](../federation/eudi-wallet.md#verify-only-step-up). **Nothing is released unless an administrator opts the application in.** (Preview, not yet tested with a real national wallet.)

!!! warning "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`](terraform.md).

| 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](#lifetime)) |
| `apiVerification`, `apiPredicateClaims` | for the [verification API](verification-api.md) only |
| `useStoredAttributes` | default off: when the sign-in has no wallet evidence, release from the user's [verified attributes kept on the profile](../federation/wallet-step-up-and-profile.md#keeping-verified-attributes) (marked `"verification_process": "helix:stored"`). Terraform `use_stored_attributes` |

```bash
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:

```json
{"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

```json
"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:

```bash
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](../federation/digid.md#releasing-the-bsn-to-an-application)), **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`:

```bash
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

- [Verification API](verification-api.md) — verify attributes without a sign-in
- [Credentials beyond the PID](../federation/wallet-credentials.md) · [EU Digital Identity Wallet](../federation/eudi-wallet.md)
- [Client scopes & claims](../manage/scopes-and-claims.md)
