Have ideas to improve npm?Join in the discussion! »

    openid-client-request

    1.10.2 • Public • Published

    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.

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

    Install

    npm i openid-client-request

    DownloadsWeekly Downloads

    0

    Version

    1.10.2

    License

    MIT

    Last publish

    Collaborators

    • avatar