bcgov/common-object-management-service

View on GitHub
app/src/middleware/authentication.js

Summary

Maintainability
B
5 hrs
Test Coverage
A
100%
const Problem = require('api-problem');
const config = require('config');
const basicAuth = require('express-basic-auth');
const jwt = require('jsonwebtoken');

const { AuthType } = require('../components/constants');
const { getConfigBoolean } = require('../components/utils');
const { userService } = require('../services');

/**
 * Basic Auth configuration object
 * @see {@link https://github.com/LionC/express-basic-auth}
 */
const _basicAuthConfig = {
  // Must be a synchronous function
  authorizer: (username, password) => {
    const userMatch = basicAuth.safeCompare(username, config.get('basicAuth.username'));
    const pwMatch = basicAuth.safeCompare(password, config.get('basicAuth.password'));
    return userMatch & pwMatch;
  },
  unauthorizedResponse: () => {
    return new Problem(401, { detail: 'Invalid authorization credentials' });
  }
};

/**
 * An express middleware function that checks basic authentication validity
 * @see {@link https://github.com/LionC/express-basic-auth}
 */
const _checkBasicAuth = basicAuth(_basicAuthConfig);

/**
 * @function _spkiWrapper
 * Wraps an SPKI key with PEM header and footer
 * @param {string} spki The PEM-encoded Simple public-key infrastructure string
 * @returns {string} The PEM-encoded SPKI with PEM header and footer
 */
const _spkiWrapper = (spki) => `-----BEGIN PUBLIC KEY-----\n${spki}\n-----END PUBLIC KEY-----`;

/**
 * @function currentUser
 * Injects a currentUser object to the request if there exists valid authentication artifacts.
 * Subsequent logic should check `req.currentUser.authType` for authentication method if needed.
 * @param {object} req Express request object
 * @param {object} res Express response object
 * @param {function} next The next callback function
 * @returns {function} Express middleware function
 * @throws The error encountered upon failure
 */
const currentUser = async (req, res, next) => {
  const authorization = req.get('Authorization');
  const currentUser = {
    authType: AuthType.NONE
  };

  if (authorization) {
    // Basic Authorization
    if (getConfigBoolean('basicAuth.enabled') && authorization.toLowerCase().startsWith('basic ')) {
      currentUser.authType = AuthType.BASIC;
    }

    // OIDC JWT Authorization
    else if (getConfigBoolean('keycloak.enabled') && authorization.toLowerCase().startsWith('bearer ')) {
      currentUser.authType = AuthType.BEARER;

      try {
        const bearerToken = authorization.substring(7);
        let isValid = false;

        if (config.has('keycloak.publicKey')) {
          const publicKey = config.get('keycloak.publicKey');
          const pemKey = publicKey.startsWith('-----BEGIN')
            ? publicKey
            : _spkiWrapper(publicKey);
          isValid = jwt.verify(bearerToken, pemKey, {
            issuer: `${config.get('keycloak.serverUrl')}/realms/${config.get('keycloak.realm')}`
          });
        } else {
          throw new Error('OIDC environment variable KC_PUBLICKEY or keycloak.publicKey must be defined');
        }

        if (isValid) {
          currentUser.tokenPayload = typeof isValid === 'object' ? isValid : jwt.decode(bearerToken);
          await userService.login(currentUser.tokenPayload);
        } else {
          throw new Error('Invalid authorization token');
        }
      } catch (err) {
        return next(new Problem(403, { detail: err.message, instance: req.originalUrl }));
      }
    }
  }

  // Inject currentUser data into request
  req.currentUser = Object.freeze(currentUser);

  // Continue middleware stack based on detected AuthType
  if (currentUser.authType === AuthType.BASIC) {
    _checkBasicAuth(req, res, next);
  }
  else next();
};

module.exports = {
  _basicAuthConfig, _checkBasicAuth, currentUser, _spkiWrapper
};