Adding user authentication

This getting started manual shows how to successfully use different authentication means to sign a contract. Contracts are used within the Nuts ecosystem to identify a user to other network participants. It also relates a user to the care organization that user is currently working for. The signed contract is used as token to authenticate the user’s (local) EHR identity to other nodes in the network and can be used as session token on the EHR. The contract is required for every request that results in personal and/or medical data being retrieved.

Generic requirements

In the contract signing flow, a device or web page communicates with the Nuts node. Therefore the Nuts node needs to be accessible from the public internet. All APIs on the Nuts node starting with /public (without a trailing slash) must be accessible over HTTPS without any additional security measures that could prevent access by web or mobile devices. A domain must also be available which resolves to those APIs. The domain must be configured on the Nuts node:

auth:
  publicurl: https://example.com

Getting a valid contract

All signing means require a valid contract to be submitted. You can create a contract with the following API on the Nuts node:

PUT /internal/auth/v1/contract/drawup

With the following body:

{
  "type": "BehandelaarLogin",
  "language": "NL",
  "version": "v3",
  "legalEntity": "did:nuts:7eaDstczhnigW9uW1JRzYF2u5KhTntFTnee8vGDHokD",
  "validFrom": "2006-01-02T15:04:05+02:00",
  "validDuration": "2h"
}

The type must be one of the valid Nuts contract types, currently only BehandelaarLogin for Dutch and PractitionerLogin for English are supported. The language selects the correct language, NL for Dutch and EN for English. The version must be v3. The legalEntity must refer to the DID of the current organization. The user either selects an organization to login for, or is already logged in. The organization must have a DID as described in Getting Started on customer integration. validFrom is a RFC3339 compliant time string. validDuration describes how long the contract is valid for. Time unit strings are used like 1h or 60m, the valid time units are: “ns”, “us” (or “µs”), “ms”, “s”, “m”, “h”. The local system timezone is used to format the date and time string.

The return value looks like:

{
  "type": "PractitionerLogin",
  "language": "EN",
  "version": "v3",
  "message": "EN:PractitionerLogin:v3 I hereby declare to act on behalf of CareBears located in CareTown. This declaration is valid from Monday, 2 January 2006 15:04:05 until Monday, 2 January 2006 17:04:05."
}

The message from this result is used by the specific means.

Signing a contract with the employee identity means

This getting started manual shows how to successfully use employee identity authentication to sign a contract.

Basic requirements

To use the employee identity as a means for signing a contract, the following is required:

  • the user interacts with the XIS/ECD via a recent browser capable of running javascript.

  • the vendor has a Nuts node running.

  • the Nuts node controls a DID for the organization of the user.

Exposed endpoints

The employee identity means uses endpoints under {publicurl}/public/auth/v1/means/employeeid/{sessionID}.

Starting a session

Start a session by calling the following API on the Nuts node:

POST /internal/auth/v1/signature/session

The body for this call looks like:

{
    "means": "employeeid",
    "payload": "<message>"
            "params": {
                    "employer": "did:nuts:7eaDstczhnigW9uW1JRzYF2u5KhTntFTnee8vGDHokD",
                    "employee": {
                            "identifier": "user@example.com",
                            "initials": "T",
                            "familyName": "Tester",
                            "roleName": "VP"
                    }
            }
}

Where message is the result from the contract call discussed earlier. The identifier field must include a unique identifier for the user, for example an email address or employee number. The employer must contain the organization DID. All fields are mandatory except roleName. roleName should contain the role the user has within the organization. This can be a functional role but also the role used for role based access control if such a role is human readable.

The result will be something like:

{
    "sessionID": "56098jvsldrioj230468",
    "url": "https://node.example.com/public/auth/v1/means/employeeid/56098jvsldrioj230468"
}

Displaying the web page

The webpage served from the url field from the initiated session result needs to be shown to the user. This can be done by using an iframe, pop-up or nested browser window (for native apps). The application should make sure the user’s attention is on the newly rendered page. The application can also query the current status of the process by calling the session status endpoint (see below). If the frontend of the application is viewing the status, it must do so by proxying the session status endpoint via it’s backend. This is because the session status endpoint is an internal endpoint and can only be accessed by a client application’s backend.

Getting the session result

The result of the session can be obtained by calling:

GET /internal/auth/v1/signature/session/<sessionID>

The call to the Nuts node will return the following response:

{
    "status": "completed",
    "verifiablePresentation": {
        // ...
    }
}

The status field may contain any of [created, in-progress, completed, cancelled, errored, expired]. These values can be used to give the user feedback on the current status. The presence of the verifiablePresentation in the result is the main method of checking if the signing session succeeded. verifiablePresentation is the cryptographic proof that needs to be stored in the user session. It’s required in the OAuth flow for obtaining an access token.

