Files
hedgedoc-hedgeagent/lib/web/auth/oauth2/index.js
Erik Michelson 35f36fccba fix(auth): add state parameters and PKCE support
Only the OAuth2 auth strategy was using the state parameter,
which should be used as described in the RFC. The other
auth strategies such as GitHub, GitLab or Google were lacking
the state parameter.
This change adds the required state parameter as well as
enabling PKCE support on providers where it's possible.

Signed-off-by: Erik Michelson <github@erik.michelson.eu>
2025-12-05 22:06:30 +01:00

156 lines
4.9 KiB
JavaScript

'use strict'
const Router = require('express').Router
const passport = require('passport')
const { Strategy, InternalOAuthError } = require('passport-oauth2')
const config = require('../../../config')
const logger = require('../../../logger')
const { passportGeneralCallback } = require('../utils')
const oauth2Auth = module.exports = Router()
class OAuth2CustomStrategy extends Strategy {
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) {
let json, profile
if (err) {
return done(new InternalOAuthError('Failed to fetch user profile', err))
}
try {
json = JSON.parse(body)
} catch (ex) {
return done(new Error('Failed to parse user profile'))
}
checkAuthorization(json, done)
try {
profile = parseProfile(json)
} catch (ex) {
return done('Failed to identify user profile information', null)
}
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) {
// only try to parse the id if a claim is configured
const id = config.oauth2.userProfileIdAttr ? extractProfileAttribute(data, config.oauth2.userProfileIdAttr) : undefined
const username = extractProfileAttribute(data, config.oauth2.userProfileUsernameAttr)
const displayName = extractProfileAttribute(data, config.oauth2.userProfileDisplayNameAttr)
const email = extractProfileAttribute(data, config.oauth2.userProfileEmailAttr)
if (id === undefined && username === undefined) {
logger.error('oauth2 auth failed: id and username are undefined')
throw new Error('User ID or Username required')
}
return {
id: id || username,
username,
displayName,
emails: email ? [email] : []
}
}
function checkAuthorization (data, done) {
// a role the user must have is set in the config
if (config.oauth2.accessRole) {
// 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
let roles = []
try {
roles = extractProfileAttribute(data, config.oauth2.rolesClaim)
} catch (err) {
logger.warn(`oauth2: failed to extract rolesClaim '${config.oauth2.rolesClaim}' from user profile.`)
return done('Permission denied', null)
}
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)
}
}
}
}
OAuth2CustomStrategy.prototype.userProfile = function (accessToken, done) {
this._oauth2.get(this._userProfileURL, accessToken, function (err, body, res) {
let json, profile
if (err) {
return done(new InternalOAuthError('Failed to fetch user profile', err))
}
try {
json = JSON.parse(body)
} catch (ex) {
return done(new Error('Failed to parse user profile'))
}
checkAuthorization(json, done)
try {
profile = parseProfile(json)
} catch (ex) {
return done('Failed to identify user profile information', null)
}
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',
userProfileURL: config.oauth2.userProfileURL,
scope: config.oauth2.scope,
pkce: config.oauth2.pkce,
state: true
}, 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', {
successReturnToOrRedirect: config.serverURL + '/',
failureRedirect: config.serverURL + '/'
})
)