Skip to content

This library handles transparent authentication of backend to backend API calls, using the OpenID Connect protocol.

License

Notifications You must be signed in to change notification settings

VilledeMontreal/authentication-nodejs-lib

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

76 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Quality Gate Status Lines of Code Duplicated Lines (%) Coverage Maintainability Rating Reliability Rating Security RatingVulnerabilities

authentication-nodejs-lib

This library handles transparent authentication of backend to backend API calls, using the OpenID Connect protocol.

It has been designed to cooperate with existing popular http client libraries using their own plugin system, in order to satisfy developers and stay out of their way.

The use of plugins has made the token injection completely transparent, simplifying development and reducing the risk of mistakes.

Why another oauth library?

  • dedicated to service to service communication, with service accounts or client credentials.
    • note that there are many very good libraries for the frontend or for servers protected by OIDC, but not many to properly implement service to service communication.
  • no external libraries
    • some libraries rely on a specific http client, which is not the one you chose and would unnecessarily increase the number of dependencies in your project.
  • use modern javascript and typescript
  • easy integration with any modern http client lib
  • auto-refresh of tokens
  • optional out of band token refresh (scheduled)
  • proper cache invalidation of bad tokens (and auto retry)
  • proper concurrency handling (renew token once when concurrent requests require it)
  • request correlation
  • easy, simple and clean API

Points of interest

  • 100% code coverage
  • 100% Sonarqube compliant
  • abstraction for any http client lib
  • immutable objects
  • small, composable objects
  • no globals, no singleton
  • fully async
  • factory/builder pattern for easy testing and extensibility
  • concurrency handling to avoid refreshing the same token twice
    • see SynchronizedAsyncValue
  • auto refresh for expired and invalid access tokens
  • scheduled refresh to avoid refreshing a token while serving a request
  • easy configuration
  • native integration with popular http request libraries (superagent, axios, got...)

Supported http clients

Tested OIDC servers

Additional plugins

Request logger

This plugin will log information before and after the execution of a request, as well as its elapsed time.

Request correlator

This plugin will inject the x-correlation-id header in the request. Note that a correlation ID can also be injected in the requests handled by a IHttpClient when the "correlationIdProvider" property of the IHttpDefaults is defined.

Request retryer

This plugin will retry a request if it meets some criteria.

  • Superagent: this is a builtin feature of the library
  • Axios plugin

Usage

Superagent

Here's the simplest example, using the Superagent http client:

npm install superagent @villedemontreal/auth-oidc-plugin-superagent
npm install @types/superagent --save-dev
import * as superagent from 'superagent';
import {
  authenticator,
  createSession,
} from '@villedemontreal/auth-oidc-plugin-superagent';
// configure
const session = createSession({
  authMethod: 'client_secret_basic',
  client: {
    id: 'client',
    secret: 'clientSecret',
  },
  issuer: 'http://localhost:5005',
  scopes: ['openid', 'profile'],
});
// custom auth for each http call:
const res = await superagent
  .get('http://localhost:4004/secured/profile')
  .use(authenticator(session));
console.log(res.status, res.body);

// or configure auth once for all http calls:
const myAgent = superagent.agent().use(authenticator(session));
// then each call will be automatically authenticated
const res2 = await myAgent.get('http://localhost:4004/secured/profile');
console.log(res2.status, res.body);

Axios

Here's the simplest example, using the Axios http client:

npm install axios @villedemontreal/auth-oidc-plugin-axios
npm install @types/axios --save-dev
import axios, { AxiosRequestConfig } from 'axios';
import {
  authenticator,
  createSession,
} from '@villedemontreal/auth-oidc-plugin-axios';
// configure
const session = createSession({
  authMethod: 'client_secret_basic',
  client: {
    id: 'client',
    secret: 'clientSecret',
  },
  issuer: 'http://localhost:5005',
  scopes: ['openid', 'profile'],
});
// custom auth for each http call:
const config: AxiosRequestConfig = {};
authenticator(session).bind(config);
const res = await axios.get('http://localhost:4004/secured/profile', config);
console.log(res.status, res.data);

// or configure auth once for all http calls:
const myAgent = axios.create();
authenticator(session).bind(myAgent);
// then each call will be automatically authenticated
const res2 = await myAgent.get('http://localhost:4004/secured/profile');
console.log(res2.status, res.data);

Request

Here's the simplest example, using the Request http client:

