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