/**
* @module flitter-auth/oauth2/Oauth2Provider
*/
const Provider = require('../Provider')
const Axios = require('axios')
const QS = require('qs')
/**
* Authentication provider for linking in OAuth2 sources.
* @extends module:flitter-auth/Provider~Provider
*/
class Oauth2Provider extends Provider {
/**
* Defines the services required by this provider.
* @returns {Array<string>}
*/
static get services() {
return [...super.services, 'configs']
}
/**
* Get the view for the login page.
* @returns {string}
*/
login_view() {
return 'auth:form_page'
}
/**
* Build the URL where the user should be redirected to sign
* into the OAuth2 provider.
* @returns {string}
* @private
*/
_get_landing_url() {
const base = this.config.source_login_page
let my_url = this.configs.get('app.url')
if ( !my_url.endsWith('/') ) my_url += '/'
return base.replace(/%c/g, this.config.source_client_id)
.replace(/%r/g, `${my_url}auth/${this.config.name}/login`)
}
/**
* Send the login page view to the response.
* @param {express/response} res - the response
* @returns {Promise<*>}
* @private
*/
_redirect_prompt(res) {
return res.page(this.login_view(), {
title: 'Login',
message: `Hi, there! Sign-in with ${this.config.source_name} to continue.`,
button_text: 'Continue',
button_link: this._get_landing_url(),
})
}
/**
* Updates the attributes on the provided {module:flitter-auth/model/User~User}
* instance with the specified data.
* @param {module:flitter-auth/model/User~User} user - the user to update
* @param {object} data - the data attributes to update
* @returns {Promise<*>}
* @private
*/
async _update_user_attributes(user, data) {
const attrs = this.config.user_data.attributes
Object.keys(attrs).forEach(user_key => {
const data_key = attrs[user_key]
if ( !(['uuid', 'uid'].includes(user_key)) && data[data_key] ) {
user[user_key] = data[data_key]
}
})
user.data_set('oauth2_retrieval_data', data)
return user
}
/**
* Get the user object that matches the specified data.
* (Data should contain the configured user ID.)
* @param {object} data - the user information, including the UID field
* @returns {Promise<module:flitter-auth/model/User~User>} - the associated user object
* @private
*/
async _get_user_object(data){
const User = this.User
const uid = data[this.config.user_data.attributes.uid]
let user = await User.findOne({ uid, provider: this.config.name })
if ( !user ){
user = new User({ uid, provider: this.config.name })
}
return await this._update_user_attributes(user, data)
}
/**
* Given a bearer token, make a request to configured user endpoint
* and build a user object from the resultant data.
* @param {string} bearer - the bearer token
* @returns {Promise<module:flitter-auth/model/User~User|boolean>} - User instance if possible, or false if it couldn't be retrieved
* @private
*/
async _build_user_from_bearer(bearer) {
const req_conf = {
headers: {
'Authorization': `${this.config.user_data.token_prefix ? this.config.user_data.token_prefix : 'Bearer '}${bearer}`,
}
}
const method = this.config.user_data.method
const endpoint = this.config.user_data.endpoint
try {
const response = await Axios[method](endpoint, req_conf)
let user_data = response.data;
if ( this.config.user_data.data_root ) user_data = user_data[this.config.user_data.data_root]
const user = await this._get_user_object(user_data)
if ( user.block_login ) return false
await user.save()
return user
} catch (e) {
return false
}
}
/**
* Handles a GET request to the register route.
* @param {express/request} req - the request
* @param {express/response} res - the response
* @param {function} next - the next function in the stack
* @returns {Promise<*>}
*/
async handle_register_get(req, res, next) {
return this.handle_login_get(req, res, next)
}
/**
* Handles a GET request to the login route.
* @param {express/request} req - the request
* @param {express/response} res - the response
* @param {function} next - the next function in the stack
* @returns {Promise<*>}
*/
async handle_login_get(req, res, next) {
const config = this.config
// TODO allow for POST returns as well
if ( !req.query || !req.query[config.callback.token_key] ) {
return this._redirect_prompt(res)
} else {
const code = req.query[config.callback.token_key]
// Make a request to the OAuth2 server to redeem our auth token for a bearer token
let spec_endpoint = new URL(config.source_token.endpoint)
try {
const body = {}
if ( config.source_token.client_id_key ) {
body[config.source_token.client_id_key] = config.source_client_id
}
if ( config.source_token.client_secret_key ) {
body[config.source_token.client_secret_key] = config.source_client_secret
}
if ( config.source_token.grant_type_key ) {
body[config.source_token.grant_type_key] = 'authorization_code'
}
if ( config.source_token.token_key ) {
body[config.source_token.token_key] = code
}
const req_conf = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json',
}
}
const response = await Axios.post(spec_endpoint.toString(), QS.stringify(body), req_conf)
const bearer = response.data[config.source_token.response_token_key]
const user = await this._build_user_from_bearer(bearer)
if ( !user ) return res.error(500)
await this.session(req, user)
if ( req.session.auth.flow ){
const destination = req.session.auth.flow
req.session.auth.flow = false
return res.redirect(destination)
}
return res.redirect(this.configs.get('auth.default_login_route'))
} catch (e) {
return this._redirect_prompt(res)
}
}
}
}
module.exports = exports = Oauth2Provider