Skip to content

Node.js authentication/login service for Digipolis

Notifications You must be signed in to change notification settings

osamah-dgp/auth_module_nodejs

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@digipolis/auth

@digipolis/auth is implemented as an Express router. It exposes a couple of endpoints that can be used in your application to handle the process of logging into a user's AProfile, mprofile or eid via oAuth.

Setup

You should use express-session in your application to enable session-storage. After this step, you can load the @digipolis/auth middleware

app.use(require('@digipolis/auth')(app, configuration));

Be sure to load this middleware before your other routes, otherwise the automatic refresh of the user's token won't work properly.

Also set the trust proxy application variable to true. Otherwise the callback URL might be constructed with protocol http instead of https.

// Trust proxy to make sure the @digipolis/auth module can construct the correct OAuth2 callback URL
app.enable('trust proxy');

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 Aprofiel/Mprofiel API.

Create Contract

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)

Unless you configured a custom redirectUri. in this case, you should enter this one

Event Handler configuration (only needed for Single log out)

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

Click on the topic related to your login methodology and click on + (add subscription) add subscription

Configure your endpoint with the correct params:

subscription configuration the push url is <protocol>://<hostname>{basePath}/event/loggedout/{service} (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)

save your subscription.

Module Configuration

  • oauthHost string: The domain corresponding to the oauth implementation (e.g: https://api-oauth2-o.antwerpen.be').
  • applicationname string: required if permissions need to be fetched (name known in UM)
  • apiHost string: the hostname corresponding to the API gateway (e.g: https://api-gw-o.antwerpen.be).
  • basePath=/auth (optional) string: the basePath which is appended to the exposed endpoints.
  • errorRedirect=/ (optional) string: where to redirect if the login fails (e.g: /login)
  • logout (optional, but needed for 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
  • auth (credentials can be acquired from the api store)
    • clientId string: client id of your application
    • clientSecret string: client secret of your application
    • apiKey string: required to fetch permissions (not needed otherwise)
  • serviceProviders: object of the available oauth login services (currently aprofiel & MProfiel). You only need to configure the ones that you need.
    • aprofiel (optional if not needed):
      • scopes string: The scopes you want of the profile (space separated identifiers)
      • url string: the url where to fetch the aprofile after the login succeeded
      • identifier string: the service identifier, used to create login url.
      • tokenUrl string: where the service should get the accesstoken
      • redirectUri (optional) string: custom redirect callback uri, do not use unless absolutely necessary
      • refresh boolean: whether or not to refresh the access token (experimental)
      • maxAgeRefreshToken number: In seconds, if set, will refresh the session until it reached its max age based on this setting (Only if refresh is set to true)
      • key=user string: the key under the session (e.g. key=profile => req.session.profile)
      • hooks (optional): async execution is supported
        • loginSuccess array of functions: function that can be plugged in to modify the behaviour of @digipolis/auth: function signature is the same as middleware (req, res, next). these will run after successful login.
        • logoutSuccess array of functions: hooks that are triggered when logout is successful
    • mprofiel (optional if not needed):
      • scopes string: the scopes you want for the profile
      • url string: url where to fetch the profile
      • key=user string: the key under the session (e.g. key=profile => req.session.profile)
      • fetchPermissions=false boolean: whether to fetch permissions in the User Man. engine
      • authenticationType=form string: form or so, can be used together, see example
      • identifier=astad.mprofiel.v1 string: the service identifier, used to create the login url.
      • tokenUrl string: where the service should get the accesstoken
      • redirectUri (optional) string: custom redirect callback uri
      • refresh boolean: whether or not to refresh the access token (experimental)
      • maxAgeRefreshToken number: In seconds, if set, will refresh the session until it reached its max age based on this setting (Only if refresh is set to true)
      • hooks (optional): async execution is supported
        • loginSuccess array of functions: function that can be plugged in to modify the behaviour of @digipolis/auth: function signature is the same as middleware (req, res, next). these will run after successful login.
        • logoutSuccess array of functions: hooks that are triggered when logout is successful
    • eid (optional if not needed):
      • scopes string: the scopes you want for the profile
      • url string: url where to fetch the profile
      • key=user string: the key under the session (e.g. key=profile => req.session.profile)
      • identifier=acpaas.fasdatastore.v1 string: the service identifier, used to create the login url.
      • tokenUrl string: where the service should get the accesstoken
      • redirectUri (optional) string: custom redirect callback uri
      • refresh boolean: whether or not to refresh the access token (experimental)
      • maxAgeRefreshToken number: In seconds, if set, will refresh the session until it reached its max age based on this setting (Only if refresh is set to true)
      • hooks (optional): async execution is supported
        • loginSuccess array of functions: function that can be plugged in to modify the behaviour of @digipolis/auth: function signature is the same as middleware (req, res, next). these will run after successful login.
        • logoutSuccess array of functions: hooks that are triggered when logout is successful

Authentication 2.0

If you want to use authentication 2.0 you can do so by adding version: 'v2' and add the necessary extra config. Your application needs a contract with the Shared Identity Data API (Similar to the API Store configuration)

  • auth2aprofiel (optional if not needed):
    • version string: authentication version you want to use (v2 in this case). Defaults to v1.
    • minimalAssuranceLevel string: Minimal Assurance Level. We support low, substantial and high.
    • authMethods string: the authentication methods you want to allow. (e.g. iam-aprofiel-userpass for simple username/password based authentication)
    • scopes string: the scopes you want for the profile
    • url string: url where to fetch the profile
    • identifier=astad.aprofiel.v1 string: the service identifier, used to log out.
    • key=user string: the key under the session (e.g. key=profile => req.session.profile)
    • tokenUrl string: where the service should get the accesstoken
    • redirectUri (optional) string: custom redirect callback uri
    • refresh boolean: whether or not to refresh the access token (experimental)
    • hooks (optional): async execution is supported
      • loginSuccess array of functions: function that can be plugged in to modify the behaviour of @digipolis/auth: function signature is the same as middleware (req, res, next). these will run after successful login.
      • logoutSuccess array of functions: hooks that are triggered when logout is successful

Concerning the authentication methods, we support:

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

iam-aprofiel-userpass will only work when minimalAssuranceLevel is low.

minimalAssuranceLevel high will only show the fas-citizen-eid authentication method.

In general; if your minimalAssuranceLevel is set to substantial you can only use substantial and above (high).

Authentication 2.0 example config

    auth2eid: {
      version: 'v2',
      scopes: 'astad.aprofiel.v1.username astad.aprofiel.v1.name astad.aprofiel.v1.avatar astad.aprofiel.v1.email astad.aprofiel.v1.phone crspersoon.givenName',
      url: 'https://api-gw-o.antwerpen.be/acpaas/shared-identity-data/v1/me',
      key: 'auth2eid',
      authMethods: 'fas-citizen-bmid,fas-citizen-totp,fas-citizen-otp,iam-aprofiel-userpass',
      minimalAssuranceLevel: 'low',
      tokenUrl: 'https://api-gw-o.antwerpen.be/acpaas/shared-identity-data/v1/oauth2/token',
      hooks: {
        loginSuccess: [],
        logoutSuccess: []
      }
    }

Authentication 2.0 available scopes

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

Creating and using SessionStoreLogoutAdapters

Your sessionstore can be backed by your server's memory or a database system like postgres, 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
    }
  });
}

