Source: server/util/AuthenticationHelper/OidcClient.js

import jwt from 'jsonwebtoken';
import stringify from 'qs/lib/stringify';
import debugLib from 'debug';
import fetch from 'node-fetch';
import serviceConfig from '../../../app/config/service.configdefinitions';
import routeToUrl from './routeToUrl';
import { OIDC_REQUEST_TIMEOUT, JWT_CLOCK_TOLERANCE } from './constants';
import hsAlgorithmData from '../../../app/data/hsAlgorithmData';

const debug = debugLib('SlimmingWorld:AuthenticationHelper');

/** @module */

/**
 * Helper class that takes care of communicating with the OIDC authentication endpoint
 * @default
 */
class OidcClient {
  constructor(oidcConfig, oidcDiscovery) {
    this.oidcDiscovery = oidcDiscovery;
    this.config = oidcConfig;
    this.jwtConfig = this.config.jwt || {
      audience: oidcConfig.audience || oidcConfig.client_id,
      issuer: oidcConfig.issuer,
    };
    // add 60 seconds leeway for time difference between servers
    this.jwtConfig.clockTolerance = JWT_CLOCK_TOLERANCE;
  }

  get scope() {
    return serviceConfig.clientScopes.concat(this.config.scopes || []).join(' ');
  }

  login(req, res, state) {
    return this.oidcDiscovery.discoverComplete.then(() => {
      /* eslint-disable */
      const params = {
        state,
        response_type: 'code',
        client_id: this.config.client_id,
        redirect_uri: routeToUrl(req, req.headers.host, this.config['callbackRoute']),
        scope: this.scope,
      };
      /* eslint-enable */
      const endpoint = this.oidcDiscovery.providerConfig.authorization_endpoint;
      const joinChar = endpoint.indexOf('?') === -1 ? '?' : '&';

      debug(`login redirect params: ${params}`);

      return res.redirect(`${endpoint}${joinChar}${stringify(params)}`);
    });
  }

  /**
   * This logout method is not used anymore in the new situation. But since it's still part
   * of the OidcClient, i keep it here for now
   *
   * @param res
   * @param authTokens
   */
  logout(res, authTokens) {
    /* eslint-disable */
    const params = {
      post_logout_redirect_uri: '/',
    };
    /* eslint-enable */

    const tokenValidation =
      authTokens && authTokens.id_token
        ? this.validateToken(authTokens.id_token).then(valid => {
            if (valid) {
              /* eslint-disable */
              params.id_token_hint = authTokens.id_token;
              /* eslint-enable */
            }
          })
        : Promise.resolve();

    tokenValidation.then(() => {
      const endpoint = this.oidcDiscovery.providerConfig.end_session_endpoint;
      const joinChar = endpoint.indexOf('?') === -1 ? '?' : '&';
      return res.redirect(`${endpoint}${joinChar}${stringify(params)}`);
    });
  }

  getTokens(req) {
    /* eslint-disable */
    const { ius, code } = req.query;
    const params = {
      grant_type: 'authorization_code',
      code,
      redirect_uri:
        routeToUrl(req, req.headers.host, this.config['callbackRoute']) +
        (ius ? `?ius=${ius}` : ''),
    };
    /* eslint-enable */

    debug(`getTokens params: ${params}`);

    return this.oidcDiscovery.discoverComplete
      .then(() => this.getAccessToken(params))
      .then(tokens => {
        const valid = this.validateTokenSync(tokens.id_token);
        if (valid) {
          return tokens;
        }
        debug('Received invalid ID token on OpenID callback endpoint');
        return null;
      });
  }

  validateToken = token =>
    this.oidcDiscovery.discoverComplete.then(() => this.validateTokenSync(token));

  validateTokenSync = token => {
    const { header: jwtHeader } = jwt.decode(token, { complete: true });
    if (jwtHeader && jwtHeader.alg && hsAlgorithmData.includes(jwtHeader.alg)) return true;

    try {
      // ID Token Validation [http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation]
      return jwt.verify(token, this.oidcDiscovery.publicKey, this.jwtConfig);
    } catch (e) {
      debug(`There was an error during validation of the ID token: ${e}`);
      return false;
    }
  };

  getAccessToken(_params) {
    /* eslint-disable */
    const params = {
      ..._params,
      client_id: this.config.client_id,
      client_secret: this.config.client_secret,
    };
    /* eslint-enable */

    return fetch(this.oidcDiscovery.providerConfig.token_endpoint, {
      method: 'POST',
      body: Object.keys(params)
        .map(param => `${param}=${encodeURIComponent(params[param])}`)
        .join('&'),
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      timeout: OIDC_REQUEST_TIMEOUT,
    }).then(res => res.json());
  }
}

export default OidcClient;