Implementing a Nuts v2 Use Case

A Nuts use case enables organizations that don't directly trust each other, but rely on trusted third parties, to find each other and securely exchange data. The use case specifies which data is exchanged, on which authorization grounds, and how API endpoints can be found.

This book tells you how to implement a use case that uses Nuts spec v2: deployment and configuration, managing DIDs and credentials, registering on and searching the Discovery Service, and requesting and validating access tokens. Nuts spec v2 uses did:web and the Discovery Service. It does NOT use did:nuts or the gRPC network.

This book uses 360-graden-clientbeeld as its example use case. Steps that are the same for every use case are written in general terms. Everything specific to 360-graden-clientbeeld is in a blue box marked "360-graden-clientbeeld" (see "How to use this book" for an example). For another use case, refer to that use case's implementation guide (IG) instead.

Start here

How this book works, who does what, and what you need from the use case.

Start here

How to use this book

This book takes you through implementing a Nuts spec v2 use case, from deploying the Nuts node to querying another care organization. Each page is one task: what it's for, what you need first, the steps, and how to check it worked.

Everything specific to the example use case, 360-graden-clientbeeld, is in a blue box like this one:

360-graden-clientbeeld

Values and rules that only apply to 360-graden-clientbeeld. For another use case, refer to that use case's implementation guide (IG).

Who does what

Party Role
Vendor Runs the Nuts node, for all of its care organizations, and integrates it with its EHR or other software. Most of this book is for the vendor.
Care organization Owns its UZI server certificate and creates its X509Credential with it (see "Get an X509Credential from a UZI certificate").
Use case Its implementation guide (IG) sets the rules: which credentials, which endpoints, which data, and on which legal basis.

What you need from the use case's IG

What Used in
Discovery Service definition, per environment "Install the use case's configuration files"
Policy definition, per environment "Install the use case's configuration files"
Which credentials a care organization needs "Set up a care organization"
Which registration parameters to publish, e.g. API endpoints "Register on the Discovery Service"
The scope, and which credentials go in a token request "Request an access token"
Which queries are allowed, and the rules for allowing them "Make the FHIR call", "Validate the access token"

Checklist

Step How often Chapter
Deploy the Nuts node Once per vendor, per environment Deploy
Install the use case's configuration files Once per use case, per environment Configure the node for a use case
Create a subject, get and load its credentials Once per care organization, per environment Set up a care organization
Register on the Discovery Service Once per care organization, per use case, per environment Join the use case
Find the other party, request a token, make the call Every query Outbound: querying another party
Validate the token, decide whether to allow the query Every incoming query Inbound: handling queries

Words used in this book

Word Meaning
Subject A care organization on your Nuts node. You use its subject ID in all calls to the node's internal API.
DID Decentralized identifier: a public ID with keys. Each subject has one or more DIDs.
did:web The DID method used in spec v2. Its DID document is published on your node's public web address.
Verifiable Credential A signed statement about a subject, e.g. its URA number (X509Credential).
Verifiable Presentation One or more credentials, signed by the holder, to show them to another party.
Wallet Where the node stores a subject's credentials.
Discovery Service A shared list where care organizations register, so others can find them and their endpoints.
Discovery Server The server that holds a Discovery Service's list. The use case says where it is.
Policy definition The credentials another party must present to get an access token, per scope.
Scope The name of what an access token gives access to. The use case sets it.
Authorization server The part of a Nuts node that issues access tokens.
Introspect Ask your Nuts node what an access token contains and whether it's valid.
DPoP A way to bind an access token to the requester's key, so a stolen token can't be used.
Start here

Implementing a spec v1 use case (did:nuts)

This book covers spec v2 use cases (did:web and the Discovery Service). Spec v1 use cases, such as eOverdracht, use did:nuts and the gRPC network, and work differently.

For spec v1, use the Getting Started guide of Nuts node v5.4:

Step v5.4 documentation
Configure your node Configure your node
Manage vendor and care organization identity Manage vendor and care organization identity
Authentication Authentication
Add authorizations Add authorizations
Connect your EHR Connect your EHR

Deploy

Deploy the Nuts node and, optionally, Nuts Admin. Put the node behind a reverse proxy with the right TLS certificates, and check that it's reachable.

Deploy

Deploy the Nuts node

There are two ways to run the Nuts part of your system. Choose one:

Option What it is When to choose it
Nuts node (default) The Nuts node on its own. You build the integration with your EHR yourself, following this book. You want full control, or you already integrate with the Nuts node
Nuts Knooppunt Has an embedded Nuts node, plus components for integrating with the MinVWS Generic Functions (Generieke Functies), such as a policy decision point (PDP) for access decisions and Mitz integration You want more ready-made parts, or you integrate with the Generic Functions. See the Knooppunt section below

One Nuts node per vendor (and per environment) hosts all of the vendor's care organizations.

Nuts node

The Nuts node documentation describes deployment in detail. Start with the recommended deployment and running on Docker.

Install

The Nuts node is published as:

Form Where
Docker image nutsfoundation/nuts-node on Docker Hub
Helm chart, for Kubernetes Helm repository https://nuts-foundation.github.io/nuts-node, chart nuts-node-chart (source)
Binaries (Linux, amd64 and arm64) GitHub releases

Use a fixed version (e.g. nutsfoundation/nuts-node:<version>), not latest. For the latest version, see the GitHub releases. Docker images are signed; see verifying image signatures.

Configuration

The settings you need for a spec v2 use case. Each can be set in nuts.yaml or as an environment variable (NUTS_ + the name in capitals, with _ instead of ., e.g. NUTS_HTTP_INTERNAL_ADDRESS).