Example implementation

const session = require('express-session');
const app = express();
app.use(session({
  secret: 'blabla'
}))

const profileLogin = require('@digipolis/auth');
// load session with corresponding persistence (postgres, mongo....)
const loginSuccessHook = (req, res, next) => {
  req.session.isEmployee = false;
  if(req.digipolisLogin && req.digipolisLogin.serviceName === 'mprofiel') {
    req.session.isEmployee = true;
  }

  req.session.save(() => next());
}

app.use(profileLogin(app, {
  oauthHost: 'https://api-oauth2-o.antwerpen.be',
  apiHost: 'https://api-gw-o.antwerpen.be',
  errorRedirect: '/',
  applicationName: 'this-is-my-app',
  basePath: '/auth',
  auth: {
    clientId: 'your-client-id',
    clientSecret: 'your-client-secret',
    apiKey: 'my-api-string', // required if fetchPermissions == true
  },
  serviceProviders: {
    aprofiel: {
      scopes: '',
      url: 'https://api-gw-o.antwerpen.be/astad/aprofiel/v1/v1/me',
      identifier:'astad.aprofiel.v1',
      tokenUrl: 'https://api-gw-o.antwerpen.be/astad/aprofiel/v1/oauth2/token',
      hooks: {
        loginSuccess: [],
        logoutSuccess: []
      }
    },
    mprofiel: {
      scopes: 'all',
      url: 'https://api-gw-o.antwerpen.be/astad/mprofiel/v1/v1/me',
      identifier: 'astad.mprofiel.v1',
      fetchPermissions: false,

      tokenUrl: 'https://api-gw-o.antwerpen.be/astad/mprofiel/v1/oauth2/token',
      hooks: {
        loginSuccess: [],
        logoutSuccess: []
      }
    },
    'mprofiel-so': {
      scopes: 'all',
      url: 'https://api-gw-o.antwerpen.be/astad/mprofiel/v1/v1/me',
      identifier: 'astad.mprofiel.v1',
      fetchPermissions: false,
      authenticationType: 'so'
      tokenUrl: 'https://api-gw-o.antwerpen.be/astad/mprofiel/v1/oauth2/token',
      hooks: {
        loginSuccess: [],
        logoutSuccess: []
      }
    },
    eid: {
      scopes: 'name nationalregistrationnumber',
      url: 'https://api-gw-o.antwerpen.be/acpaas/fasdatastore/v1/me',
      key: 'eid'
      identifier:'acpaas.fasdatastore.v1',
      tokenUrl: 'https://api-gw-o.antwerpen.be//acpaas/fasdatastore/v1/oauth2/token',
      hooks: {
        loginSuccess: [],
        logoutSuccess: []
      }
    },
  }
}));

