auth/SecurityContext.js

/**
 * @module flitter-auth/SecurityContext
 */

const { Injectable } = require('flitter-di')

/**
 * Request-specific security context that provides helper functions
 * with regard to security checks for the relevant request.
 * @extends module:flitter-di/src/Injectable~Injectable
 */
class SecurityContext extends Injectable {
    /**
     * Defines the services required by this unit.
     * @returns {Array<string>}
     */
    static get services() {
        return [...super.services, 'configs', 'auth', 'models']
    }

    /**
     * The relevant request.
     * @type {express/request}
     */
    #request

    /**
     * The relevant response.
     * @type {express/response}
     */
    #response

    /**
     * Instantiate the security context.
     * @param {express/request} req - the relevant request
     * @param {express/response} res - the relevant response
     */
    constructor(req, res) {
        super()
        this.#request = req
        this.#response = res
    }

    /**
     * Deny the client access to the requested resource.
     * Displays the 401 error page and passes along the specified message.
     * @param {string} [message = 'Access Denied']
     */
    deny(message = 'Access Denied') {
        this.#response.error(401, {message})
    }

    /**
     * Deny the client access to the requested resource.
     * Displays the 401 error page and passes along the specified message.
     * If the request has a user in the session, the user will be forcibly signed out.
     * @param {string} [message = 'Access Denied']
     */
    kickout(message = 'Access Denied') {
        if ( this.#request.user ) {
            this.provider().logout(this.#request).then(() => {
                this.deny(message)
            })
        } else {
            this.deny(message)
        }
    }

    /**
     * Deny the client access to the requested resource.
     * Displays the 401 error page and passes along the specified message.
     * If the request has a user in the session, the user's block_login flag will
     * be set, and they will be forcibly signed out.
     *
     * WARNING: this flag will prevent the user from signing into the application AT ALL.
     * @param {string} [message = 'Access Denied']
     */
    ban(message = 'Access Denied') {
        if ( this.#request.user ) {
            this.#request.user.block_login = true
            this.#request.user.save().then(() => {
                this.kickout(message)
            })
        } else {
            this.deny(message)
        }
    }

    /**
     * Get the name of the auth provider for the request.
     * If the request is authenticated, use the user's provider.
     * Otherwise, if a provider exists in the route params, use that.
     * Otherwise, use the default_provider specified in the config.
     * @returns {string}
     */
    provider_name() {
        let provider_name = this.configs.get('auth.default_provider')
        if ( this.#request.is_auth ) provider_name = this.#request.user.provider
        else provider_name = this.#request.params.provider ? this.#request.params.provider : provider_name
        return provider_name
    }

    /**
     * Get the auth provider for the request.
     * @returns {module:flitter-auth/Provider~Provider}
     */
    provider() {
        return this.auth.get_provider(this.provider_name())
    }

    /**
     * Generate a key action that will resolve to the specified handler.
     *
     * @example
     * const action = await request.security.key_action('controller::Home.password_reset')
     * return res.send(`Reset your password at: ${action.url()}`)
     * @param {string} handler - canonical name of the handler - e.g. "controller::Home.welcome"
     * @returns {Promise<module:flitter-auth/model/KeyAction~KeyAction>}
     */
    async key_action(handler) {
        const KeyAction = this.models.get('auth:KeyAction')
        const ka_data = { handler, used: false }
        if ( this.#request.user ) ka_data.user_id = this.#request.user._id

        const ka = new KeyAction(ka_data)
        await ka.save()
        return ka
    }
}

module.exports = exports = SecurityContext