Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

openid-client-request

joshbarr17MIT1.10.2

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

openid, connect, client, relying, party, oidc, auth, authentication, identity, oauth, passport, passportjs, strategy, certified, dynamic, config, basic, hybrid, implicit, oauth2

readme

openid-client (with request)

build codecov

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

Differences from openid-client

This package has been modified to use the request library for making HTTP connections, rather than using got. This makes it more compatible with tunneling agents where you might be behind a proxy.

Table of Contents

Implemented specs & features

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

Certification

OpenID 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

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.

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.

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

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.

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

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

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

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

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 params
  // 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'
}

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

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

changelog

openid-client CHANGELOG

Yay for SemVer.

Table of Contents

Version 1.10.0

  • DIFF
  • added pure OAuth 2.0 stripped down callback function #oauthCallback
  • added an extra option for #userinfo requests to have extra params in either query or body

Version 1.9.0

  • DIFF
  • added introspection/revocation specific client and issuer properties. To remain backwards compatible they default to their token endpoint counterparts
    • issuer.revocation_endpoint_auth_methods_supported
    • issuer.introspection_endpoint_auth_methods_supported
    • issuer.revocation_endpoint_auth_signing_alg_values_supported
    • issuer.introspection_endpoint_auth_signing_alg_values_supported
    • client.revocation_endpoint_auth_method
    • client.introspection_endpoint_auth_method
    • client.revocation_endpoint_auth_signing_alg
    • client.introspection_endpoint_auth_signing_alg

Version 1.8.0

Version 1.8.2

  • DIFF
  • bumped node-jose dependency to avoid github tar.gz dependencies
  • adjusted token_endpoint_auth_method=none to how it should be

Version 1.8.0

  • DIFF
  • Issuer and Client now recognize custom properties, this is so that new Registry Contents do not require a new release of openid-client to be picked up. Custom properties are exposed as getters so long as they do not interfere with the object's Prototype and they are always available in #metadata getter.

Version 1.7.0

Version 1.7.2

  • DIFF
  • added missing check for webfinger issuer location protocol

Version 1.7.1

  • DIFF
  • added authorizationCallback support for submitting code_verifier
  • example now includes session management OP and RP frames

1.7.0 failed to publish properly, use 1.7.1 instead

Version 1.6.0

Version 1.6.4

  • DIFF
  • fixed receiving (correct) empty responses from revocation endpoints (#21)

Version 1.6.3

Version 1.6.2

  • DIFF
  • fixed verify callback skipping userinfo when userinfo_endpoint is not configured (#19)
  • removed mandatory checks from passport strategy, allowing i.e. implicit only OPs (#19)

Version 1.6.1

  • DIFF
  • fixed verify callback skipping userinfo call when arity says it should but no access token is present (#18)

Version 1.6.0

  • DIFF
  • added at_hash presence assertion for applicable (implicit) ID Token validation
  • added c_hash presence assertion for applicable (hybrid) ID Token validation from the authorization_endpoint

Version 1.5.0

Version 1.5.3

  • DIFF
  • fixed an ID Token validation for ID Token returned by Token Endpoint that includes c_hash

Version 1.5.2

  • DIFF
  • fixed passport strategy, have it use prototype instead of ES6 class syntax

Version 1.5.1

  • DIFF
  • fixed client_assertion aud claim for _jwt auth methods when used in introspection and revocation

Version 1.5.0

  • DIFF
  • added a passport.js strategy
  • added missing max_age, default_max_age related functionality
    • authorizationCallback now supports max_age check
    • clients with default_max_age use this default value automatically
    • when max_age is checked auth_time claim is mandatory and must be a number
  • added missing require_auth_time related functionality
    • clients with require_auth_time = true have the presence and format of auth_time claim validated
  • authorizationUrl and authorizationPost now removes null and undefined values and ensures parameters are stringified before passed to url.format
  • added client.CLOCK_TOLERANCE property, to allow for clock skew (in seconds)

Version 1.4.0

  • DIFF
  • deprecated passing keystore directly to Client#register, pass an object with keystore property instead
  • added the option to provide InitialAccessToken value to Client#register

Version 1.3.0

Version 1.3.1

  • DIFF
  • added error messages when expected response is missing

Version 1.3.0

  • DIFF
  • added #requestObject method to Client to return signed and/or encrypted Request Object

Version 1.2.0

  • DIFF
  • added #claims getter to TokenSets returned from authorizationCallback and refresh;

Version 1.1.0

  • DIFF
  • fixed unpacking aggregated claims with alg=none and no iss claim
  • fetching distributed claims now expects a JWT response, previously expected invalid OP responses

Version 1.0.0

Version 1.0.2

  • DIFF
  • fixed signed userinfo response validation in case iss, aud and similar ID Token claims are missing

Version 1.0.1

  • DIFF
  • Updated uuid dependency

Version 1.0.0

RP test tools are passing, no changes required from the library, API is declared stable, hence 1.0.0 release.

Migrating from 0.x to 1.0

  1. update your package.json file to "^1.0.0"
  2. sit back and relax, no breaking changes

pre 1.x changelog

4. Major version zero (0.y.z) is for initial development. Anything may change at any time.
   The public API should not be considered stable.

5. Version 1.0.0 defines the public API.