xen-orchestra/packages/xo-server-auth-oidc/index.js

123 lines
3.5 KiB
JavaScript

'use strict'
const { join } = require('node:path/posix')
const { Strategy } = require('passport-openidconnect')
// ===================================================================
const DISCOVERABLE_SETTINGS = ['authorizationURL', 'issuer', 'userInfoURL', 'tokenURL']
exports.configurationSchema = {
type: 'object',
properties: {
discoveryURL: {
description: 'If this field is not used, you will need to manually enter settings in the *Advanced* section.',
title: 'Auto-discovery URL',
type: 'string',
},
clientID: { title: 'Client identifier (key)', type: 'string' },
clientSecret: { title: 'Client secret', type: 'string' },
advanced: {
title: 'Advanced',
type: 'object',
default: {},
properties: {
authorizationURL: { title: 'Authorization URL', type: 'string' },
callbackURL: {
title: 'Callback URL',
default: '/signin/oidc/callback',
type: 'string',
},
issuer: { title: 'Issuer', type: 'string' },
tokenURL: { title: 'Token URL', type: 'string' },
userInfoURL: { title: 'User info URL', type: 'string' },
usernameField: {
default: 'username',
description: 'Field to use as the XO username (e.g. `displayName`, `username` or `email`)',
title: 'Username field',
type: 'string',
},
},
},
},
required: ['clientID', 'clientSecret'],
anyOf: [{ required: ['discoveryURL'] }, { properties: { advanced: { required: DISCOVERABLE_SETTINGS } } }],
}
// ===================================================================
const WELL_KNOWN_ENDPOINT = '/.well-known/openid-configuration'
class AuthOidc {
#conf
#unregisterPassportStrategy
#xo
constructor(xo) {
this.#xo = xo
}
async configure({ advanced, ...conf }, { loaded }) {
this.#conf = { ...advanced, ...conf }
if (loaded) {
await this.unload()
await this.load()
}
}
async load() {
const xo = this.#xo
const { discoveryURL, usernameField, ...conf } = this.#conf
if (discoveryURL !== undefined) {
let url = discoveryURL
let onError
// try with the well-known path first
if (!url.endsWith(WELL_KNOWN_ENDPOINT)) {
url = join(url, WELL_KNOWN_ENDPOINT)
// on error, retry with the original URL
onError = () => this.#xo.httpRequest(discoveryURL)
}
const res = await this.#xo.httpRequest(url).catch(onError)
const data = await res.json()
for (const key of DISCOVERABLE_SETTINGS) {
if (!conf[key]) {
conf[key] = data[key.endsWith('URL') ? key.slice(0, -3).toLowerCase() + '_endpoint' : key]
}
}
}
this.#unregisterPassportStrategy = xo.registerPassportStrategy(
new Strategy(conf, async (issuer, profile, done) => {
try {
// See https://github.com/jaredhanson/passport-openidconnect/blob/master/lib/profile.js
const { id } = profile
done(
null,
await xo.registerUser2('oidc:' + issuer, {
user: { id, name: usernameField === 'email' ? profile.emails[0].value : profile[usernameField] },
})
)
} catch (error) {
done(error.message)
}
}),
{ label: 'OpenID Connect', name: 'oidc' }
)
}
unload() {
this.#unregisterPassportStrategy()
}
}
// ===================================================================
exports.default = ({ xo }) => new AuthOidc(xo)