Setting Value Why
url Your node's public URL, e.g. https://nuts.example.com Used in your did:web DIDs and OAuth2 URLs. Must be HTTPS.
strictmode true (default) Forbids insecure settings. Only turn it off on development or test systems without production data.
didmethods web Spec v2 only uses did:web (see "Create a subject").
http.internal.address e.g. :8081 The internal API. Default 127.0.0.1:8081, which can't be reached from outside the container.
storage.sql.connection Connection string to PostgreSQL, MySQL or SQL Server (including Azure SQL) The default, SQLite, isn't meant for production.
crypto.storage vaultkv (HashiCorp Vault) or azure-keyvault Where private keys are stored. fs (on disk) is only for development.
discovery.definitions.directory Directory with the Discovery Service definitions See chapter "Configure the node for a use case".
policy.directory Directory with the policy definitions See chapter "Configure the node for a use case".

Network

Check that it works

Nuts Knooppunt

Knooppunt has an embedded Nuts node, so the steps in this book still apply. It also has a policy decision point (PDP) that helps you decide whether to allow an inbound query (see "Validate the access token").

See the Knooppunt documentation, in particular:

Deploy

Endpoints and TLS certificates

Which endpoints you expose, to whom, and with which TLS certificate. This repeats the recommended deployment for spec v2 use cases; see there for details.

Nuts node endpoints

Your reverse proxy terminates TLS for the public endpoints.

Endpoint Interface Who uses it Protection
/iam Public (:8080) Other parties resolving your did:web DID documents HTTPS with a publicly trusted server certificate
/.well-known Public (:8080) Other parties: DID documents, OAuth2 metadata HTTPS with a publicly trusted server certificate
/oauth2 Public (:8080) Other Nuts nodes requesting access tokens from you HTTPS with a publicly trusted server certificate
/statuslist Public (:8080) Parties checking whether credentials you issued are revoked HTTPS with a publicly trusted server certificate
/discovery Public (:8080) Only if your node runs a Discovery Service server HTTPS with a publicly trusted server certificate
/internal Internal (:8081) Your own applications, Nuts Admin Never public: restrict access through network separation
/status, /status/diagnostics, /health, /metrics Internal (:8081) Monitoring, health checks Restrict access through network separation
/n2n, /public, gRPC (:5555) — Spec v1 (did:nuts) only Don't expose them

Your data endpoints

Protect your FHIR API (or other data API) with mutual TLS (mTLS) based on PKIoverheid certificates: both your server and the calling vendor's client authenticate with a PKIoverheid certificate. Check the use case's IG for what's allowed in test environments.

Don't mix up the certificates

Certificate Used for
UZI server certificate Creating the care organization's X509Credential (see "Get an X509Credential from a UZI certificate")
Publicly trusted TLS certificate The Nuts node's public endpoints
PKIoverheid certificate mTLS on your data endpoints
Deploy

Deploy admin app

Nuts Admin is an optional web application for managing your Nuts node by hand: create subjects, load and issue credentials, and register on Discovery Services. It's useful for testing and for small numbers of care organizations. For many care organizations, integrate with the node's internal API instead, as described in the rest of this book.

Run it

docker run -p 1305:1305 -e NUTS_NODE_ADDRESS=http://nutsnode:8081 nutsfoundation/nuts-admin:<version>

See the Nuts Admin README for all settings.

Secure it

Nuts Admin passes requests on to the node's internal API. Anyone who can reach Nuts Admin can manage your node.

Never expose Nuts Admin to the internet without login.

Deploy

Configure url rewrite for OAuth

Other parties look up your node's OAuth2 metadata at a .well-known URL, which always starts at the root of the domain (RFC 8615). If your node runs at the root of its own (sub)domain, e.g. https://nuts.example.com, you don't need to do anything.

If the node runs on a subpath, e.g. https://example.com/nuts, your reverse proxy must forward the OAuth2 metadata URL to the node. Example for a node on /nuts:

Other parties request Forward to the node as
https://example.com/.well-known/oauth-authorization-server/nuts/oauth2/<subject ID> /.well-known/oauth-authorization-server/oauth2/<subject ID>

In short: remove the subpath (/nuts) after the .well-known/... part.

The node serves all its other endpoints at the root too (/iam, /oauth2, /statuslist, ...). So your proxy must also remove /nuts from those, e.g. forward https://example.com/nuts/oauth2/... to /oauth2/....

NGINX

location /.well-known/oauth-authorization-server/nuts/ {
    proxy_pass http://nuts-node:8080/.well-known/oauth-authorization-server/;
}

Azure Application Gateway

path: '/.well-known/oauth-authorization-server/nuts'
rewriteCondition: '/.well-known/oauth-authorization-server/nuts(.*)'
rewriteRule: '/.well-known/oauth-authorization-server{var_uri_path_1}'

Check that it works

From outside your network, open:

https://example.com/.well-known/oauth-authorization-server/nuts/oauth2/<subject ID>

It should return JSON with your node's OAuth2 metadata, e.g. an issuer of https://example.com/nuts/oauth2/<subject ID>.

Configure the node for a use case

Install the Discovery Service and policy definitions you receive from the use case.

Configure the node for a use case

Install the use case's configuration files

A use case gives you two kinds of configuration files. Your Nuts node needs both, for every use case you join.

File What it tells your node Directory setting
Discovery Service definition Where the use case's Discovery Service is, and which credentials a care organization must present to register on it discovery.definitions.directory
Policy definition Which credentials another party must present to get an access token from you, per scope policy.directory

Do this once per use case, per environment.

Don't change these files. They contain the use case's trust rules, such as which certificate authorities are trusted. A changed file can make your node accept parties it shouldn't.

