# DigiD (NL Citizen eID)

Sign in Dutch citizens with **DigiD** — the national citizen eID. The connector ships with HelixIAM and is configurable from the console; going live needs your own accepted DigiD connection.

## What it is

DigiD is the Netherlands' citizen electronic identity. With HelixIAM the DigiD **connector is built in** — there is no add-on to install. You add it like any other identity provider, and the realm can authenticate citizens with their DigiD identity, with the resulting user [provisioned just-in-time](identity-providers.md) and mapped into your realm.

!!! note "You bring the DigiD agreement"
    The connector is included, but DigiD is a regulated scheme: to connect to the DigiD **pre-production (Preprod)** or **production** environment you need your own accepted DigiD connection — directly or through a recognised broker — along with the environment's trust material (certificates, metadata, and a minimum level of assurance). Helix supplies the protocol connector and facilitated defaults; you supply the agreement and trust material.

DigiD is one of HelixIAM's flagship differentiators — a built-in EU eID connector that most identity products leave you to build yourself.

DigiD is an [identity provider](identity-providers.md) with `protocol: "digid"`. It builds on the SAML 2.0 SP profile but adds the eID-specific material: an SP decryption key (the assertion — and thus the **BSN** — is encrypted to the SP), an SP signing key (the AuthnRequest must be XML-signed), and a minimum level of assurance.

**Binding support.** Helix supports the classic SAML2 **HTTP-Artifact** binding — back-channel **ArtifactResolve** over mutual-TLS SOAP — as well as **HTTP-POST**, selectable per connection so you can match the profile your environment requires. With the artifact binding, the back-channel `ArtifactResponse` must carry a valid signature by the IdP's metadata signing key: an unsigned response is refused, and so is any response when the connection has no IdP signing certificate (`idpSigningCertificate`).

## In the console

