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.
- 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
- 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...)
- Superagent
- Axios
- Request (Note that this module has been deprecated but is still used by some projects such as https://openapi-generator.tech/docs/generators/typescript-node)
- Got (planned)
This plugin will log information before and after the execution of a request, as well as its elapsed time.
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.
This plugin will retry a request if it meets some criteria.
- Superagent: this is a builtin feature of the library
- Axios plugin
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);
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);
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');
For more information about the configuration options, the flows and the object model, go to the Documentation folder.
For a real example, go to the Examples folder.
This project is a monorepo, that relies on Lerna to simplify common tasks and take care of interdependencies.
The project uses Jest for handling unit tests.
Running tests:
- From the command-line:
npm t
- From VSCode: continuous testing with the Jest extension
- From the browser: using the Majestic test runner
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
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)
npm install
Note that it will automatically install the modules of each project managed by this mono-repo, then it will compile all projects.
npm run compile
Run unit tests:
npm run test
Run linter for Typescript and formatting (Prettier):
npm run lint
or
npm run lint-fix
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
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
The source code of this project is distributed under the MIT License.
See CONTRIBUTING.md.
Participation in this poject is governed by the Code of Conduct.
- add more logs
- trigger more events
- implement UMA2
- implement SCIM