adapt-authoring-auth/lib/Authentication.js

import { App } from 'adapt-authoring-core'
import apidefs from './apidefs.js'
import AuthToken from './AuthToken.js'
import AuthUtils from './AuthUtils.js'
import AbstractAuthModule from './AbstractAuthModule.js'
/**
 * Handles the authentication of incoming requests
 * @memberof auth
 */
class Authentication {
  /**
   * Creates and instanciates the class
   * @return {Promise} Resolves with the instance
   */
  static async init (auth) {
    const instance = new Authentication()
    await instance.init(auth)
    return instance
  }

  /** @constructor */
  constructor () {
    /**
     * Registered authentication plugins
     * @type {Object}
     */
    this.plugins = {}
  }

  /**
   * Initialises the instance
   * @param {AuthModule} auth The app auth module instance
   * @return {Promise}
   */
  async init (auth) {
    const jsonschema = await App.instance.waitForModule('jsonschema')

    jsonschema.extendSchema('user', 'authuser')

    auth.router.addRoute({
      route: '/check',
      handlers: { get: this.checkHandler.bind(this) },
      meta: apidefs.check
    }, {
      route: '/disavow',
      handlers: { post: this.disavowHandler.bind(this) },
      meta: apidefs.disavow
    }, {
      route: '/generatetoken',
      handlers: { post: this.generateTokenHandler.bind(this) },
      meta: apidefs.generatetoken
    }, {
      route: '/tokens',
      handlers: { get: this.retrieveTokensHandler.bind(this) },
      meta: apidefs.tokens
    })
    auth.unsecureRoute(`${auth.router.path}/check`, 'get')
    auth.secureRoute(`${auth.router.path}/disavow`, 'post', ['disavow:auth'])
    auth.secureRoute(`${auth.router.path}/generatetoken`, 'post', ['generatetoken:auth'])
    auth.secureRoute(`${auth.router.path}/tokens`, 'get', ['read:me'])
  }

  /**
   * Registers a module to be used for authentication
   * @param {String} type Identifier for the module
   * @param {AbstractAuthModule} instance The auth module to register
   */
  registerPlugin (type, instance) {
    if (this.plugins[type]) {
      throw App.instance.errors.DUPL_AUTH_PLUGIN_REG
        .setData({ name: type })
    }
    if (!(instance instanceof AbstractAuthModule)) {
      throw App.instance.errors.AUTH_PLUGIN_INVALID_CLASS
        .setData({ name: type })
    }
    AuthUtils.log('debug', 'AUTH_PLUGIN', type)
    this.plugins[type] = instance
  }

  /**
   * Shortcut to authentication helper function
   * @param {String} authType Authentication type
   * @param {Object} userData Data to be inserted if user doesn't exist
   * @return {Promise}
   */
  async registerUser (authType, userData) {
    const authPlugin = this.plugins[authType]
    if (!authPlugin) {
      throw App.instance.errors.NOT_FOUND
        .setData({ id: authType, type: 'auth plugin' })
    }
    const users = await App.instance.waitForModule('users')
    return users.insert({ ...userData, authType }, { schemaName: authPlugin.userSchema })
  }

  /**
   * Deauthenticates a user
   * @param {object} query Token search query
   * @return {Promise}
   */
  async disavowUser (query) {
    if (!query.userId) {
      throw App.instance.errors.INVALID_PARAMS.setData({ params: ['userId'] })
    }
    const users = await App.instance.waitForModule('users')
    await users.find({ _id: query.userId })
    return AuthToken.revoke(query)
  }

  /**
   * Verifies the incoming request is correctly authenticated
   * @param {external:ExpressRequest} req
   * @param {external:ExpressResponse} res
   * @param {Function} next
   */
  async checkHandler (req, res, next) {
    try {
      if (!req.auth.header) {
        throw App.instance.errors.UNAUTHENTICATED
      }
      await AuthToken.initRequestData(req)

      res.json({
        scopes: req.auth.scopes,
        isSuper: req.auth.isSuper,
        user: {
          _id: req.auth.user._id,
          email: req.auth.user.email,
          firstName: req.auth.user.firstName,
          lastName: req.auth.user.lastName,
          roles: req.auth.user.roles
        }
      })
    } catch (e) {
      AuthUtils.log('debug', 'ACCESS_BLOCKED', e.code, req?.auth?.user?._id?.toString())
      res.sendError(e)
    }
  }

  /**
   * Verifies the incoming request is correctly authenticated
   * @param {external:ExpressRequest} req
   * @param {external:ExpressResponse} res
   * @param {Function} next
   */
  async disavowHandler (req, res, next) {
    try {
      const sessions = await App.instance.waitForModule('sessions')
      await this.disavowUser({ userId: req.auth.user._id, signature: req.auth.token.signature })
      await sessions.clearSession(req)
    } catch (e) {
      return next(e)
    }
    res.status(204).end()
  }

  /**
   * Handles token generation requests
   * @param {external:ExpressRequest} req
   * @param {external:ExpressResponse} res
   * @param {Function} next
   */
  async generateTokenHandler (req, res, next) {
    try {
      res.json({ token: await AuthToken.generate(req.auth.user.authType, req.auth.user, { lifespan: req.body.lifespan }) })
    } catch (e) {
      return next(e)
    }
  }

  /**
   * Handles token retrieval requests
   * @param {external:ExpressRequest} req
   * @param {external:ExpressResponse} res
   * @param {Function} next
   */
  async retrieveTokensHandler (req, res, next) {
    try {
      res.json(await AuthToken.find({ userId: req.auth.user._id }, { sanitise: true }))
    } catch (e) {
      return next(e)
    }
  }
}

export default Authentication