Skip to content

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 and mapped into your realm.

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 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.
  6. Save; the provider loads at runtime and appears as a DigiD sign-in option.

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, with protocol: "digid". The examples below assume you have set $HELIX_URL / $REALM and logged in — see Authenticating to the API.

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.

# 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"
{
  "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.

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), 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 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).

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"
{ "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)

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.

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.

A broker that requires the nl-eid-gdi URN in the AuthnRequest is not supported as a DigiD connection. The generic SAML broker 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 for the shared flow, and the API reference for every field.

See also