Signing a contract with the IRMA means

This getting started manual shows how to successfully use IRMA to sign a contract.

Basic requirements

To use IRMA as a means for signing a contract, the following is required:

  • the user has the IRMA app installed on an Android or iOS device with camera and an internet connection.

  • the user has retrieved the BRP and email credentials in the IRMA app.

  • the user interacts with the XIS/ECD via a recent browser capable of running javascript.

  • the vendor has a Nuts node running.

IRMA flow

We use the Nuts node as IRMA server and as tool to start an IRMA session. This follows the flow as described on this IRMA Github page. The XIS/ECD will have to provide two endpoints for the frontend. One endpoint to start a session and one to get the session result. More info on these endpoints will be provided further down.

Additional configuration

The public URL will be expanded to {publicurl}/public/auth/irmaclient as IRMA path. IRMA’s session API will be mounted at that base path. The Nuts APIs used for signing will embed this URL in the QR code shown to the user. The javascript in the frontend will also use this URL (exposed via the QR code) to check the status of the signing session. Therefore the domain which serves the frontend must be able to do requests to that domain. The browser will require CORS headers to be configured on the domain configured in the Nuts node config. This can be done by the following snippet:

http:
  default:
    cors:
      origin: "other.com"

Where other.com is the domain serving the frontend. For development purposes * is also allowed. If the public APIs are mounted on a different port/interface in the nuts config then the default key should be changed to alt.public in the example above.

Setting up the frontend

For the frontend we’ll be using the irma-frontend-packages javascript library. More info on how to use this library can be found on https://irma.app/docs/irma-frontend/. You can choose to load the IRMA frontend packages javascript via an HTML tag, in which case you’ll need to build the javascript file yourself given the instructions on https://github.com/privacybydesign/irma-frontend-packages or you can choose to use npm:

"dependencies": {
  "@privacybydesign/irma-frontend": "^0.3.3"
}

Make sure you use the latest version.

IRMA allows for multiple frontends to be used. The most important ones are the web and popup frontends. The web frontend allows for embedding the IRMA web component within a html element. The popup frontend will render a new component that will render on top of the rest of the website. This manual will use the popup frontend.

A complete example:

let options = {
      // Developer options
      debugging: true,

      // Front-end options
      language: 'en',

      // customize textual components
      translations: {
        header: "Sign your contract"
      },

      // Back-end options
      session: {
        // Point to your web backend
        url: '/web/auth',

        // The request that will be send to the backend:
        start: {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify(this.some_data)
        },

        // required to translate Nuts specific return values
        mapping: {
          sessionPtr:      r => r.sessionPtr.clientPtr,
          sessionToken:    r => r.sessionID
        }
      }
    };

    // we'll use the popup frontend
    let irmaPopup = irma.newPopup(options);

    // start the interaction
    irmaPopup.start()
        .then(result => {
          console.log("success!")
          console.log(response)
        })
        .catch(error => {
          if (error === 'Aborted') {
            console.log('Aborted');
            return;
          }
          console.error("error", error);
        })
        .finally(() => irmaPopup = irma.newPopup(options));
  }

Lets break this down into parts.

// Developer options
debugging: true,

Is used to enabling debugging. The IRMA library will output more information helpful for development.

// Front-end options
language: 'en',

// customize textual components
translations: {
  header: "Sign your contract"
},

Sets the language to english which will set some default textual representations on the IRMA web component. The translations configuration option can be used to change each of the textual representation on the IRMA web component. In this case, only the header is changed.

// Back-end options
session: {
  // Point to your web backend
  url: '/web/auth',

  // The request that will be send to the backend:
  start: {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(this.some_data)
  },

  // required to translate Nuts specific return values
  mapping: {
    sessionPtr:      r => r.sessionPtr.clientPtr,
    sessionToken:    r => r.sessionID
  }
}

The session object contains all the technical parts to connect the IRMA javascript library to your backend. The contents of the start object configures the initial request to start a signing session. You can control the type of request and the contents. In this case, some data from the frontend is sent as JSON. This is optional and no particular data is required. The url, in this case /web/auth, must be set so the frontend can access the following URLs:

<url>/session
<url>/session/<sessionID>/result

These URLs must both be available on the backend. For the example above this means that both /web/auth/session/ and /web/auth/session/<sessionID>/result are available. The <sessionID> is the token that will be returned by the call to <url>/session/. How to parse the result of that call and extract the token is done via the mapping object.

