@digipolis/auth

This package contains two components:

Usage no npm install needed!

<script type="module">
  import digipolisAuth from 'https://cdn.skypack.dev/@digipolis/auth';
</script>

README

@digipolis/auth

This package contains two components:

  1. A router which exposes a couple of endpoints which can be used to implement login in your application.
  2. A middleware that can be used to enable single sign-on (SSO) between different apps inside the antwerp/digipolis ecosystem.

In this version aprofiel with assurance levels and different authentication methods is supported (for mprofiel support, check out version 1.X.X).

Setup

You should use express-session in your application to enable session storage.

Be sure to load this middleware before other routes in your application, this enables the automatic refresh of the user's access token.

trust proxy should also be set to true to enable automatic generation of the application's login callback.

Basic example

import {
  createRouter, 
  createSSOMiddleware
} from '@digipolis/auth';
import Express from 'express';
import Session from 'express-session';

const app = new Express();
app.use(Session({
  secret: 'keyboard cat',
  resave: false,
  saveUninitialized: true,
  cookie: { secure: true }
}));

app.enable('trust proxy');


const authConfig = {
  clientId: 'client-id',
  clientSecret: 'client-secret',
  oauthHost: 'https://api-oauth2-a.antwerpen.be',
  basePath: '/auth',
  defaultScopes: [
    'astad.aprofiel.v1.name',
    'astad.aprofiel.v1.avatar',
    'astad.aprofiel.v1.email',
  ],
  scopeGroups: {
    address: ['crspersoon.housenumber', 'crspersoon.streetname'],
    personal: ['crspersoon.nationalnumber', 'crspersoon.nationality']
  },
  url: 'https://api-gw-a.antwerpen.be/acpaas/shared-identity-data/v1' ,
  consentUrl: 'https://api-gw-a.antwerpen.be/acpaas/consent/v1',
};


// This exposes endpoints to enable login 
app.use(createRouter(app, authConfig));

// This middleware enables SSO
const sso = createSSOMiddleware(authConfig);

// The sso middleware should be used where you serve your application
// it relies on redirects, which cannot be used with ajax calls.
router
.get('/index', sso, (req, res) => res.send('hello world'));

Configuration