Session

Multiple profile can be logged in at the same time, if a key is configured inside the serviceProvider configuration. If no key is given, the default key user (req.session.user) is used, and the possibility exists that a previous user is overwritten by another when logging in.

The token can be found under req.session.userToken if the default key is used, otherwise it can be found under req.session[configuredKey + Token] e.g: token configured is aprofiel , the access token will be found under req.session.aprofielToken

{
  accessToken: 'D20A4360-EDD3-4983-8383-B64F46221115'
  refreshToken: '469FDDA4-7352-4E3E-A810-D0830881AA02'
  expiresIn: '2020-12-31T23.59.59.999Z'
}

Available Routes

Each route is prepended with the configured basePath, if no basePath is given, default basePath auth will be used.

GET {basePath}/login/{serviceName}?fromUrl={thisiswheretoredirectafterlogin}&lng={language}&auth_type={auth_type}&auth_methods={auth_methods}

This endpoints tries to redirect the user to the login page of the service corresponding to the serviceName (aprofiel, mprofiel, eid). (this will not work if the endpoint is called with an AJAX call)

query params

  • fromUrl: can be used to redirect the user to a given page after login.
  • lng: can be used to define the language. Currently supported: nl, de, fr and en
  • auth_type: can be used if you want to restrict the authentication types to others than defined in your service provider.
  • auth_methods: can be used to override the default defined authMethods. to limit the number of available methods or to enable true SSO. (comma seperated list)

GET {basePath}/isloggedin

The isloggedin endpoint can be used to check if the user is currently loggedIn in any of the configured services if he is logged in in some services, the following payload will be returned:

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

If the user is not logged in in any of the services, the following payload is returned.

{
  isLoggedin: false
}

GET {basePath}/isloggedin/{service}

check whether the user is logged in in the specified service. If he is logged in:

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


If the user is not logged in int the service, the following payload is returned.
```js
{
  isLoggedin: false
}

GET {basePath}/login/callback

Endpoint that you should not use manually, is used to return from the identity server and fetches a user corresponding to the login and stores it on the session.

If a redirect url was given through the fromUrl in the login endpoint, the user will be redirected to this url after the callback has executed successfully.

If the callback is does not originate from the login flow triggered from the application, it will trigger a 401. (this is checked with the state param).

Hooks defined in the serviceProviders[serviceName].hooks.loginSuccess will be called here. Session data can be modified in such a hook.

GET {basePath}/logout/{service}?fromUrl={thisiswheretoredirectafterlogout}

Redirects the user to the logout for the specified service. 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.

GET {basePath}/logout/callback/{service}

Cleans up the session after the initial logout.

POST {basePath}/event/loggedout/{service}

Endpoint which can be used to logout events from the eventhandler. This is used to remove a user's session when the user has logged out in an other application.

Contributing

Pull requests are always welcome, however keep the following things in mind:

  • New features (both breaking and non-breaking) should always be discussed with the repo's owner. If possible, please open an issue first to discuss what you would like to change.
  • Fork this repo and issue your fix or new feature via a pull request.
  • Please make sure to update tests as appropriate. Also check possible linting errors and update the CHANGELOG if applicable.

Support

Stefan Pante ([email protected])

About

Node.js authentication/login service for Digipolis

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages

  • JavaScript 100.0%