1. Open **Federation & eIDs → Identity providers** and click **Add identity provider**.
2. Choose **DigiD**. This applies **facilitated defaults** for the scheme, so most of the connection is pre-shaped and you supply only your environment-specific details.
3. Supply your environment-specific trust material and service details.
4. Select the **binding** — HTTP-Artifact (with the ArtifactResolve back-channel over mutual TLS) or HTTP-POST — to match your connection profile.
5. Configure **attribute mappers** for any other attributes you consume. The BSN itself never goes through a mapper:
   see [How the BSN is handled](#how-the-bsn-is-handled).
6. Save; the provider loads at runtime and appears as a DigiD sign-in option.

!!! note "Operational note — production accreditation"
    The DigiD connector is **built in and configurable now**. Connecting to **production**, however, is a formal scheme process: admission and accreditation are arranged through **Logius**, and include a **mandatory annual security assessment**. Plan that accreditation track separately from the technical configuration, which you can complete immediately.

## Over the API

DigiD is created like any [identity provider](identity-providers.md), with `protocol: "digid"`. The examples below assume you have set `$HELIX_URL` / `$REALM` and logged in — see [Authenticating to the API](../getting-started/api-authentication.md).

### Create a DigiD connector

The `config` map carries the SAML-SP endpoints and the eID trust material. Set `responseBinding` to `artifact` for the classic DigiD "Koppelvlak SAML" back-channel resolve, or `post` for the modern front-channel profile.

```bash
# Refresh CSRF after any GET, then POST (see the auth guide for the helper)
curl -s -b cookies.txt -c cookies.txt "$HELIX_URL/admin/realms/$REALM/identity-providers" >/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 \
  -H "Content-Type: application/json" \
  -H "X-XSRF-TOKEN: $CSRF" \
  -d '{
        "alias": "digid",
        "protocol": "digid",
        "displayName": "DigiD",
        "enabled": true,
        "config": {
          "ssoUrl": "https://digid.example/saml/idp/request_authentication",
          "idpEntityId": "https://digid.example/saml/idp/metadata",
          "spEntityId": "https://auth.example.com/realms/acme/digid",
          "assertionConsumerServiceUrl": "https://auth.example.com/realms/acme/broker/digid/callback",
          "idpSigningCertificate": "-----BEGIN CERTIFICATE-----\n…\n-----END CERTIFICATE-----",
          "spDecryptionPrivateKey": "-----BEGIN PRIVATE KEY-----\n…\n-----END PRIVATE KEY-----",
          "spSigningPrivateKey": "-----BEGIN PRIVATE KEY-----\n…\n-----END PRIVATE KEY-----",
          "spSigningCertificate": "-----BEGIN CERTIFICATE-----\n…\n-----END CERTIFICATE-----",
          "minimumLoa": "loa3",
          "subjectAttribute": "bsn",
          "responseBinding": "artifact",
          "artifactResolutionServiceUrl": "https://digid.example/saml/idp/resolve_artifact"
        }
      }' \
  "$HELIX_URL/admin/realms/$REALM/identity-providers"
```
```json
{
  "realmId": "acme",
  "alias": "digid",
  "protocol": "digid",
  "displayName": "DigiD",
  "enabled": true,
  "config": {
    "ssoUrl": "https://digid.example/saml/idp/request_authentication",
    "idpEntityId": "https://digid.example/saml/idp/metadata",
    "spEntityId": "https://auth.example.com/realms/acme/digid",
    "assertionConsumerServiceUrl": "https://auth.example.com/realms/acme/broker/digid/callback",
    "minimumLoa": "loa3",
    "subjectAttribute": "bsn",
    "responseBinding": "artifact",
    "artifactResolutionServiceUrl": "https://digid.example/saml/idp/resolve_artifact",
    "spDecryptionPrivateKeySet": true,
    "spSigningPrivateKeySet": true
  }
}
```

The SP private keys are write-only: the response reports `spDecryptionPrivateKeySet` / `spSigningPrivateKeySet` instead of the keys — see [secrets are write-only](identity-providers.md#secrets-are-write-only).

!!! note "Config keys"
    SAML-SP endpoints (`ssoUrl`, `idpEntityId`, `spEntityId`, `assertionConsumerServiceUrl`, `idpSigningCertificate`) plus the eID material: `spDecryptionPrivateKey`, `spSigningPrivateKey`, `spSigningCertificate`, `minimumLoa` (e.g. `loa3`; see [Levels of assurance](#levels-of-assurance)), `subjectAttribute` (the attribute that carries the BSN, when it is not the NameID), `authnRequestBinding` (`post`/`redirect`), `responseBinding` (`post`/`artifact`), and `artifactResolutionServiceUrl` when the response binding is `artifact`. A wizard connection may instead carry `metadataUrl` / `metadataXml`.

## How the BSN is handled

Under the Wabb and article 87 of the UAVG, only an organisation with a legal basis may process a citizen service
number (BSN). Helix keeps it away from everything that does not need it:

- **Never the username.** A DigiD sign-in provisions an account with a non-identifying username, `digid-` plus 12
  random hex characters (template `HELIX_BSN_USERNAME_TEMPLATE`, default `{alias}-{random}`; it has no placeholder
  that could carry the BSN). `preferred_username` therefore never contains a BSN; `sub` is the internal user id.
- **Linked by a keyed hash.** The federated link is keyed on `HMAC-SHA256(realm pepper, BSN)`, not the BSN; a returning
  citizen is found by that hash. The pepper is the realm's secret (stored encrypted with `DB_ENCRYPTION`), so DigiD
  sign-in needs `DB_ENCRYPTION`.
- **Stored encrypted, per realm.** The number itself is kept in its own table per realm and user, encrypted with
  `DB_ENCRYPTION` (AES-GCM), outside the profile attributes. A BSN collected in one realm is never shown in, or released
  by, another realm the same user belongs to.
- **Only from a trusted source.** A BSN is recorded only from the subject of a DigiD connection or of an eIDAS
  connection with a Dutch PersonIdentifier, or from a provider a realm administrator explicitly marked as a BSN source
  (**Identity providers → (provider) → Settings → Trust this provider as a source of BSNs**, config key
  `bsnAuthoritative`; needs `realm-admin`, audited as `IDP_BSN_AUTHORITATIVE_CHANGED`, never carried in by a realm
  import). While a provider is marked, repointing it (issuer, endpoints, JWKS, metadata, certificates, client
  credentials, trusted lists, protocol, enabled) also needs `realm-admin`, through the API, the console or an import. From any other provider a BSN-shaped value is removed but never stored. A recorded BSN is never overwritten
  by a different one: that sign-in is refused and `BSN_CONFLICT` is audited.
- **No BSN-shaped usernames.** A username in the DigiD sector form or an 8/9-digit number that passes the 11-proef is
  refused by the admin API, self-registration, SCIM and imports.
- **Recognised** as the DigiD sector form `s00000000:<bsn>` (NameID or attribute, on any provider), a bare number that
  passes the 11-proef on a DigiD connection or typed as a BSN, an eIDAS PersonIdentifier from NL, or a `bsn`
  claim/attribute of an OIDC or SAML broker. Attributes that contain it are dropped before mappers run. A DigiD
  Machtigen representative's BSN (`actingSubject`) is stored as its hash.
- **Masked in the console.** Users show `*****1234` (`bsnMasked` in the admin API). Revealing the full number needs the
  [`reveal-bsn`](../operations/admin-roles.md) permission and is audited (`BSN_REVEALED`). Log lines and audit events
  carry the hash, never the number. The realm export carries no BSN; GDPR anonymisation removes it.

### Releasing the BSN to an application

By default **no application receives the BSN**. A realm administrator can opt one client in, recording the legal basis:
in the console under **Applications → (client) → Advanced → Citizen service number (BSN)**, or **SAML clients →
NameID & attributes**. The application then receives the `bsn` member of the **userinfo** response — never inside an
ID or access token, which are persisted and forwarded to resource servers; call userinfo with the access token — or the SAML
attribute `urn:nl-eid-gdi:1.0:id:BSN` (FriendlyName `bsn`). No protocol mapper or profile attribute can emit a `bsn`
claim. Every change is audited (`BSN_RELEASE_GRANTED` / `BSN_RELEASE_REVOKED`, with the legal basis).

```bash
curl -s -b cookies.txt -c cookies.txt "$HELIX_URL/admin/realms/$REALM/bsn-release" >/dev/null
CSRF=$(awk '($6=="__Host-XSRF-TOKEN"||$6=="XSRF-TOKEN")&&$7~/^r1\./{t=$7} END{print t}' cookies.txt)

# Release the BSN to the OIDC client "wmo-portal" (realm-admin only)
curl -s -b cookies.txt -X PUT \
  -H "Content-Type: application/json" \
  -H "X-XSRF-TOKEN: $CSRF" \
  -d '{"protocol": "oidc", "clientId": "wmo-portal", "enabled": true,
       "legalBasis": "Wmo 2015 art. 2.3.2 (maatwerkvoorziening)"}' \
  "$HELIX_URL/admin/realms/$REALM/bsn-release"
```
```json
{ "protocol": "oidc", "clientId": "wmo-portal", "enabled": true,
  "legalBasis": "Wmo 2015 art. 2.3.2 (maatwerkvoorziening)", "grantedBy": "…", "grantedAt": 1792267251799 }
```

For a SAML service provider use `"protocol": "saml"` and its entity id as `clientId`; `"enabled": false` stops the
release. The grant is bound to the client's internal id: it needs an existing client (`404` otherwise), is deleted with
the client, and a client created later under the same id inherits nothing. A session an administrator impersonates
never receives the BSN (audited as `BSN_SUPPRESSED_IMPERSONATION`). Releasing needs a `legalBasis` of at least 10 characters (`400` with `fieldErrors` otherwise) and
`realm-admin` (`403` otherwise).

| Method & path (under `/admin/realms/{realm}`) | Purpose |
| --- | --- |
| `GET /bsn-release` | Clients and SAML providers that receive the BSN, with their legal basis (`view-clients`) |
| `PUT /bsn-release` | Turn the release on or off for one client (`realm-admin`, audited) |
| `POST /users/{userId}/bsn/reveal` | The user's full BSN (`reveal-bsn`, audited, never cached) |

!!! warning "Upgrading accounts from before this release"
    Accounts DigiD created earlier had the BSN as username and as link key. At the first start Helix moves them, once
    (new username, link re-keyed on the hash, BSN removed from profile attributes and the audit log in the database;
    audited as `BSN_MIGRATION`). The BSN is recorded only for links of a trusted source; a BSN-shaped username without
    one is only replaced (`BSN_MIGRATION_SKIPPED`). Citizens keep their accounts. Relying parties that read the BSN from
    `preferred_username` must be opted in to the `bsn` claim instead. The BSN remains in older backups and external log
    stores until they expire.

## Levels of assurance

`minimumLoa` is both **requested** in the AuthnRequest (`RequestedAuthnContext Comparison="minimum"`) and **enforced** on the assertion. DigiD levels map to the SAML 2.0 context classes defined in Logius' *Koppelvlakspecificatie DigiD SAML Authenticatie* v3.5:

| DigiD level | `minimumLoa` keywords | Requested and ranked class (`urn:oasis:names:tc:SAML:2.0:ac:classes:…`) |
| --- | --- | --- |
| Basis | `loa1`, `basis` | `PasswordProtectedTransport` |
| Midden | `loa2`, `midden` | `MobileTwoFactorContract` |
| Substantieel | `loa3`, `substantieel` | `Smartcard` |
| Hoog | `loa4`, `hoog` | `SmartcardPKI` |

The four full class URNs are accepted as values too. A broker that asserts the NL eID-stelsel URNs `urn:nl-eid-gdi:1.0:LoA:Low` / `Midden` / `Substantial` / `High` is accepted at the same rungs, and such a URN may still be stored as the minimum: it is compared as stored, but the AuthnRequest asks for the context class of the same level, because DigiD does not know the nl-eid-gdi URNs in a request.

!!! warning "Unknown minimums are refused"
    A `minimumLoa` the server does not know is refused when you save the connection (`400` with `fieldErrors.minimumLoa`) and when a realm import contains it; a stored one is skipped when it loads. A blank minimum means no minimum. Before rc.8 an unknown minimum silently accepted every level — see the [upgrade guide](../operations/upgrade-rc8-rc9.md#8-unknown-eid-minimum-levels-are-refused).

A broker that *requires* the nl-eid-gdi URN in the AuthnRequest is not supported as a DigiD connection. The generic [SAML broker](saml.md) sends no `RequestedAuthnContext` and enforces no eID minimum. Routing services may assert other URIs (for example eIDAS `…/LoA/NotNotified/*`), which are not ranked and so never meet a set minimum.

### Full endpoint set

| Method & path (under `/admin/realms/{realm}`) | Purpose |
| --- | --- |
| `GET /identity-providers` · `POST /identity-providers` | List / create providers |
| `GET` · `PUT` · `DELETE /identity-providers/{alias}` | Read / update / delete a provider |

See [Identity providers](identity-providers.md) for the shared flow, and the [API reference](../integration/api-reference.md) for every field.

## See also

- [Identity providers](identity-providers.md) — broker model, JIT provisioning, SLO
- [eHerkenning (NL business)](eherkenning.md) · [eIDAS (EU cross-border)](eidas.md)
- [SAML broker](saml.md) — the underlying SAML 2.0 profile
- [Users & credentials](../manage/users.md) · [Authentication flows](../authentication/flows.md)