npm install request @villedemontreal/auth-oidc-plugin-request
npm install @types/request --save-dev
import * as request from 'request';
import {
  authenticator,
  createSession,
} from '@villedemontreal/auth-oidc-plugin-request';
// configure
const session = createSession({
  authMethod: 'client_secret_basic',
  client: {
    id: 'client',
    secret: 'clientSecret',
  },
  issuer: 'http://localhost:5005',
  scopes: ['openid', 'profile'],
});
// custom auth for each http call:
const config: request.CoreOptions = {
  baseUrl: 'http://localhost:4004',
};
authenticator(session).bind(config);
request.get('/secured/profile', config, (err, res, body) => {
  if (err) {
    console.error(err);
  } else {
    console.log(res.statusCode, body);
  }
});

Note that the plugins will also work with 'request' promises such as 'request-promise-native' or similar.

Note that this plugin won't implement all features (such as retry or IHttpClient) since the 'request' module has been deprecated. We implemented the bare minimum to help integrate our lib with the https://openapi-generator.tech/docs/generators/typescript-node generator.

There are a couple of helper functions that make it easy to add OIDC authentication into the generated API classes:

import {
  ILogger,
  IHttpRequestCorrelator,
} from '@villedemontreal/auth-core';
import {
  IOidcSession,
  authInterceptor,
  requestCorrelationInterceptor,
  requestLoggingInterceptor
} from '@villedemontreal/auth-oidc-plugin-request';
import { OrderApi } from 'OrderApi';

export function createOrderApi(
  baseUrl: string,
  session: IOidcSession,
  logger: ILogger,
  correlator: IHttpRequestCorrelator,
) {
  const orderApi = new OrderApi(baseUrl);
  orderApi.addInterceptor(authInterceptor(session));
  orderApi.addInterceptor(requestLoggingInterceptor(logger));
  orderApi.addInterceptor(requestCorrelationInterceptor(correlationIdService));
  return orderApi;
}

const orderApi = createOrderApi(...);
const res = await orderApi.getOrderById('abc123');

Documentation

For more information about the configuration options, the flows and the object model, go to the Documentation folder.

Examples

For a real example, go to the Examples folder.

Developing

This project is a monorepo, that relies on Lerna to simplify common tasks and take care of interdependencies.

Unit testing

The project uses Jest for handling unit tests.

Running tests:

Viewing code coverage:

npm t
npm run show-coverage

Debugging:

  • make sure that the coverage feature is disabled while debugging otherwise you won't be able to hit your breakpoints.
  • the project already contains the .vscode folder that exposes 2 debug tasks:
    • Jest current file
    • Jest all

Code quality

Finally, the project is configured to enforce code quality using:

All those quality gates are enforced through the use of Git commit hooks (provided by https://github.com/typicode/husky)

Note that you should install VSCode plugins to enforce those rules when saving files. (The project already has the .vscode/settings.json file configured)

Installation

npm install

Note that it will automatically install the modules of each project managed by this mono-repo, then it will compile all projects.

Build

npm run compile

Testing

Run unit tests:

npm run test

Run linter for Typescript and formatting (Prettier):

npm run lint

or

npm run lint-fix

Releasing

Develop branch

Publish the package as a beta release.

Note that it will patch the current version (but it won't alter the committed package.json file), inject a beta tag and a Git sha.

Ex: @villedemontreal/[email protected]+55b3646

git checkout develop
npm run publish:dev

Master branch

git checkout develop
git pull

Generate a new version in package.json, commit it and tag it (should be executed manually):

First, edit the root package.json and bump its version.

Then run the following task that will execute a javascript script which copy the root version into each package and it will also adjust the dependencies.

npm run bump

Merge develop into master:

git checkout master
git pull
git merge develop --ff-only

Tag the release:

git tag v1.x.x

Push to Github:

git push
git push --tags

Note that any push to the master branch will trigger a Github action that will build, test, run Sonarqube but it won't publish the lib to NPM.

To publish the lib to npm automatically, you will have to create a release in Github using the last tag you just pushed. This will trigger a build that will invoke the publish:master task.

However, if you need to publish manually, you can execute the following command, providing you did a npm login first:

npm run publish:master

License

The source code of this project is distributed under the MIT License.

Contributing

See CONTRIBUTING.md.

Code of Conduct

Participation in this poject is governed by the Code of Conduct.

TODOs

  • add more logs
  • trigger more events
  • implement UMA2
  • implement SCIM