Install the files

  1. Download the Discovery Service definition and the policy definition for your environment from the use case's IG.
  2. Put them in the directories set in discovery.definitions.directory and policy.directory. Use any file name that ends in .json; the node skips other files.
  3. If you use Docker, mount the directories read-only.
  4. Restart the node. It reads the files at startup. If it can't read a Discovery Service definition, it doesn't start; the log says which file.

Each environment has its own files. Only install the files for the environment the node runs in. A use case typically uses the same service ID and scope in all environments; with files for two environments, the node won't start.

360-graden-clientbeeld

The files are in the nl-360-ig repository:

Environment Discovery Service definition Policy definition Discovery Server
Development discovery_360_dev.json policy_360_dev.json http://localhost:9091 (a local Discovery Server)
Test discovery_360_test.json policy_360_test.json https://development.nedap-nuts.nl
Acceptance discovery_360_acc.json policy_360_acc.json https://staging.nedap-nuts.nl
Production discovery_360_prod.json policy_360_prod.json https://nedap-nuts.nl

All environments use the service ID 360-graden.

What's in a Discovery Service definition

Part What it means
id The service ID. You use it in the Discovery Service API calls.
endpoint The Discovery Server where registrations are stored
presentation_max_validity How long a registration is valid, in seconds. Your node refreshes it before then.
presentation_definition.input_descriptors The credentials a care organization must present to register
Fields with a filter Requirements on a credential, e.g. which certificate authorities are trusted. This is why each environment has its own file.
Fields with an id Values the node takes from the credentials. They show up as fields in search results. A pattern with a capture group, like (\d+), returns only the captured part.

360-graden-clientbeeld

The test definition, shortened: some fields, like organization_id, organization_city and fhir_base_url_stu3, are left out. It requires an X509Credential from a trusted certificate authority, and a DiscoveryRegistrationCredential with the endpoints. The organization_ura pattern returns only the URA.

{
  "id": "360-graden",
  "endpoint": "https://development.nedap-nuts.nl/discovery/360-graden",
  "presentation_max_validity": 259200,
  "presentation_definition": {
    "input_descriptors": [
      {
        "id": "id_uzicert_x509credential",
        "constraints": {
          "fields": [
            { "path": ["$.type"], "filter": { "const": "X509Credential" } },
            { "path": ["$.issuer"], "filter": { "pattern": "^did:x509:0:sha256:(GwlhBZ...|szqMaT...|svKzNx...)..." } },
            { "id": "organization_ura", "path": ["$.credentialSubject.san.otherName"], "filter": { "pattern": "^[0-9.]+-\\d+-\\d+-S-(\\d+)-00\\.000-\\d+$" } },
            { "id": "organization_name", "path": ["$.credentialSubject.subject.O"] }
          ]
        }
      },
      {
        "id": "id_registration_parameters_credential",
        "constraints": {
          "fields": [
            { "path": ["$.type"], "filter": { "const": "DiscoveryRegistrationCredential" } },
            { "id": "authorization_server_url", "path": ["$.credentialSubject.authServerURL"] },
            { "id": "fhir_base_url_r4", "path": ["$.credentialSubject.fhirR4BaseURL"], "optional": true }
          ]
        }
      }
    ]
  }
}

What's in a policy definition

A policy definition maps a scope to the credentials another party must present to get an access token for it.

360-graden-clientbeeld

The policy, shortened. The scope is 360-graden. The requester must present an X509Credential and a NutsEmployeeCredential.

{
  "360-graden": {
    "organization": {
      "input_descriptors": [
        { "id": "id_uzicert_uracredential", "...": "X509Credential from a trusted CA, as in the Discovery Service definition" },
        { "id": "id_employee", "...": "NutsEmployeeCredential with employee_name, employee_role and employee_identifier" }
      ]
    }
  }
}

Set up a care organization

Do this once per care organization, on the vendor's Nuts node: create a subject, get an X509Credential from the organization's UZI server certificate, and load it into the wallet.

Set up a care organization

Create a subject

Each care organization gets its own subject on the vendor's Nuts node. A subject is the name you use for the care organization in all calls to the Nuts node's internal API. Behind a subject, the node creates one or more DIDs (decentralized identifiers) and their keys.

Do this once per care organization, per environment.

Before you start

Decide: generated or your own subject ID

You have to choose one of these:

Option How What you do
Let the Nuts node generate the subject ID Leave out subject in the request Store the subject ID from the response in your own system, with the care organization
Bring your own subject ID Set subject in the request, e.g. to the ID your system already uses for the care organization Nothing to store: you already know the ID

Keep in mind:

If you bring your own subject ID and it isn't random (e.g. a KVK number or an auto-incrementing database key), consider adding the environment at the end, e.g. 12345678-acc. The same ID then can't exist in two environments, which reduces the chance of leaking data because environments got mixed up. The examples on this page use careorg-a-acc.

Using the API

POST <internal Nuts interface>/internal/vdr/v2/subject
Content-Type: application/json

{"subject": "careorg-a-acc"}

The response contains the subject ID and its DID documents:

{
  "subject": "careorg-a-acc",
  "documents": [
    {
      "id": "did:web:nuts.example.com:iam:657f064a-ebef-4f0f-aa87-88ed32db3142",
      ...
    }
  ]
}

Using Nuts Admin

  1. Log in to Nuts Admin.
  2. Click "Identities".
  3. Click the plus sign on the right.
  4. Enter the subject ID, e.g. careorg-a-acc.
  5. Click "Create Identity".

Check that it worked