The login router & the SSO middleware use the same configuration.

  • logLevel: defaults to error
  • basePath: string (default: '/auth')
    Each of the routes in the auth router will be prepended with this property
  • clientId: string
    Client credentials from the API gateway (see section api store)
  • clienSecret: string
    Client credentials from the API gateway (see section api store)
  • consentUrl: string
    The url of the consent api is necessary to enable SSO (also see section api store).
    (e.g. https://api-gw-a.antwerpen.be/acpaas/consent/v1)
  • shouldUpgradeAssuranceLevel: boolean default true Whether you should upgrade to an higher assurancelevel when you already have a session in the app defaults to true (can be disabled for performance reasons)
  • defaultScopes: string[ ]
    list of scopes you will always use (see section scopes) Should be compatible with assurance level = low
  • errorRedirect: string (default: '/')
    Where your application should redirect when something goes wrong during the login process.
  • hooks: object Hooks can be used to add custom logic to the login process. Can be used to modify your session or clean up when logging out.
    Each hook has the same signature as express middleware
    (req, res, next) => {})
    • preLogin: function[ ]
      List of functions that will be executed before login
    • preLogout: function[ ]
      List of functions that will be executed before logout
    • loginSuccess: function[ ]
      List of functions that will be executed when login has succeeded
    • logoutSuccess: function[ ]
      List of functions that will be executed when logout has succeeded
  • key: string (default: 'user')
    The loggedin user will be stored on req.session. This property defines where the user and his accesstoken will be stored.
    For 'user', the user will be at req.session.user and the accesstoken will be at req.session.userToken
  • logout (optional, but needed for single logout(SLO) with the event handler)
    • headerKey string
      the name of the http header where the key is located (defaults to x-logout-token)
    • securityHash string
      bcrypt hash of the token that will be placed in the http header.
    • sessionStoreLogoutAdapter Function
      function that returns a promise when the sessionStore has been successfully updated and rejects otherwise. This adapter is responsible for removing the session. More information
  • oauthHost: string This is where the actual login process starts after leaving your application. This is needed to generate a redirect url to the login page
    (e.g.: https://api-oauth2-a.antwerpen.be)
  • refresh: boolean (default false)
    Enables automatic refresh of the user's access token
  • scopeGroups: object
    scopeGroups is an object where all keys should have an array of scopes as values. These can be used to request additional scopes when logging in through the use of the query parameter scopeGroups (available scopes)
  • url: String Url where the user will be retrieved with after login has succeeded
    (e.g.: https://api-gw-a.antwerpen.be/acpaas/shared-identity-data/v1)

API Store configuration

For this module to fully work, some configuration on the API store is required.

After creating your application on the api store, you should create a contract with the Shared Identity Data API.

Create Contract shared identity

and the Consent API (if you want to enable SSO) Create Contract consent

The next step is to navigate to your applications and clicking on actions

actions

Click on oauth2 config. You'll find your clientId and secret here.

configure callback

You'll need to configure your callback path here normally, it will be <protocol>://<your-domain>/auth/login/callback (this module exposes this endpoint) (change the basePath if you have configured another)

Eventhandler configuration (for SLO)

Navigate to the eventhandler and go to the oauth namespace oauth namespace

Add a new wildcard subscription

subscription configuration the push url is <protocol>://<hostname>{basePath}/event/loggedout/ (basePath defaults to auth).

You should add a custom header which corresponds to the headerKey in your logout configuration (defaults to x-logout-token). Add your token. (this token will not be known to your application, only the hashed version) (don't forget to click the plus symbol)

How to use the Routes

GET {basePath}/login

This endpoint can be used to login. There are some query parameters available to control in which ways the user can login and which scopes the user can use.

Query parameters

  • scopeGroups
    comma seperated list of the keys of the scopeGroups configured in your configuration. If none are given, only the default scopes from the configration are requested.

  • minimal_assurance_level (default: low for context citizen, substantial for context enterprise)
    possible values: low, substantial, high
    Determines which authentication methods are available to the user.
    If specified, only authentication methods corresponding with the specified assurance level will be available for the user to log in with. See Available authentication methods for info about which authentication methods correspond to which assurance levels.

  • fromUrl (default /)
    Where the user should be redirected if the login process is successful.

  • context (enterprise, citizen or enterprise-citizen) (default citizen)
    Specifies whether the user should log in as a citizen or as an enterprise user. Logging in with context enterprise enables the application to fetch additional enterprise related roles from the authz api with the access token of the user. The context enterprise-citizen presents the user with a choice if they want to login with either citizen or their enterprise.

  • save_consent (default: true) optional query parameter that can be used if you do not want to save the consent.

  • force_auth (default: false) optional query parameter that can be used to force the login screen to show, even if a user is loggedin on the identity provider.

  • auth_methods
    A comma separated list of the auth methods to allow the user to log in with. This limits the list of authentication methods provided to the user by the minimal_assurance_level parameter (if specified) and the context.

    Note that you cannot provide conflicting auth methods with those determined by either the minimal_assurance_level parameter or the context parameter.

    e.g.:

    • auth_methods=iam-aprofiel-userpass&context=enterprise
      (enterprise context requires a minimal assurance level of substantial, iam-aprofiel-userpass has an assurance level of low)

    • auth_methods=iam-aprofiel-userpass&minimal_assurance_level=high
      (iam-aprofiel-userpass has an assurance level of low, which is not sufficient for the specified minimal assurance level)

    See Available authentication methods for a comprehensive list of available authentication methods.

GET {basePath}/isloggedin

The isloggedin endpoint can be used to check if the user is currently loggedIn

{
  isLoggedin: true,
  user: { ... } // this corresponds to the key that is configured in the serviceProvider
}

If the user is not logged in the following payload is returned.

{
  isLoggedin: false
}

GET {basePath}/logout

Redirects the user to the logout. This will cause the session to be destroyed on the IDP. The fromUrl query parameter can be used to redirect the user to a given page after logout.

Available scopes

Scope Alias Minimal assurance level
astad.aprofiel.v1.address aprofiel.address low
astad.aprofiel.v1.all aprofiel.all low
astad.aprofiel.v1.avatar aprofiel.avatar low
astad.aprofiel.v1.email aprofiel.email low
astad.aprofiel.v1.name aprofiel.name low
astad.aprofiel.v1.phone aprofiel.phone low
astad.aprofiel.v1.username aprofiel.username low
crspersoon.birthdate substantial
crspersoon.death substantial
crspersoon.deathdate substantial
crspersoon.familyname substantial
crspersoon.gendercode substantial
crspersoon.givenName substantial
crspersoon.housenumber substantial
crspersoon.housenumberextension substantial
crspersoon.municipalityname substantial
crspersoon.municipalityniscode substantial
crspersoon.nationality substantial
crspersoon.nationalnumber substantial
crspersoon.postalcode substantial
crspersoon.registrationstate substantial
crspersoon.streetname substantial

Available authentication methods

Name Assurance level Context Description
iam-aprofiel-userpass low citizen Our default aprofiel authentication with username and password
fas-citizen-bmid substantial citizen Belgian Mobile ID (e.g. Itsme)
fas-citizen-otp substantial citizen Authentication with one time password (e.g. sms)
fas-citizen-totp substantial citizen Time-based one time password (e.g. Google Authenticator)
fas-citizen-eid high citizen Authentication with eID-card and pin-code
fas-enterprise-bmid substantial enterprise Belgian Mobile ID (e.g. Itsme)
fas-enterprise-otp substantial enterprise Authentication with one time password (e.g. sms)
fas-enterprise-totp substantial enterprise Time-based one time password (e.g. Google Authenticator)
fas-enterprise-eid high enterprise Authentication with eID-card and pin-code

Creating and using SessionStoreLogoutAdapters

Your sessionstore can be backed by your server's memory or a database system like postgreSQL, mongodb. Because we have no generic way to query each type of store, we introduce the concept of adapters.

function adapter(sessionKey: String, accessTokenKey: String, userInformation: Object): Promise

An adapter should return a promise which resolves if it succeeds in altering the session or rejects when it fails.

It has 3 arguments:

  • sessionKey: This is the key under which your user is stored in the session (this is the same as the key property in your serviceProvider, defaults to user). essentially, this is the property that should be removed from your session to remove the user's information
  • accessTokenKey: this is the key of the accesstoken property inside your session, which should also be removed.
  • userInformation: this is an object that contains the information of the user that has been loggedout.
    • user: the id of the user,
    • timestamp: timestamp of logout. Could be used to ignore the request if the logout was long ago.

Available adapters

Existing adapters will be added here.

Example of an adapter implementation

// this is a non functional example,
function createAdapter(options) {
  const {
    connectionString
  } = options;

  const db = createConnection(connectionString);

  return function adapter(sessionKey, accessTokenKey, userInformation) {
    return new Promise((resolve, reject) => {
          const session = db.query({
        [`session.${sessionKey}.id`]: userInformation.user
    });

     // alter record and resave or do it in one query.
     // be aware that multiple sessions could have the same userId,
     // maybe we should also check whether the session is currently valid.

      return resolve();
    })
  }

  const authConfig = require('./authConfig');

  const adapter = createAdapter({
    connectionString: process.env.connectionString
  });

  Object.assign(authConfig, {
    logout: {
      adapter,
      securityHash: 'blabla'
    }
  });
}