123 lines
3.5 KiB
JavaScript
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)
|