Other parties must be able to resolve the DID from the internet. Test that with a public DID resolver:

  1. Take the DID from the id field in the response above, e.g. did:web:nuts.example.com:iam:657f064a-ebef-4f0f-aa87-88ed32db3142.
  2. Paste it into the Universal Resolver at https://dev.uniresolver.io/ and click "Resolve".
  3. It should return the DID document. If it fails, check that the node's public interface is reachable from the internet through your reverse proxy (see chapter "Deploy"). The resolver fetches the document from https://nuts.example.com/iam/657f064a-ebef-4f0f-aa87-88ed32db3142/did.json, so you can also open that URL from outside your network.

You need this DID in the next step, to issue the X509Credential. To look up a subject's DIDs later, use GET <internal Nuts interface>/internal/vdr/v2/subject/careorg-a-acc.

API reference: Verifiable Data Registry (v2)

Set up a care organization

Get an X509Credential from a UZI certificate

Many use cases identify care organizations by their URA number. The CIBG doesn't issue Verifiable Credentials with the URA yet, so you create one yourself from the care organization's UZI server certificate: an X509Credential.

The node uses the X509Credential to:

The X509Credential is:

It contains the UZI certificate, and the URA, name and city taken from it. It does not contain the private key.

For background, such as how the URA ends up in the credential and which certificate authority a use case trusts, see X509Credential with UZI server certificates.

Do this once per care organization, per environment.

Who does what

The UZI certificate and its private key belong to the care organization. Ideally, the private key never leaves the care organization:

Step Who
Create the subject and give its did:web DID to the care organization Vendor
Create the X509Credential with the UZI certificate and private key Care organization
Send the X509Credential (a JWT, without the private key) to the vendor Care organization
Load the X509Credential into the wallet Vendor

In practice, the vendor often already manages the care organization's UZI certificate, e.g. for client TLS connections. The vendor then has the private key anyway, and can create the X509Credential itself. The steps below work for the care organization and the vendor alike.

Before you start

Create the credential

Put the certificate chain (certificate.pem) and private key (privatekey.pem) in a directory called certs. From the directory that contains certs, run:

docker run --rm -v "$(pwd)/certs:/certs" \
  nutsfoundation/go-didx509-toolkit:1.2.0 \
  vc /certs/certificate.pem /certs/privatekey.pem \
  "<CA name>" \
  <did>

Check https://hub.docker.com/r/nutsfoundation/go-didx509-toolkit/tags for the latest version.

The output is the X509Credential as a JWT (a long string starting with eyJ). Send it to the vendor, who loads it into the care organization's wallet (see "Load the credential into the wallet").

Non-production environments

For non-production environments, the CIBG issues test certificates whose name, URA and city don't match the real care organization. Where the use case allows it, you can instead create a fake UZI server certificate with a test CA:

  1. Check out https://github.com/nuts-foundation/go-didx509-toolkit. You need Bash and openssl.

  2. In the test_ca directory, run:

    ./issue-cert.sh <domain (CN)> <name (O)> <city (L)> <uzi> <ura> <agb>
    

    The domain, UZI and AGB values can be anything; use cases don't use them.

  3. The certificate chain and private key are written to the out directory. Copy them to your certs directory.

  4. Create the credential as above, but with "CN=Fake UZI Root CA" as the CA:

    docker run --rm -v "$(pwd)/certs:/certs" \
      nutsfoundation/go-didx509-toolkit:1.2.0 \
      vc /certs/<domain>-chain.pem /certs/<domain>.key \
      "CN=Fake UZI Root CA" \
      <did>
    

Which CAs an environment accepts is set in the use case's Discovery Service definition.

360-graden-clientbeeld

The X509Credential is the only credential a care organization needs to obtain. Accepted certificates per environment:

Environment Accepted certificates
Development, test Fake UZI certificates and CIBG test certificates (G1 and acceptance G4)
Acceptance CIBG test certificates (G1 and acceptance G4) and real G1 certificates. Not fake ones, and not real G4 ones.
Production Real CIBG certificates only (G1 and G4)

Lifecycle

The X509Credential is only valid as long as the UZI certificate it was created from. Create and load a new X509Credential:

Load the new credential as described in "Load the credential into the wallet", then remove the old one (see "Renew credentials").

Set up a care organization

Load the credential into the wallet

Load the X509Credential into the care organization's wallet on the Nuts node. The node uses credentials from the wallet when it registers on the Discovery Service and when it requests access tokens.

The same steps work for any other credential you receive for the care organization.

Before you start

Using the API

POST <internal Nuts interface>/internal/vcr/v2/holder/careorg-a-acc/vc
Content-Type: application/json

"eyJ..."

Using Nuts Admin

  1. Log in to Nuts Admin.
  2. Click "Identities".
  3. Click the care organization.
  4. Under "Credentials in Wallet", click "Upload".
  5. Paste the credential (the JWT) in the input field.
  6. Click "Upload Credential".

Check that it worked

List the credentials in the wallet:

GET <internal Nuts interface>/internal/vcr/v2/holder/careorg-a-acc/vc

The result should contain a credential with type X509Credential.

API reference: Verifiable Credential Registry (v2)

Set up a care organization

Obtaining credentials

Some use cases require credentials besides the X509Credential. Check the use case's Discovery Service and policy definitions to see which credentials you need.

360-graden-clientbeeld

No other credentials are needed. You can skip this page.

A credential is issued in one of two ways, depending on the use case:

Issued by When What you do
A trusted third party The use case names an issuer, e.g. VZVZ or Vektis Receive the credential from the issuer and load it into the wallet
The care organization itself (self-issued) The use case allows it, e.g. because there's no trusted issuer yet Issue it with your Nuts node, then load it into the wallet

Self-issue a credential

The steps below use the HealthcareProviderRoleTypeCredential as example, from the Huisartsinzage use case.