The mapping object is a map where two keys are expected: sessionPtr and sessionToken. sessionPtr must point to the data that is used to render the QR code. sessionToken must point to the sessionID token used to get the result (IRMA uses the term sessionToken for sessionID ).

Setting up the backend

As discussed in the previous chapter, the backend is required to expose two APIs to the frontend:

<url>/session
<url>/session/<sessionID>/result

No particular security context is required, you may require a user session if needed.

Starting a session

The <url>/session API is used to start a session. To start a session at the Nuts node, a valid contract has to be drawn up first. You can create such a contract with the following API on the Nuts node:

PUT /internal/auth/v1/contract/drawup

With the following body:

{
  "type": "BehandelaarLogin",
  "language": "NL",
  "version": "v3",
  "legalEntity": "did:nuts:90348275fjasihnva4857qp39hn",
  "validFrom": "2006-01-02T15:04:05+02:00",
  "validDuration": "2h"
}

The type must be one of the valid Nuts contract types, currently only BehandelaarLogin for Dutch and PractitionerLogin for English are supported. The language` selects the correct language, NL for Dutch and EN for english. The version must be v3. The legalEntity must refer to the DID of the current organization. The user either selects an organization to login for, or is already logged in. The organization must have a DID as described in Getting Started on customer integration. validFrom is a RFC3339 compliant time string. validDuration describes how long the contract is valid for. Time unit strings are used like 1h or 60m, the valid time units are: “ns”, “us” (or “µs”), “ms”, “s”, “m”, “h”. The local system timezone is used to format the date and time string.

The return value looks like:

{
  "type": "PractitionerLogin",
  "language": "EN",
  "version": "v3",
  "message": "EN:PractitionerLogin:v3 I hereby declare to act on behalf of CareBears located in CareTown. This declaration is valid from Monday, 2 January 2006 15:04:05 until Monday, 2 January 2006 17:04:05."
}

The message from this result is used in the next part. Start an IRMA session by calling the following API on the Nuts node:

POST /internal/auth/v1/signature/session

The body for this call looks like:

{
    "means": "irma",
    "payload": "<message>"
}

Where message is the result from the contract call. The result from this call must be passed directly to the frontend. If any transformation is done, the mapping setting in the frontend must be changed accordingly.

Getting the session result

The IRMA javascript frontend library will check for the status of the signing session. When the session has been completed it’ll call the following url:

GET <url>/session/<sessionToken>/result

where <url> is the base url configured under session.url in the javascript options and <sessionToken> is the token returned by the previous call. The backend must implement this API, the implementation must call the following API on the Nuts node:

GET /internal/auth/v1/signature/session/<sessionToken>

Any error in calling this service need to be relayed to the frontend. This will instruct the user on why things went wrong and what to do next. The call to the Nuts node will return the following response:

{
    "status": "completed",
    "verifiablePresentation": {
        // ...
    }
}

The status field has a different content when a different signing means is used. The presence of the verifiablePresentation in the result is the main method of checking if the signing session succeeded. verifiablePresentation is the cryptographic proof that needs to be stored in the user session. It’s required in the OAuth flow for obtaining an access token.

Verifiable presentation validity

The backend should check if the signed contract (verifiable presentation) is still valid when using it. The validity can be checked by calling the following API with the verifiable presentation at the place of <vp>:

PUT /internal/auth/v1/signature/verify

with

{
    "checkTime": "2006-01-02T15:54:05+02:00",
    "verifiablePresentation": "<vp>"
}

It will return a structure similar to:

{
  "validity": true,
  "vpType": "NutsIrmaPresentation",
  "issuerAttributes": {
    "pbdf.gemeente.personalData.initials": "T",
        "pbdf.gemeente.personalData.prefix": "",
        "pbdf.gemeente.personalData.familyname": "Tester",
        "pbdf.sidn-pbdf.email.email": "tester@example.com"
  },
  "credentials": {
    "organization": "CareBears",
    "validFrom": "2006-01-02T15:04:05+02:00",
    "validTo": "2006-01-02T17:04:05+02:00"
  }
}

The validity will indicate its validity. An expired contract is considered invalid.

Audit log requirements

Information in the access token can be used to fulfill audit log requirements for protected resources. An access token issued by the Nuts node can be introspected using the introspection API (RFC7662):

POST /internal/auth/v1/accesstoken/introspect

The POST body is application/x-www-form-urlencoded encoded

token=<jwt>

The result will return the following identifying properties if user authentication was present in the access token:

  • username

  • initials

  • family_name

  • assurance_level

username can be used as a unique user identifier. It will be filled with an email address or other identifier, issued by the user’s employer.

Note

The listed properties are the minimal result, additional properties may be present in the access token.

Note

This is not yet the case for the UZI means.