# Wallet step-up and profile

Settings for a [verify-only wallet connection](eudi-wallet.md#verify-only-step-up) that control how long a wallet step-up lasts, whether a signed-in user can step up without signing in again, whether an unlinked wallet can be bound to the user by matching profile attributes, and whether verified attributes are kept on the profile. (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. **Everything on this page is off by default.**

## Setting it up

Per verify-only connection: **Identity providers → the connection → Step-up & profile**, the admin API, or Terraform `helix_wallet_profile_settings`. Writes need **realm-admin**; reads need `view-identity-providers`. An unknown alias is `404`, a sign-in connection `400`. The settings are deleted with the connection and the realm, and are not part of realm export / import.

| Field | Meaning |
| --- | --- |
| `stepUpLifetimeSeconds` | 60–86400. How long a step-up of this connection satisfies `acr_values`. Unset: until the session ends |
| `matchEnabled`, `matchRules` | Bind a wallet that is linked to nobody, for a user without a wallet link, when every rule (`{claim, attribute}`, 2–6 rules) matches |
| `keepEnabled`, `keepClaims`, `keepRetentionDays` | Keep these verified claims on the profile for 1–3650 days (default 365) after the verification |

```bash
curl -s -b cookies.txt -c cookies.txt "$HELIX_URL/admin/realms/$REALM/wallet-profile" >/dev/null
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 '{"stepUpLifetimeSeconds": 900,
       "matchEnabled": true,
       "matchRules": [{"claim": "family_name", "attribute": "lastName"},
                      {"claim": "given_name", "attribute": "givenName"},
                      {"claim": "birthdate", "attribute": "birthdate"}],
       "keepEnabled": true, "keepClaims": ["nationalities"], "keepRetentionDays": 365}' \
  "$HELIX_URL/admin/realms/$REALM/wallet-profile/eudi-wallet-verify"
```

The `attribute` names are your realm's own profile attribute names (here `lastName`, `givenName`, `birthdate`); only attributes an administrator, SCIM or an import set are used.

`GET /admin/realms/{realm}/wallet-profile` lists the settings of every verify-only connection; `GET` / `DELETE …/wallet-profile/{alias}` read or remove one. In the console, **Reset to defaults** on the **Step-up & profile** tab is that `DELETE` (confirmed, audited); kept attributes stay until they expire.

```hcl
resource "helix_wallet_profile_settings" "verify" {
  alias                    = "eudi-wallet-verify"
  step_up_lifetime_seconds = 900
  match_enabled            = true
  match_rules = [
    { claim = "family_name", attribute = "lastName" },
    { claim = "given_name", attribute = "givenName" },
    { claim = "birthdate", attribute = "birthdate" },
  ]
  keep_enabled        = true
  keep_claims         = ["nationalities"]
  keep_retention_days = 365
}
```

## Step-up without the first factor

An application that needs the wallet for a signed-in user sends `acr_values=http://eidas.europa.eu/LoA/high`. When the session's `acr` does not satisfy it and the client's login flow has a **top-level REQUIRED** wallet verification step, the user goes straight to that step:

- no password again, the same SSO session (`sid`) and the same `auth_time` (`max_age` keeps measuring the last full sign-in);
- the wallet must be the user's (its link, or an [attribute match](#binding-a-wallet-by-matching-attributes)); another person's wallet is refused and audited, and the session keeps its `acr`;
- once the step completes, the authorization request is resumed once, even if the wallet's level still does not meet `acr_values`, so it cannot loop.

**Still a full sign-in:** `prompt=login`, an expired `max_age` and the realm's maximum session lifetime force a full sign-in (with the client's flow). `prompt=none` never steps up. An administrator's impersonation answers `login_required`. A client whose flow has no wallet step still gets one full sign-in, as before.

**Behaviour change:** a signed-in user asked for `acr_values` by a client whose flow has the wallet step is no longer sent to the sign-in page first.

## Step-up lifetime

Without a lifetime, a raised `acr` stays for the SSO session until the next login. With `stepUpLifetimeSeconds`, after that time the session's `acr` is again what it was without the step-up (for example an eID's `substantial`, or none) — also after several step-ups in a row, which keep the original level as their base. New codes carry that level, and `acr_values` asks the wallet again; the user stays signed in. A refreshed ID token keeps the `acr` its authorization was issued with: relying parties that need a fresh step-up send `acr_values` again.

## Binding a wallet by matching attributes

For a user without a wallet link, a wallet that is linked to nobody can be bound when **every** rule matches: a PID claim (one the connection requests) must equal the profile attribute after normalisation (Unicode NFKC, case-folded, trimmed, inner white space collapsed; no accent folding, nothing fuzzy). Rules need at least two **distinct** profile attributes.