The HealthcareProviderRoleTypeCredential states the type of care organization, e.g. H1 (huisartsinstelling) or X3 (verplegings- of verzorgingsinstelling). Huisartsinzage requires it. VZVZ might issue it in the future; until then, care organizations issue it themselves.

Before you start

Issue the credential

POST <internal Nuts interface>/internal/vcr/v2/issuer/vc
Content-Type: application/json

{
  "@context": "https://www.w3.org/2018/credentials/v1",
  "type": "HealthcareProviderRoleTypeCredential",
  "issuer": "did:web:nuts.example.com:iam:657f064a-ebef-4f0f-aa87-88ed32db3142",
  "credentialSubject": {
    "id": "did:web:nuts.example.com:iam:657f064a-ebef-4f0f-aa87-88ed32db3142",
    "roleCodeNL": "X3"
  },
  "expirationDate": "2027-10-27T00:00:00Z",
  "format": "jwt_vc"
}

The response is the credential as a JWT, in double quotes.

Load the credential into the wallet

Issuing doesn't put the credential in the wallet. Load it as described in "Load the credential into the wallet", using the response from the step above as the request body.

Lifecycle

Issue and load a new credential before expirationDate, and when the care organization's type changes.

API reference: Verifiable Credential Registry (v2)

Join the use case

Do this once per care organization, per use case: register on the use case's Discovery Service so other parties can find you.

Join the use case

Register on the Discovery Service

Register the care organization on the use case's Discovery Service, so other parties can find it and its API endpoints. In the Nuts node this is called "activating" the Discovery Service for a subject.

Do this once per care organization, per use case. The node then keeps the registration up to date by itself.

Before you start

Registration parameters

Registration parameters are values you publish with the registration, typically API endpoints. The use case says which ones you need. The node puts them in a DiscoveryRegistrationCredential, so you don't need to issue that credential yourself.

The node always adds authServerURL: your Nuts node's OAuth2 authorization server URL for this subject.

360-graden-clientbeeld

The wallet only needs the X509Credential. Registration parameters:

Parameter Required Value
fhirR4BaseURL At least one of the two Base URL of your FHIR R4 API
fhirStu3BaseURL At least one of the two Base URL of your FHIR STU3 API

The service ID is 360-graden.

Using the API

POST <internal Nuts interface>/internal/discovery/v1/<service ID>/<subject ID>
Content-Type: application/json

{
  "registrationParameters": {
    "<parameter>": "<value>"
  }
}

360-graden-clientbeeld

POST <internal Nuts interface>/internal/discovery/v1/360-graden/careorg-a-acc
Content-Type: application/json

{
  "registrationParameters": {
    "fhirR4BaseURL": "https://fhir.example.com/r4"
  }
}

The node registers right away:

Response Meaning What to do
200 OK Registered. The node refreshes the registration by itself before it expires. Nothing
412 Precondition Failed Not registered. The response says why, e.g. missing credentials or the Discovery Server can't be reached. The node does not retry by itself. Fix the cause and call the API again
404 Not Found Unknown service ID or subject ID Check both IDs

Check that it worked

Get the registration status:

GET <internal Nuts interface>/internal/discovery/v1/<service ID>/<subject ID>

Then check that others can find you, by searching for your own care organization, e.g. by name:

GET <internal Nuts interface>/internal/discovery/v1/<service ID>?<path in the credential>=<value>

360-graden-clientbeeld

The name is in the X509Credential:

GET <internal Nuts interface>/internal/discovery/v1/360-graden?credentialSubject.subject.O=Zorgorganisatie%20A*

See "Find care organizations and their endpoints" for more search options.

Changing or removing the registration

API reference: Discovery Service

Outbound: querying another party

Find another care organization, request an access token, and query its FHIR API.

Outbound: querying another party

Find care organizations and their endpoints

Before you can query another care organization, you need its API endpoint and its authorization server URL. Find both by searching the use case's Discovery Service through your own Nuts node.

Before you start

GET <internal Nuts interface>/internal/discovery/v1/<service ID>?<path in the credential>=<value>

Query parameters are paths in the registered credentials:

Which paths you can search on depends on the credentials the use case requires.

360-graden-clientbeeld

Name and city are in the X509Credential, so you can combine them:

Search by Query parameter
Name credentialSubject.subject.O=Zorgorganisatie*
City credentialSubject.subject.L=Utrecht
GET <internal Nuts interface>/internal/discovery/v1/360-graden?credentialSubject.subject.O=Zorgorganisatie*

Your node searches its own copy of the Discovery Service. It updates that copy every 10 minutes by default (discovery.client.refreshinterval), so a new registration can take that long to show up.

The result

Part What it is
credential_subject_id The care organization's DID
fields Values taken from the credentials, named by the field IDs in the Discovery Service definition
registrationParameters The values the care organization registered, e.g. its API endpoints
authServerURL The authorization server URL. You need it to request an access token (see "Request an access token")

Which fields and registration parameters you get depends on the use case.

360-graden-clientbeeld

[
  {
    "id": "did:web:nuts.other.example:iam:2f1b7c3e-0d1a-4a7e-9a55-4b8f3c2e9d10#7c4e...",
    "credential_subject_id": "did:web:nuts.other.example:iam:2f1b7c3e-0d1a-4a7e-9a55-4b8f3c2e9d10",
    "fields": {
      "organization_id": "did:web:nuts.other.example:iam:2f1b7c3e-0d1a-4a7e-9a55-4b8f3c2e9d10",
      "organization_name": "Zorgorganisatie B",
      "organization_city": "Utrecht",
      "organization_ura": "87654321",
      "authorization_server_url": "https://nuts.other.example/oauth2/careorg-b-acc",
      "fhir_base_url_r4": "https://fhir.other.example/r4"
    },
    "registrationParameters": {
      "authServerURL": "https://nuts.other.example/oauth2/careorg-b-acc",
      "fhirR4BaseURL": "https://fhir.other.example/r4"
    },
    "vp": { ... }
  }
]

