npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@privoro/openid-client

v1.20.1

Published

OpenID Connect Relying Party (RP, Client) implementation for Node.js servers, supports passportjs

Downloads

21

Readme

openid-client

build codecov

openid-client is a server side OpenID Relying Party (RP, Client) implementation for Node.js, supports passport.

Table of Contents

Implemented specs & features

The following client/RP features from OpenID Connect/OAuth2.0 specifications are implemented by openid-client.

Certification


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.

build

Sponsor

Example

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.

Get started

On the off-chance you want to manage multiple clients for multiple issuers you need to first get an Issuer instance.

via Discovery (recommended)

const Issuer = require('openid-client').Issuer;
Issuer.discover('https://accounts.google.com') // => Promise
  .then(function (googleIssuer) {
    console.log('Discovered issuer %s', googleIssuer);
  });

manually

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.

manually (recommended)

You should provide at least 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'
}, [keystore]); // => Client

keystore is an optional argument for instantiating a client with configured asymmetrical ID Token or UserInfo response encryption.

via registration client uri

Should your oidc provider have provided you with a registration client uri and registration access token you can also have the Client discovered.

googleIssuer.Client.fromUri(registration_client_uri, registration_access_token, [keystore]) // => Promise
  .then(function (client) {
    console.log('Discovered client %s', client);
  });

keystore is an optional argument for instantiating a client through registration client uri with configured asymmetrical ID Token or UserInfo response encryption.

Usage

Getting authorization url

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)

Processing callback

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);
  });

Processing callback with state, nonce or max_age check

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);
  });

IdP Errors - OpenIdConnectError

When the OpenID Provider returns an OIDC formatted error from either authorization callbacks or any of the JSON responses the library will reject a given Promise with OpenIdConnectError instance.

The message of this error is "${error} (${error_description})". However the OpenIdConnectError object also has the following properties:

  • error
  • error_description
  • error_uri
  • state
  • scope

Values are undefined if these were not provided in the response. Additionally, for API call responses a response property is available with the response object from the used http client.

Handling multiple response modes

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' };

// example koa v2.x w/ koa-body
app.use(bodyParser({ patchNode: true }));
app.use(async function (ctx, next) {
  const params = client.callbackParams(ctx.request.req); // => parsed url query, url fragment or body object
  // ...
});

// example 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
  // ...
});

Refreshing a token

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;

Revoke a token

client.revoke(token, [tokenTypeHint]) // => Promise
  .then(function (response) {
    console.log('revoked token %s', token, response);
  });

Introspect a token

client.introspect(token, [tokenTypeHint]) // => Promise
  .then(function (response) {
    console.log('token details %j', response);
  });

Fetching userinfo

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

with extra query/body payload

client.userinfo(accessToken, { params: { fields: 'email,ids_for_business' } }); // => 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:

  • signed
  • signed and encrypted (nested JWT)
  • just encrypted

Fetching Distributed Claims

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

Unpacking Aggregated Claims

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

Custom token endpoint grants

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

Registering new client (via Dynamic Registration)

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);
  });

Generating a signed/encrypted Request Object

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)
});

WebFinger discovery

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.

TokenSet

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);
});

Usage with passport

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 request parameters go here
  // client_id defaults to client.client_id
  // redirect_uri defaults to client.redirect_uris[0]
  // response type defaults to client.response_types[0], then 'code'
  // scope defaults to 'openid'
}
const passReqToCallback = false; // optional, defaults to false, when true req is passed as a first
                                 // argument to verify fn

const usePKCE = true; // optional, defaults to false, when true the code_challenge_method will be
                      // resolved from the issuer configuration, instead of true you may provide
                      // any of the supported values directly, i.e. "S256" (recommended) or "plain"

passport.use('oidc', new Strategy({ client, [params], [passReqToCallback], [usePKCE] }, (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' }));

Configuration

Allow for system clock skew

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

Changing HTTP request defaults

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);

Proxy settings

Because of the lightweight nature of got library the client will not use environment-defined http(s) proxies. In order to have them used you'll need to either provide your own http request implementation using the provided httpClient setter or use the bundled request one.

Custom implementation:

/*
 * url {String}
 * options {Object}
 * options.headers {Object}
 * options.body {String|Object}
 * options.form {Boolean}
 * options.query {Object}
 * options.timeout {Number}
 * options.retries {Number}
 * options.followRedirect {Boolean}
 */

Issuer.httpClient = {
   get(url, options) {}, // return Promise
   post(url, options) {}, // return Promise
   HTTPError, // used error constructor
};

Bundled (and maintained + tested) request implementation after you've added request to your package.json bundle:

npm install request@^2.0.0 --save
Issuer.useRequest();