2017-06-27 13:08:05 -04:00
|
|
|
'use strict'
|
|
|
|
|
|
|
|
const Router = require('express').Router
|
|
|
|
const passport = require('passport')
|
2018-11-14 04:39:43 -05:00
|
|
|
const { Strategy, InternalOAuthError } = require('passport-oauth2')
|
2017-06-27 13:08:05 -04:00
|
|
|
const config = require('../../../config')
|
2020-11-21 14:26:12 -05:00
|
|
|
const logger = require('../../../logger')
|
2019-11-28 06:25:59 -05:00
|
|
|
const { passportGeneralCallback } = require('../utils')
|
2017-06-27 13:08:05 -04:00
|
|
|
|
2021-02-15 03:42:51 -05:00
|
|
|
const oauth2Auth = module.exports = Router()
|
2017-06-27 13:08:05 -04:00
|
|
|
|
2018-11-14 04:39:43 -05:00
|
|
|
class OAuth2CustomStrategy extends Strategy {
|
2017-06-27 13:08:05 -04:00
|
|
|
constructor (options, verify) {
|
|
|
|
options.customHeaders = options.customHeaders || {}
|
|
|
|
super(options, verify)
|
|
|
|
this.name = 'oauth2'
|
|
|
|
this._userProfileURL = options.userProfileURL
|
|
|
|
this._oauth2.useAuthorizationHeaderforGET(true)
|
|
|
|
}
|
|
|
|
|
|
|
|
userProfile (accessToken, done) {
|
|
|
|
this._oauth2.get(this._userProfileURL, accessToken, function (err, body, res) {
|
2021-02-15 03:42:51 -05:00
|
|
|
let json
|
2017-06-27 13:08:05 -04:00
|
|
|
|
|
|
|
if (err) {
|
2018-11-14 04:39:43 -05:00
|
|
|
return done(new InternalOAuthError('Failed to fetch user profile', err))
|
2017-06-27 13:08:05 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
json = JSON.parse(body)
|
|
|
|
} catch (ex) {
|
|
|
|
return done(new Error('Failed to parse user profile'))
|
|
|
|
}
|
|
|
|
|
2020-11-21 14:26:12 -05:00
|
|
|
checkAuthorization(json, done)
|
2021-02-15 03:42:51 -05:00
|
|
|
const profile = parseProfile(json)
|
2017-06-27 13:08:05 -04:00
|
|
|
profile.provider = 'oauth2'
|
|
|
|
|
|
|
|
done(null, profile)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function extractProfileAttribute (data, path) {
|
|
|
|
// can handle stuff like `attrs[0].name`
|
|
|
|
path = path.split('.')
|
|
|
|
for (const segment of path) {
|
|
|
|
const m = segment.match(/([\d\w]+)\[(.*)\]/)
|
|
|
|
data = m ? data[m[1]][m[2]] : data[segment]
|
|
|
|
}
|
|
|
|
return data
|
|
|
|
}
|
|
|
|
|
|
|
|
function parseProfile (data) {
|
2020-11-30 09:04:30 -05:00
|
|
|
// only try to parse the id if a claim is configured
|
|
|
|
const id = config.oauth2.userProfileIdAttr ? extractProfileAttribute(data, config.oauth2.userProfileIdAttr) : undefined
|
2017-06-27 13:08:05 -04:00
|
|
|
const username = extractProfileAttribute(data, config.oauth2.userProfileUsernameAttr)
|
|
|
|
const displayName = extractProfileAttribute(data, config.oauth2.userProfileDisplayNameAttr)
|
|
|
|
const email = extractProfileAttribute(data, config.oauth2.userProfileEmailAttr)
|
|
|
|
|
|
|
|
return {
|
2020-11-21 14:26:12 -05:00
|
|
|
id: id || username,
|
2017-06-27 13:08:05 -04:00
|
|
|
username: username,
|
|
|
|
displayName: displayName,
|
|
|
|
email: email
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-21 14:26:12 -05:00
|
|
|
function checkAuthorization (data, done) {
|
2020-11-30 09:04:30 -05:00
|
|
|
// a role the user must have is set in the config
|
2020-11-21 14:26:12 -05:00
|
|
|
if (config.oauth2.accessRole) {
|
2020-11-30 09:04:30 -05:00
|
|
|
// check if we know which claim contains the list of groups a user is in
|
|
|
|
if (!config.oauth2.rolesClaim) {
|
|
|
|
// log error, but accept all logins
|
|
|
|
logger.error('oauth2: "accessRole" is configured, but "rolesClaim" is missing from the config. Can\'t check group membership!')
|
|
|
|
} else {
|
|
|
|
// parse and check role data
|
|
|
|
const roles = extractProfileAttribute(data, config.oauth2.rolesClaim)
|
|
|
|
if (!roles) {
|
|
|
|
logger.error('oauth2: "accessRole" is configured, but user profile doesn\'t contain roles attribute. Permission denied')
|
|
|
|
return done('Permission denied', null)
|
|
|
|
}
|
|
|
|
if (!roles.includes(config.oauth2.accessRole)) {
|
|
|
|
const username = extractProfileAttribute(data, config.oauth2.userProfileUsernameAttr)
|
|
|
|
logger.debug(`oauth2: user "${username}" doesn't have the required role. Permission denied`)
|
|
|
|
return done('Permission denied', null)
|
|
|
|
}
|
2020-11-21 14:26:12 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-06-27 13:08:05 -04:00
|
|
|
OAuth2CustomStrategy.prototype.userProfile = function (accessToken, done) {
|
|
|
|
this._oauth2.get(this._userProfileURL, accessToken, function (err, body, res) {
|
2021-02-15 03:42:51 -05:00
|
|
|
let json
|
2017-06-27 13:08:05 -04:00
|
|
|
|
|
|
|
if (err) {
|
2018-11-14 04:39:43 -05:00
|
|
|
return done(new InternalOAuthError('Failed to fetch user profile', err))
|
2017-06-27 13:08:05 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
json = JSON.parse(body)
|
|
|
|
} catch (ex) {
|
|
|
|
return done(new Error('Failed to parse user profile'))
|
|
|
|
}
|
|
|
|
|
2020-11-21 14:26:12 -05:00
|
|
|
checkAuthorization(json, done)
|
2021-02-15 03:42:51 -05:00
|
|
|
const profile = parseProfile(json)
|
2017-06-27 13:08:05 -04:00
|
|
|
profile.provider = 'oauth2'
|
|
|
|
|
|
|
|
done(null, profile)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
passport.use(new OAuth2CustomStrategy({
|
|
|
|
authorizationURL: config.oauth2.authorizationURL,
|
|
|
|
tokenURL: config.oauth2.tokenURL,
|
|
|
|
clientID: config.oauth2.clientID,
|
|
|
|
clientSecret: config.oauth2.clientSecret,
|
|
|
|
callbackURL: config.serverURL + '/auth/oauth2/callback',
|
2020-06-20 10:33:57 -04:00
|
|
|
userProfileURL: config.oauth2.userProfileURL,
|
2020-06-16 04:45:23 -04:00
|
|
|
scope: config.oauth2.scope,
|
|
|
|
state: true
|
2017-06-27 13:08:05 -04:00
|
|
|
}, passportGeneralCallback))
|
|
|
|
|
|
|
|
oauth2Auth.get('/auth/oauth2', function (req, res, next) {
|
|
|
|
passport.authenticate('oauth2')(req, res, next)
|
|
|
|
})
|
|
|
|
|
|
|
|
// github auth callback
|
|
|
|
oauth2Auth.get('/auth/oauth2/callback',
|
|
|
|
passport.authenticate('oauth2', {
|
2018-11-27 09:13:18 -05:00
|
|
|
successReturnToOrRedirect: config.serverURL + '/',
|
|
|
|
failureRedirect: config.serverURL + '/'
|
2017-06-27 13:08:05 -04:00
|
|
|
})
|
|
|
|
)
|