organization_ura is the bare URA, because the definition's pattern has a capture group.

API reference: Discovery Service

Outbound: querying another party

Request an access token

To query another care organization's API, you need an access token from that organization's authorization server. Your Nuts node requests it for you: it shows the authorization server the credentials the use case requires.

Before you start

360-graden-clientbeeld

Request the token

POST <internal Nuts interface>/internal/auth/v2/<subject ID>/request-service-access-token
Content-Type: application/json

{
  "authorization_server": "<authServerURL of the other care organization>",
  "scope": "<scope>",
  "credentials": [ <credentials that aren't in the wallet, if the use case requires them> ]
}
Field Value
<subject ID> (in the URL) The subject ID of your care organization: the one making the request, e.g. careorg-a-acc
authorization_server The other care organization's authServerURL, from the Discovery Service search result
scope Set by the use case
credentials Optional. Credentials to present that aren't in the wallet, typically about the user. See below
token_type Optional. DPoP (default) or Bearer. Use DPoP: a stolen DPoP token can't be used without your node's key

The user's identity

Some use cases require a credential about the logged-in user, e.g. a NutsEmployeeCredential. Pass it in credentials. You don't sign it, and you leave out issuer and credentialSubject.id: the node fills them in with your care organization's DID. The node's signature on the whole request protects it.

360-graden-clientbeeld

A NutsEmployeeCredential is required, for the other party's audit log:

Field Value
name The user's name
roleName The user's role, e.g. "Verpleegkundige"
identifier The user's ID in your system
POST <internal Nuts interface>/internal/auth/v2/careorg-a-acc/request-service-access-token
Content-Type: application/json

{
  "authorization_server": "https://nuts.other.example/oauth2/careorg-b-acc",
  "scope": "360-graden",
  "credentials": [
    {
      "@context": [
        "https://www.w3.org/2018/credentials/v1",
        "https://nuts.nl/credentials/v1"
      ],
      "type": ["VerifiableCredential", "NutsEmployeeCredential"],
      "credentialSubject": {
        "name": "J. de Vries",
        "roleName": "Verpleegkundige",
        "identifier": "123456"
      }
    }
  ]
}

The response

{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6Ikp...",
  "token_type": "DPoP",
  "dpop_kid": "did:web:nuts.example.com:iam:657f064a-ebef-4f0f-aa87-88ed32db3142#<key id>",
  "scope": "<scope>",
  "expires_in": 900
}

Errors

Response Meaning
400 Bad Request A parameter is wrong, or the authorization server returned an OAuth error
412 Precondition Failed Your wallet doesn't have the credentials the policy requires
502 Bad Gateway The other party's authorization server returned an error
503 Service Unavailable The other party's authorization server can't be reached

API reference: Auth (v2)

Outbound: querying another party

Make the FHIR call

With an access token, you can query the other care organization's FHIR API (or other data API).

The token is either a DPoP token (the default) or a Bearer token, depending on token_type in your token request. DPoP is optional, but more secure: every request also needs a DPoP header, which proves the request comes from you, the party the token was issued to. A stolen DPoP token is useless without your node's key. Your Nuts node creates the DPoP header for you.

Before you start

1. Create the DPoP header (DPoP tokens only)

Create a new DPoP header for every request. It's only valid for one HTTP method and URL, and can only be used once.

POST <internal Nuts interface>/internal/auth/v2/dpop/<dpop_kid, URL-encoded>
Content-Type: application/json

{
  "htm": "<HTTP method>",
  "htu": "<URL of the request>",
  "token": "<access token>"
}
Field Value
<dpop_kid> (in the URL) dpop_kid from the token response, URL-encoded once, e.g. # becomes %23
htm The HTTP method of the request you're about to make, e.g. GET
htu The URL of the request you're about to make. Query parameters are ignored.
token The access token

The response:

{
  "dpop": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImRwb3Arand0In0..."
}

2. Make the request

Send the access token in the Authorization header with the type DPoP, and the DPoP header in DPoP:

<HTTP method> <URL of the request>
Authorization: DPoP <access token>
DPoP: <dpop from step 1>

For a Bearer token, skip step 1 and send Authorization: Bearer <access token>.

Errors

Response from the other party What to do
401 Unauthorized The token expired, or the DPoP header doesn't match the request. Check htm and htu. If the token expired, request a new one. If it hasn't expired, the other party may have lost it: request a new token with the header Cache-Control: no-cache, which skips your node's token cache.
403 Forbidden The other party doesn't allow this query, e.g. no legal basis such as consent. Check the use case's rules.

360-graden-clientbeeld

Use fhirR4BaseURL or fhirStu3BaseURL from the search result, depending on which FHIR version the other party offers. The examples below use R4, with base URL https://fhir.other.example/r4.

1. Get the CapabilityStatement. It says which queries the other party supports. It doesn't need an access token, and you need it before any query other than Patient.

GET https://fhir.other.example/r4/metadata

2. Find the patient by BSN. Only POST-based search is allowed. Create the DPoP header with htm POST and htu https://fhir.other.example/r4/Patient/_search.

POST https://fhir.other.example/r4/Patient/_search
Content-Type: application/x-www-form-urlencoded
Authorization: DPoP <access token>
DPoP: <dpop>

identifier=http://fhir.nl/fhir/NamingSystem/bsn|123456789

The result has the patient's ID at the other party, e.g. Patient/abc123.

3. Query the data. For example, the patient's allergies. Create a new DPoP header with htm GET and htu https://fhir.other.example/r4/AllergyIntolerance.

GET https://fhir.other.example/r4/AllergyIntolerance?patient=abc123&_profile=http://nictiz.nl/fhir/StructureDefinition/nl-core-AllergyIntolerance,http://nictiz.nl/fhir/StructureDefinition/zib-AllergyIntolerance
Authorization: DPoP <access token>
DPoP: <dpop>

API reference: Auth (v2)

Inbound: handling queries

Check the access token on incoming requests and decide whether to allow the query.

Inbound: handling queries

Validate the access token

When another care organization queries your FHIR API (or other data API), the request carries an access token your Nuts node issued. Your API (or the API gateway in front of it) must check that token on every request, then decide whether to allow the query.

The Nuts node checks the token and tells you who is asking. Whether that party may see the data is decided by your application.

Before you start

What an incoming request looks like

For a DPoP token, the request has two headers:

<HTTP method> <URL of the request>
Authorization: DPoP eyJhbGciOiJSUzI1NiIsInR5cCI6Ikp...
DPoP: eyJhbGciOiJFUzI1NiIsInR5cCI6ImRwb3Arand0In0...

For a Bearer token, there's only Authorization: Bearer <token>.

1. Introspect the token

POST <internal Nuts interface>/internal/auth/v2/accesstoken/introspect
Content-Type: application/x-www-form-urlencoded

token=eyJhbGciOiJSUzI1NiIsInR5cCI6Ikp...

The result has the standard OAuth2 fields, plus the fields from the use case's policy definition:

Field What it is
active Whether the token is valid
scope The scope the token was issued for
iss The authorization server that issued the token: your node, for one of your care organizations
client_id The client ID the requester gave in its token request. It isn't verified, so don't use it to identify the requester: use the fields from the credentials instead, e.g. organization_ura.
exp When the token expires (seconds since 1970)
cnf.jkt DPoP tokens only: the thumbprint of the requester's DPoP key
Other fields Values from the credentials the requester presented, named by the field IDs in the policy definition

360-graden-clientbeeld

{
  "active": true,
  "iss": "https://nuts.example.com/oauth2/careorg-a-acc",
  "client_id": "https://nuts.other.example/oauth2/careorg-b-acc",
  "scope": "360-graden",
  "exp": 1793110000,
  "cnf": { "jkt": "jlkhnp87453slfhansdhf" },
  "organization_ura": "87654321",
  "organization_name": "Zorgorganisatie B",
  "organization_city": "Utrecht",
  "employee_name": "J. de Vries",
  "employee_role": "Verpleegkundige",
  "employee_identifier": "123456"
}

Need the presented credentials themselves, e.g. for your audit log? Use POST <internal Nuts interface>/internal/auth/v2/accesstoken/introspect_extended: same result, plus the Verifiable Presentations.

2. Check the DPoP header (DPoP tokens only)

A DPoP token is only valid together with a DPoP header that the requester signed for this request:

POST <internal Nuts interface>/internal/auth/v2/dpop/validate
Content-Type: application/json

{
  "dpop_proof": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImRwb3Arand0In0...",
  "method": "<HTTP method of the request>",
  "url": "<URL of the request>",
  "thumbprint": "jlkhnp87453slfhansdhf",
  "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6Ikp..."
}
Field Value
dpop_proof The DPoP header of the incoming request
method The HTTP method of the incoming request
url The URL of the incoming request, as the requester called it. Query parameters are ignored.
thumbprint cnf.jkt from the introspection result
token The access token from the Authorization header

The node answers with 200 OK and whether the DPoP header is valid:

{
  "valid": false,
  "reason": "DPoP proof is expired"
}

Use the URL as the requester called it, not as your API sees it after a reverse proxy rewrote it. Otherwise the check fails.

3. Decide whether to allow the query

The Nuts node doesn't decide whether the requester may see this data. Your application does, using the introspection result and the rules of the use case.

The use case's IG describes the rules, e.g. which legal basis is needed (such as consent registered in Mitz), and what the request must contain.

You don't have to build this from scratch: Nuts Knooppunt has a policy decision point (PDP). Your reverse proxy or API gateway sends it the introspection result and the request details, and gets back allow or deny. It checks things like the FHIR CapabilityStatement, required search parameters and Mitz consent. It has built-in policies per scope, e.g. for BgZ, eOverdracht and PZP. See using the PDP in the Knooppunt docs.

360-graden-clientbeeld

The rules are in volume 2a of the IG. In short:

The IG has an OPA policy that implements these rules. It isn't used in production yet, but can be a starting point. Knooppunt has no built-in 360-graden policy.

Log every request with the requester's organization and user, as the use case requires.

API reference: Auth (v2)

Test your setup

Check each part of your setup, from the node itself to a query with another party.

Test your setup

Test your setup step by step

Check your setup in the order you built it. Each check tells you which part works; if one fails, fix it before going on.

# Check Tests
1 The node runs Deployment
2 Others can reach your node Reverse proxy, TLS, URL rewrite
3 The wallet has the credentials Credentials
4 You're registered and can be found Discovery Service registration
5 You can get an access token from yourself Policy definition, credentials, token flow, DPoP
6 You can query another party Everything, with a real other party

1. The node runs

GET <internal Nuts interface>/health

Should return 200 OK with status UP. If not, check the node's log.

2. Others can reach your node

From outside your network:

If this fails, check your reverse proxy, TLS certificate and URL rewrite (see chapter "Deploy").

3. The wallet has the credentials

GET <internal Nuts interface>/internal/vcr/v2/holder/<subject ID>/vc

The result should contain every credential the use case requires (see "Load the credential into the wallet").

4. You're registered and can be found

GET <internal Nuts interface>/internal/discovery/v1/<service ID>/<subject ID>

activated should be true and status active. Then search for your own care organization (see "Register on the Discovery Service").

5. Get an access token from yourself

Request a token from your own authorization server, as if you were another party. This tests your policy definition, your credentials and the whole token flow.

POST <internal Nuts interface>/internal/auth/v2/<subject ID>/request-service-access-token
Content-Type: application/json

{
  "authorization_server": "https://<your node>/oauth2/<subject ID>",
  "scope": "<scope>",
  "credentials": [ <user credentials, if the use case requires them> ]
}

authorization_server is your own authServerURL. Your node reaches it through its public URL, so this also tests your reverse proxy.

The result is an access token (see "Request an access token"). Then check it as you would an incoming request (see "Validate the access token"):

  1. Introspect the token. active should be true, and the fields from the policy definition should be there.
  2. Create a DPoP header for a test URL (see "Make the FHIR call"), and validate it with the same method and URL. valid should be true.
  3. Validate the same DPoP header again. Now valid should be false, with reason jti already used: a DPoP header can only be used once.
Response to the token request Likely cause
412 Precondition Failed The wallet doesn't have the credentials the policy requires
400 Bad Request Wrong scope (no policy for it), or a wrong credentials value
503 Service Unavailable Your node can't reach its own public URL

360-graden-clientbeeld

Use scope 360-graden, and pass a NutsEmployeeCredential with test values:

{
  "authorization_server": "https://<your node>/oauth2/careorg-a-acc",
  "scope": "360-graden",
  "credentials": [
    {
      "@context": ["https://www.w3.org/2018/credentials/v1", "https://nuts.nl/credentials/v1"],
      "type": ["VerifiableCredential", "NutsEmployeeCredential"],
      "credentialSubject": { "name": "Test User", "roleName": "Tester", "identifier": "test-1" }
    }
  ]
}

The introspection result should contain organization_id, organization_ura, organization_name, organization_city, employee_name, employee_role and employee_identifier.

6. Query another party

In a test environment, agree with another vendor to test with each other's care organizations:

  1. Find their care organization (see "Find care organizations and their endpoints").
  2. Request a token from their authorization server, and make a query (see chapter "Outbound: querying another party").
  3. Let them do the same with yours, and check your logs.

Running it

Keep credentials valid, monitor and back up the node, and fix common problems.

Running it

Renew credentials

Credentials expire. When one expires, the node no longer uses it, and the care organization can't register on the Discovery Service or get access tokens anymore. Renew them in time.

What to renew, and when

What When How
X509Credential Before the UZI server certificate expires, or when it's replaced or revoked Create a new one from the renewed certificate (see "Get an X509Credential from a UZI certificate")
Self-issued credentials Before their expirationDate Issue a new one (see "Obtaining credentials")
Credentials from a trusted third party Before they expire Ask the issuer for a new one
Discovery Service registration Never: the node refreshes it by itself —

Keep a list of expiry dates per care organization, or check them regularly (see "Find expiring credentials" below).

Replace a credential

  1. Load the new credential into the wallet (see "Load the credential into the wallet").

  2. Remove the old credential from the wallet, so the node doesn't pick it by mistake:

    DELETE <internal Nuts interface>/internal/vcr/v2/holder/<subject ID>/vc/<credential ID, URL-encoded>
    

    Find the credential ID (id) by listing the wallet: GET <internal Nuts interface>/internal/vcr/v2/holder/<subject ID>/vc.

The node uses the new credential right away for new access tokens. It uses it for the Discovery Service registration at the next refresh. To update the registration right away, call the registration POST again (see "Register on the Discovery Service").

Find expiring credentials

List the wallet and look at each credential's expirationDate (or exp for credentials in JWT form):

GET <internal Nuts interface>/internal/vcr/v2/holder/<subject ID>/vc

This only returns valid credentials: expired ones are left out.

Running it

Operate the node

Monitoring

Endpoint (internal interface) Use it for
/health Health checks by Docker or Kubernetes. Returns 200 OK when the node is healthy.
/status/diagnostics Details about the node's state, for administrators
/metrics Metrics in Prometheus format

See monitoring in the Nuts node documentation.

Backups

Back up both, and test that you can restore them:

Without the private keys, the node can't use its DIDs anymore, and every care organization needs a new subject, new credentials and a new registration.

Upgrades

Troubleshooting

Problem Likely cause What to do
The node doesn't start A Discovery Service definition can't be read The log says which file. Use the files from the use case's IG, unchanged.
A DID doesn't resolve from the internet Reverse proxy, TLS or DNS See "Endpoints and TLS certificates" and check 2 in "Test your setup step by step"
Discovery registration returns 412 Missing or expired credentials, or the Discovery Server can't be reached The response says which. Fix it and call the registration POST again: the node doesn't retry by itself.
Registration status is error A refresh failed error in the status says why. Fix it; the node retries at the next refresh.
You can't find a care organization that just registered Your node updates its copy of the Discovery Service every 10 minutes by default Wait, or check discovery.client.refreshinterval
Token request returns 412 Your wallet doesn't have the credentials the other party's policy requires Check the wallet and the use case's policy definition
Token request returns 503 The other party's authorization server can't be reached Check the authServerURL and your outbound network
The other party returns 401 for a token that hasn't expired Their authorization server may have lost the token, e.g. after a restart Request a new token with the header Cache-Control: no-cache, which skips your node's token cache. Don't do this by default: it adds load on both sides.
DPoP validation fails for incoming requests The URL your API sees differs from the URL the requester used, e.g. after a reverse proxy rewrote it Validate with the URL as the requester called it (see "Validate the access token")

Still stuck? Set verbosity to debug for more detail in the log, and ask on the Nuts Slack.