Source: utils/auth.js

'use strict';

/**
 * Some helper functions/utilities for authentication.
 * @module utils/auth
 * @license MIT
 * @author Kai KRETSCHMANN <kai@kretschmann.consulting>
 */

const UsersService = require('../service/UsersService');

const jwt = require('jsonwebtoken');
require('custom-env').env(true);
const sharedSecret = process.env.JWT_SECRET;
const issuer = process.env.JWT_ISSUER;

const log4js = require('log4js');
const logger = log4js.getLogger();
logger.level = process.env.LOGLEVEL || /* istanbul ignore next */ 'warn';

/**
 * Verify a given token from bearer http header request.
 * @method verifyToken
 * @public
 * @async
 * @param {object} req The complete web request
 * @param {string} _scopes Not used here
 * @param {object} schema Secruity schema used for this API method
 * @returns {boolean} true if valid token found in request
 */
exports.verifyToken = async function (req, _scopes, schema) {
  logger.info('In verifyToken');

  const currentReqScopes = req.openapi.schema['x-security-scopes']
  logger.info(`currentReqScopes: ${currentReqScopes}`);
  logger.info('schema:');
  logger.info(schema);

  const token = req.headers.authorization;

  logger.info(`token header: ${token}`);

  // validate the 'Authorization' header. it should have the following format:
  // 'Bearer tokenString'
  if (token && token.indexOf('Bearer ') === 0) {
    const tokenString = token.split(' ')[1];
    const untrustedDecodedToken = jwt.decode(tokenString);
    let trustedDecodedToken = '';

    try {
      trustedDecodedToken = jwt.verify(tokenString, sharedSecret);
    } catch (err) {
      logger.error(`JWT verify failed: ${err}`);
      logger.error(err.message);
      logger.error(`Token data, user=${untrustedDecodedToken.sub}, IP=${req.realip}`);

      if (err.name === 'TokenExpiredError') {
        await UsersService.logAuthExpired(untrustedDecodedToken.sub, req.realip, err.expiredAt);
      } else {
        await UsersService.logAuthFailure(untrustedDecodedToken.sub, req.realip);
      }

      if (req.sentry) {
        req.sentry.setUser({ username: untrustedDecodedToken.sub, ip_address: req.realip });
        req.sentry.setContext('JWT', {
          msg: err.message
        });
        req.sentry.captureException(err);
      }
      throw (err);
    }

    logger.debug('Decoded token:');
    logger.debug(trustedDecodedToken);
    if (!trustedDecodedToken) {
      logger.error('Decode failed');
      await UsersService.logAuthFailure(untrustedDecodedToken.sub, req.realip);
      return false;
    }

    // check if the JWT was verified correctly
    if (trustedDecodedToken.role) {
      logger.info(`User has role ${trustedDecodedToken.role} in JWT`);

      // check if the issuer matches
      const issuerMatch = trustedDecodedToken.iss === issuer;
      if (!issuerMatch) {
        await UsersService.logAuthFailure(untrustedDecodedToken.sub, req.realip);
        logger.error('issuer doesn\'t match');
        return false;
      }

      // Check if users role matches api precondition, will return if OK, otherwise jump out
      logger.debug('Check active users role matching API requirements');
      await UsersService.isActiveHasRole(trustedDecodedToken.sub, currentReqScopes);

      req.auth = trustedDecodedToken;
      logger.debug('Added AUTH to request object');

      // Add user data to sentry
      req.sentry.setUser({ username: trustedDecodedToken.sub, ip_address: req.realip });

      // store success
      await UsersService.logAuthSuccess(trustedDecodedToken.sub, req.realip, trustedDecodedToken.exp);

      return true;
    }
  } else { // not reached if API filters right before
    logger.error('Token without Bearer keyword');
    logger.debug(req.headers);
    logger.debug(req);
    return false;
  }
};

/**
 * Helper function for issuing a web token
 * @method doIssue
 * @private
 * @param {string} username The textual user name
 * @param {string} therole The textual user role
 * @param {string} exprange The textual length in time
 * @returns {object} JWT token
 */
function doIssue (username, therole, exprange) {
  logger.info(`user ${username}, ${therole} role`);
  return jwt.sign(
    {
      sub: username,
      iss: issuer,
      role: therole
    },
    sharedSecret, {
      expiresIn: exprange
    }
  );
}

/**
 * Issue a valid web token for a given user and role.
 * Make it default valid duration.
 * @method issueToken
 * @public
 * @param {string} username The textual user name
 * @param {string} role The textual user role
 * @returns {object} JWT token
 */
exports.issueToken = function (username, role) {
  return doIssue(username, role, '365d');
};

/**
 * Issue a short valid web token for a given user and role.
 * Make it short testing valid duration.
 * @method issueShortToken
 * @public
 * @param {string} username The textual user name
 * @param {string} role The textual user role
 * @returns {object} JWT token
 */
exports.issueShortToken = function (username, role) {
  return doIssue(username, role, '1s');
};