**Only values an administrator set count.** Every profile attribute records who set its current value — `ADMIN` (admin API or console), `SCIM`, `IMPORT`, `SELF` (account console), `REGISTRATION` (sign-up) or `FEDERATION_MAPPER` (an identity provider) — bound to a keyed hash of that value. Matching trusts only `ADMIN`, `SCIM` and `IMPORT`, and only while the value is unchanged. So someone who signs up with another person's name and birth date and relays the step-up QR code to that person gets no link. Attributes from before the upgrade have no recorded source and are untrusted ([upgrade note](../operations/upgrade-rc13-1.1.0-rc1.md#4-profile-attributes-record-who-set-them)).

Refused, and audited as `WALLET_ATTRIBUTE_MATCH` with a `reason` (names only, never values):

| Reason | When |
| --- | --- |
| `PARTIAL` | fewer than two rules, or a rule that differs or is missing |
| `BSN` | a BSN-shaped name or value |
| `UNTRUSTED_SOURCE` | an attribute not set by an administrator, SCIM or an import |
| `SELF_EDITABLE` | a profile attribute users may edit themselves |
| `PRIVILEGED` | an administrator account: administrators are never bound by attributes |
| `AMBIGUOUS` | another account of the realm also matches every rule |
| `ALREADY_LINKED` | the user already has a wallet link at the sign-in connection |
| `NO_SUBJECT` | a PID without an administrative number when the sign-in connection does not accept the attribute tuple |

**The user confirms.** On a match the user sees **"Link this wallet to your account?"** with the wallet's name, and confirms or declines (audited `WALLET_ATTRIBUTE_MATCH_CONFIRMED` / `WALLET_ATTRIBUTE_MATCH_DECLINED`). Only after confirming, and only once the login or step-up completes, is the wallet linked at the linked **sign-in** connection, with provenance `WALLET_MATCH`. Such a link never signs in an administrator until a realm admin confirms it, and never proves an administrator's wallet at a step-up. A match is never combined with a [transaction confirmation](wallet-transaction-confirmation.md). The user gets the "wallet linked" e-mail.

## Keeping verified attributes

With `keepEnabled`, on a successful step the claims covered by `keepClaims` (PID claims, a credential id or `<credential>.<claim>`) are stored once the login or step-up completes: sealed (AES-256-GCM under a key derived from `DB_ENCRYPTION`, bound to realm, user and name), with the connection, credential, issuer, level, verification time and expiry. Never a BSN-shaped claim, never a derived `age_over_NN`. A newer verification replaces a claim's value.

- **Applications:** a client whose [verified-claims policy](../integration/verified-claims.md) has `useStoredAttributes` (console: "Also use verified details kept on the profile"; Terraform `use_stored_attributes`) gets, when the sign-in has no wallet evidence, its release from the user's latest stored verification — only what the policy allows and the client asks for, only within the policy's max age counted from the original verification, marked `"verification_process": "helix:stored"` with the original `time`. Each delivery re-checks that the switch is still on and that the stored verification still holds every released claim.
- **The user:** account console → **Verified by wallet** shows the values, source, verification time and expiry, with **Delete** (one or all; audited `ACCOUNT_VERIFIED_ATTRIBUTES_DELETED`). Not shown or deletable in an impersonation.
- **Administrators:** Users → a user → **Verified attributes** (`GET /admin/realms/{realm}/users/{userId}/verified-attributes`, `view-users`) shows names, source and times. Values only through `POST …/verified-attributes/reveal` with `reveal-bsn` (audited `VERIFIED_ATTRIBUTES_REVEALED`). `DELETE …/verified-attributes` (optionally `?name=`) needs `manage-users`.
- **Retention and GDPR:** the data-retention job deletes expired values; the user's GDPR export includes `verifiedAttributes` (not in an impersonation; names only in an administrator's export); anonymisation and user deletion remove them.

## Not in this release

A realm-wide default step-up lifetime; keeping attributes from a wallet *sign-in* connection; step-up without the first factor for the essential-claims step-up of [verified data](../integration/verified-claims.md) and for PAR requests.

## See also

- [EU Digital Identity Wallet — verify-only](eudi-wallet.md#verify-only-step-up)
- [Verified data for applications](../integration/verified-claims.md) · [Account console](../integration/account-console.md)
- [Upgrading from rc.13 to 1.1.0-rc.1](../operations/upgrade-rc13-1.1.0-rc1.md)
