adapt-authoring-auth/lib/Authentication.js

import { App, readJson } from 'adapt-authoring-core'
import { registerRoutes } from 'adapt-authoring-server'
import AuthToken from './AuthToken.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.registerSchemasHook.tap(this.registerSchema.bind(this))
    this.registerSchema()

    const { routes } = await readJson(`${import.meta.dirname}/../routes.json`)
    for (const r of routes) {
      r.handlers = Object.fromEntries(
        Object.entries(r.handlers).map(([m, h]) => [m, this[h].bind(this)])
      )
    }
    registerRoutes(auth.router, routes, auth)
  }

  /**
   * 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 })
    }
    App.instance.logger.log('debug', 'auth', 'AUTH_PLUGIN', type)
    this.plugins[type] = instance
  }

  /**
   * Adds user schema extension
   */
  async registerSchema () {
    const jsonschema = await App.instance.waitForModule('jsonschema')
    jsonschema.extendSchema('user', 'authuser')
  }

  /**
   * 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) {
      App.instance.logger.log('debug', 'auth', '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('manual', 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