Security News
ESLint is Now Language-Agnostic: Linting JSON, Markdown, and Beyond
ESLint has added JSON and Markdown linting support with new officially-supported plugins, expanding its versatility beyond JavaScript.
openid-client
Advanced tools
OpenID Connect Relying Party (RP, Client) implementation for Node.js servers, supports passportjs
The openid-client package is a server-side library that allows Node.js applications to act as a Relying Party (RP) for any OpenID Connect (OIDC) compliant Identity Provider (IP). It provides functionality to discover OIDC providers, authenticate users, validate ID tokens, and securely perform token operations.
Discovery of OpenID Provider configuration
This feature allows the client to automatically discover the OpenID Provider's configuration using the issuer's URL. It simplifies the process of setting up the client by fetching the necessary endpoints and public keys.
const { Issuer } = require('openid-client');
(async () => {
const googleIssuer = await Issuer.discover('https://accounts.google.com');
console.log('Discovered issuer %s', googleIssuer.issuer);
})();
Client authentication with an OpenID Provider
This code sample demonstrates how to authenticate with an OpenID Provider by creating a client instance with the necessary credentials and generating an authorization URL for user redirection.
const { Issuer } = require('openid-client');
(async () => {
const issuer = await Issuer.discover('https://example.com');
const client = new issuer.Client({
client_id: 'your-client-id',
client_secret: 'your-client-secret',
redirect_uris: ['https://your-callback-url/callback'],
response_types: ['code']
});
const authorizationUrl = client.authorizationUrl({
scope: 'openid email profile',
});
console.log('Authorization URL:', authorizationUrl);
})();
Handling authentication responses
This feature is used to handle the callback from the OpenID Provider after user authentication. It involves parsing the callback parameters, exchanging the authorization code for tokens, and validating the ID token.
const { Issuer, generators } = require('openid-client');
(async () => {
const issuer = await Issuer.discover('https://example.com');
const client = new issuer.Client({
client_id: 'your-client-id',
client_secret: 'your-client-secret',
redirect_uris: ['https://your-callback-url/callback'],
response_types: ['code']
});
const code_verifier = generators.codeVerifier();
const code_challenge = generators.codeChallenge(code_verifier);
const params = client.callbackParams('https://your-callback-url/callback?code=AUTH_CODE&state=STATE');
const tokenSet = await client.callback('https://your-callback-url/callback', params, { code_verifier });
console.log('Received and validated tokens %j', tokenSet);
console.log('ID Token claims %j', tokenSet.claims());
})();
Token management
This code sample shows how to manage tokens, including how to exchange an authorization code for tokens and how to refresh tokens using a refresh token.
const { Issuer } = require('openid-client');
(async () => {
const issuer = await Issuer.discover('https://example.com');
const client = new issuer.Client({
client_id: 'your-client-id',
client_secret: 'your-client-secret'
});
const tokenSet = await client.grant({
grant_type: 'authorization_code',
code: 'AUTH_CODE',
redirect_uri: 'https://your-callback-url/callback'
});
const refreshedTokenSet = await client.refresh(tokenSet.refresh_token);
console.log('Refreshed tokens %j', refreshedTokenSet);
})();
Passport is a widely used authentication middleware for Node.js. It supports a wide range of strategies, including OpenID Connect. Compared to openid-client, Passport provides a more general authentication solution that can be extended with various strategies for different authentication mechanisms.
oidc-provider is a Node.js library that allows developers to implement their own OpenID Connect Provider. It is different from openid-client, which is designed to be used as a client for existing providers. oidc-provider is more suitable for those who want to create an identity provider rather than connect to one.
openid-client is a server side OpenID Relying Party (RP, Client) implementation for Node.js, supports passport.
Table of Contents
The following client/RP features from OpenID Connect/OAuth2.0 specifications are implemented by openid-client.
Filip Skokan has certified that openid-client
conforms to the RP Basic, RP Implicit, RP Hybrid, RP Config and RP Dynamic profiles
of the OpenID Connect™ protocol.
Head over to the example folder to see the library in use. This example is deployed and configured to use an example OpenID Connect Provider here. The provider is using oidc-provider library.
On the off-chance you want to manage multiple clients for multiple issuers you need to first get an Issuer instance.
const Issuer = require('openid-client').Issuer;
Issuer.discover('https://accounts.google.com') // => Promise
.then(function (googleIssuer) {
console.log('Discovered issuer %s', googleIssuer);
});
const Issuer = require('openid-client').Issuer;
const googleIssuer = new Issuer({
issuer: 'https://accounts.google.com',
authorization_endpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
token_endpoint: 'https://www.googleapis.com/oauth2/v4/token',
userinfo_endpoint: 'https://www.googleapis.com/oauth2/v3/userinfo',
jwks_uri: 'https://www.googleapis.com/oauth2/v3/certs',
}); // => Issuer
console.log('Set up issuer %s', googleIssuer);
Now you can create your Client.
You should provide the following metadata; client_id, client_secret
. You can also provide
id_token_signed_response_alg
(defaults to RS256
) and token_endpoint_auth_method
(defaults to
client_secret_basic
);
const client = new googleIssuer.Client({
client_id: 'zELcpfANLqY7Oqas',
client_secret: 'TQV5U29k1gHibH5bx1layBo0OSAvAbRT3UYW3EWrSYBB5swxjVfWUa1BS8lqzxG/0v9wruMcrGadany3'
}); // => Client
Should your oidc provider have provided you with a registration client uri and registration access token you can also have the Client discovered.
new googleIssuer.Client.fromUri(registration_client_uri, registration_access_token) // => Promise
.then(function (client) {
console.log('Discovered client %s', client);
});
client.authorizationUrl({
redirect_uri: 'https://client.example.com/callback',
scope: 'openid email',
}); // => String (URL)
You can also get HTML body of a self-submitting form to utilize POST to the authorization url with
#authorizationPost
method, same signature as #authorizationUrl
.
client.authorizationPost({
redirect_uri: 'https://client.example.com/callback',
scope: 'openid email',
}); // => String (Valid HTML body)
client.authorizationCallback('https://client.example.com/callback', request.query) // => Promise
.then(function (tokenSet) {
console.log('received and validated tokens %j', tokenSet);
console.log('validated id_token claims %j', tokenSet.claims);
});
const state = session.state;
const nonce = session.nonce;
client.authorizationCallback('https://client.example.com/callback', request.query, { state, nonce, max_age }) // => Promise
.then(function (tokenSet) {
console.log('received and validated tokens %j', tokenSet);
console.log('validated id_token claims %j', tokenSet.claims);
});
When handling multiple response modes with one single pass you can use #callbackParams
to get the params object from the koa/express/node request object or a url string.
(http.IncomingMessage). If form_post is your response_type you need to include a body parser prior.
client.callbackParams('https://client.example.com/cb?code=code'); // => { code: 'code' };
client.callbackParams('/cb?code=code'); // => { code: 'code' };
// koa v1.x w/ koa-body
app.use(bodyParser({ patchNode: true }));
app.use(function* (next) {
const params = client.callbackParams(this.request.req); // => parsed url query, url fragment or body object
// ...
});
// express w/ bodyParser
app.use(bodyParser.urlencoded({ extended: false }));
app.use(function (req, res, next) {
const params = client.callbackParams(req); // => parsed url query, url fragment or body object
// ...
});
client.refresh(refreshToken) // => Promise
.then(function (tokenSet) {
console.log('refreshed and validated tokens %j', tokenSet);
console.log('refreshed id_token claims %j', tokenSet.claims);
});
Tip: accepts TokenSet as well as direct refresh token values;
client.revoke(token, [tokenTypeHint]) // => Promise
.then(function (response) {
console.log('revoked token %s', token, response);
});
client.introspect(token, [tokenTypeHint]) // => Promise
.then(function (response) {
console.log('token details %j', response);
});
client.userinfo(accessToken) // => Promise
.then(function (userinfo) {
console.log('userinfo %j', userinfo);
});
Tip: accepts TokenSet as well as direct access token values;
via POST
client.userinfo(accessToken, { verb: 'post' }); // => Promise
auth via query
client.userinfo(accessToken, { via: 'query' }); // => Promise
auth via body
client.userinfo(accessToken, { verb: 'post', via: 'body' }); // => Promise
userinfo also handles (as long as you have the proper metadata configured) responses that are:
let claims = {
sub: 'userID',
_claim_names: {
credit_history: 'src1',
email: 'src2',
},
_claim_sources: {
src1: { endpoint: 'https://src1.example.com/claims', access_token: 'foobar' },
src2: { endpoint: 'https://src2.example.com/claims' },
},
};
client.fetchDistributedClaims(claims, { src2: 'bearer.for.src2' }) // => Promise
.then(function (output) {
console.log('claims %j', claims); // ! also modifies original input, does not create a copy
console.log('output %j', output);
// removes fetched names and sources and removes _claim_names and _claim_sources members if they
// are empty
});
// when rejected the error will have a property 'src' with the source name it relates to
let claims = {
sub: 'userID',
_claim_names: {
credit_history: 'src1',
email: 'src2',
},
_claim_sources: {
src1: { JWT: 'probably.a.jwt' },
src2: { JWT: 'probably.another.jwt' },
},
};
client.unpackAggregatedClaims(claims) // => Promise, autodiscovers JWT issuers, verifies signatures
.then(function (output) {
console.log('claims %j', claims); // ! also modifies original input, does not create a copy
console.log('output %j', output);
// removes fetched names and sources and removes _claim_names and _claim_sources members if they
// are empty
});
// when rejected the error will have a property 'src' with the source name it relates to
Use when the token endpoint also supports client_credentials or password grants;
client.grant({
grant_type: 'client_credentials'
}); // => Promise
client.grant({
grant_type: 'password',
username: 'johndoe',
password: 'A3ddj3w',
}); // => Promise
const opts = { keystore, initialAccessToken }; // both optional
issuer.Client.register(metadata, [opts]) // => opts optional, Promise
.then(function (client) {
console.log('Registered client %s, %j', client, client.metadata);
});
client.requestObject({ response_mode: 'form_post' }, {
sign: client.request_object_signing_alg,
encrypt: {
alg: client.request_object_encryption_alg,
enc: client.request_object_encryption_enc,
}
}).then(function (request) {
console.log('Nested signed and encrypted JWT Request Object %s', request)
});
Issuer.webfinger(userInput) // => Promise
.then(function (issuer) {
console.log('Discovered issuer %s', issuer);
});
Accepts, normalizes, discovers and validates the discovery of User Input using E-Mail, URL, acct, Hostname and Port syntaxes as described in Discovery 1.0.
Uses already discovered (cached) issuers where applicable.
authorizationCallback
and refresh
methods on a Client return TokenSet, when assigned an
expires_in
value a TokenSet calculates and assigns an expires_at
with the corresponding unix
time. It also comes with few helpers.
client.authorizationCallback(..., ...).then(function (tokenSet) {
console.log('tokenSet#expires_at', tokenSet.expires_at);
console.log('tokenSet#expires_in', tokenSet.expires_in);
setTimeout(function () {
console.log('tokenSet#expires_in', tokenSet.expires_in);
}, 2000);
console.log('tokenSet#expired()', tokenSet.expired());
console.log('tokenSet#claims', tokenSet.claims);
});
Once you have a Client instance, just pass it to the Strategy. Issuer is best discovered, Client passed properties manually or via an uri (see get-started).
Verify function is invoked with a TokenSet, userinfo only when requested, last argument is always the done function which you invoke once you found your user.
const Strategy = require('openid-client').Strategy;
const params = {
// ... any authorization params
// client_id defaults to client.client_id
// redirect_uri defaults to redirect_uris[0]
// response type defaults to client.response_types[0], then 'code'
// scope defaults to 'openid'
}
passport.use('oidc', new Strategy({ client, [params] }, (tokenset, userinfo, done) => {
console.log('tokenset', tokenset);
console.log('access_token', tokenset.access_token);
console.log('id_token', tokenset.id_token);
console.log('claims', tokenset.claims);
console.log('userinfo', userinfo);
User.findOne({ id: tokenset.claims.sub }, function (err, user) {
if (err) return done(err);
return done(null, user);
});
}));
// start authentication request
// options [optional], extra authentication parameters
app.get('/auth', passport.authenticate('oidc', [options]));
// authentication callback
app.get('/auth/cb', passport.authenticate('oidc', { successRedirect: '/', failureRedirect: '/login' }));
It is possible the RP or OP environment has a system clock skew, to set a clock tolerance (in seconds)
client.CLOCK_TOLERANCE = 5; // to allow a 5 second skew
Setting defaultHttpOptions
on Issuer
always merges your passed options with the default.
openid-client uses got for http requests with the following default request options
const DEFAULT_HTTP_OPTIONS = {
followRedirect: false,
headers: { 'User-Agent': `${pkg.name}/${pkg.version} (${pkg.homepage})` },
retries: 0,
timeout: 1500,
};
You can add your own headers, change the user-agent used or change the timeout setting
Issuer.defaultHttpOptions = { timeout: 2500, headers: { 'X-Your-Header': '<whatever>' } };
Confirm your httpOptions by
console.log('httpOptions %j', Issuer.defaultHttpOptions);
Version 1.6.1
FAQs
OpenID Connect Relying Party (RP, Client) implementation for Node.js runtime, supports passportjs
The npm package openid-client receives a total of 1,938,603 weekly downloads. As such, openid-client popularity was classified as popular.
We found that openid-client demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
ESLint has added JSON and Markdown linting support with new officially-supported plugins, expanding its versatility beyond JavaScript.
Security News
Members Hub is conducting large-scale campaigns to artificially boost Discord server metrics, undermining community trust and platform integrity.
Security News
NIST has failed to meet its self-imposed deadline of clearing the NVD's backlog by the end of the fiscal year. Meanwhile, CVE's awaiting analysis have increased by 33% since June.