auth/controllers/Oauth2.js

const Controller = require('libflitter/controller/Controller')

/**
 * @module flitter-auth/controllers/Oauth2
 */

/**
 * Provides default handlers for OAuth2 authorization and data retrieval
 * @extends module:libflitter/controller/Controller~Controller
 */
class Oauth2 extends Controller {
    /**
     * Get the services required by this unit.
     * @returns {Array<string>}
     */
    static get services() {
        return [...super.services, 'configs', 'models']
    }

    /**
     * Send a response in a uniform, JSON-encoded format:
     * { success: Boolean, message: String, data: any }
     * @param {express/Response} res - the response
     * @param {string} message - the message to send
     * @param {boolean} [error = false] - true if an error was encountered
     * @param {object} [data = {}] - data to be returned
     * @private
     */
    _uniform(res, message, error = false, data = {}) {
        return res.send({
            message,
            success: !error,
            data
        })
    }

    /**
     * Based on the request query's client_id and redirect_uri, try to fetch the Oauth2Client
     * instance. If none is found (or invalid redirect URI), return false.
     * @param {express/Request} req - the express request
     * @returns {Promise<boolean|Oauth2Client>} - if valid params, return the corresponding client. Else, false.
     * @private
     */
    async _get_authorize_client(req) {
        const Client = this.models.get('auth::Oauth2Client')
        if ( !req.query || !req.query.client_id || !req.query.redirect_uri ) return false

        const client = await Client.findOne({clientID: req.query.client_id})
        if ( !client ) return false
        if ( !client.redirectUris.includes(req.query.redirect_uri) ) return false

        return client
    }

    /**
     * Show the authorize request approval view to the user. This view is passed the Oauth2Client
     * and the redirect URI as: {client: Oauth2Client, uri: URL}
     * @param {express/Request} req
     * @param {express/Response} res
     * @param {function} next
     * @returns {Promise<*>}
     */
    async authorize_get(req, res, next) {
        const client = await this._get_authorize_client(req)
        if ( !client ) return this._uniform(res, 'Unable to authorize client application. The application config is invalid. Please check the client ID and redirect URI and try again.')
        const uri = new URL(req.query.redirect_uri)
        const title = `Authorize ${client.name}?`

        return res.page('auth:oauth2_authorize.pug', {client, uri, title})
    }

    /**
     * Add the code to the URI's search query params as &code=<code>.
     * @param {URL} uri - the uri to modify
     * @param {string} code - the code to be added
     * @returns {URL} - the modified uri
     * @private
     */
    _encode_uri(uri, code) {
        let url = uri.toString()
        if ( uri.search.length < 1 ) url += '?'
        else url += '&'

        url += `code=${code}`

        return new URL(url)
    }

    /**
     * Called when an authorization request has been approved. Generates a single-use
     * authorization ticket for the client, adds that ticket's code to the redirect URI
     * params, then redirects the user to the client application.
     * @param {express/Request} req
     * @param {express/Response} res
     * @param {function} next
     * @returns {Promise<*>}
     */
    async authorize_post(req, res, next) {
        const Oauth2AuthorizationTicket = this.models.get('auth::Oauth2AuthorizationTicket')
        const client = await this._get_authorize_client({query: req.body})
        if ( !client ) return this._uniform(res, 'Invalid post authorization, or invalid client config.')
        const uri = new URL(req.body.redirect_uri)

        const ticket = new Oauth2AuthorizationTicket({
            client_id: req.body.client_id,
            user_id: req.session.auth.user_id,
        })

        await ticket.save()

        const redirect = this._encode_uri(uri, ticket.token)
        return res.redirect(redirect.toString())
    }

    /**
     * Redeem an authorization ticket for an OAuth2 bearer token.
     * @param {express/Request} req
     * @param {express/Response} res
     * @param {function} next
     * @returns {Promise<*>}
     */
    async redeem_token(req, res, next) {
        return req.app.oauth2.grant()(req, res, next)
    }

    /**
     * From the user authenticated by the request's bearer token, get the
     * data elements configured in the auth.servers.oauth2.built_in_endpoints.user
     * config and return them as a JSON object. Expects req.user.id to be set.
     * @param {express/Request} req
     * @param {express/Response} res
     * @param {function} next
     * @returns {Promise<*>}
     */
    async data_user_get(req, res, next) {
        const User = this.models.get('auth:User')
        const user = await User.findById(req.user.id)
        const config = this.configs.get('auth.servers.oauth2')

        const include_fields = config.built_in_endpoints.user.fields

        const data = {}
        Object.keys(include_fields).forEach(data_field => {
            const user_field = include_fields[data_field]

            if ( data_field !== 'data' ) {
                data[data_field] = user[user_field]
            } else {
                // Pull data from the serialized JSON
                Object.keys(user_field).forEach(json_data_key => {
                    const json_user_key = user_field[json_data_key]
                    data[json_data_key] = user.data_get(json_user_key)
                })
            }
        })

        this._uniform(res, 'Success', false, data)
    }
}

module.exports = exports = Oauth2