Possibility to configure an OpenID Connect provider on the instance level WIP (#128).

This commit is contained in:
John Livingston
2024-04-15 18:29:09 +02:00
parent c1e877cb44
commit 514cc1d159
14 changed files with 388 additions and 12 deletions

View File

@ -10,6 +10,7 @@ import { getVideoLiveChatInfos } from '../federation/storage'
import { getBaseRouterRoute, getBaseStaticRoute } from '../helpers'
import { getProsodyDomain } from '../prosody/config/domain'
import { getBoshUri, getWSUri } from '../uri/webchat'
import { ExternalAuthOIDC } from '../external-auth/oidc'
interface GetConverseJSParamsParams {
readonly?: boolean | 'noscroll'
@ -76,6 +77,10 @@ async function getConverseJSParams (
roomJID
} = connectionInfos
const oidc = ExternalAuthOIDC.singleton()
// TODO:
const externalAuthOIDC = await oidc.isOk() ? undefined : undefined
return {
peertubeVideoOriginalUrl: roomInfos.video?.url,
peertubeVideoUUID: roomInfos.video?.uuid,
@ -98,7 +103,8 @@ async function getConverseJSParams (
transparent,
// forceDefaultHideMucParticipants is for testing purpose
// (so we can stress test with the muc participant list hidden by default)
forceDefaultHideMucParticipants: params.forceDefaultHideMucParticipants
forceDefaultHideMucParticipants: params.forceDefaultHideMucParticipants,
externalAuthOIDC
}
}

View File

@ -0,0 +1,39 @@
import type { RegisterServerOptions } from '@peertube/peertube-types'
import { newResult, TestResult } from './utils'
import { ExternalAuthOIDC } from '../external-auth/oidc'
export async function diagExternalAuthCustomOIDC (test: string, _options: RegisterServerOptions): Promise<TestResult> {
const result = newResult(test)
result.label = 'Test External Auth Custom OIDC'
result.next = 'everything-ok'
try {
const oidc = ExternalAuthOIDC.singleton()
if (oidc.isDisabledBySettings()) {
result.ok = true
result.messages.push('Feature disabled in plugins settings.')
return result
}
const errors = await oidc.check()
if (errors.length) {
result.messages.push({
level: 'error',
message: 'The ExternalAuthOIDC singleton got some errors:'
})
result.messages.push(...errors)
return result
}
} catch (err) {
result.messages.push({
level: 'error',
message: 'Error while retrieving the ExternalAuthOIDC singleton:' + (err as string)
})
return result
}
result.ok = true
result.messages.push('Configuration OK.')
return result
}

View File

@ -4,6 +4,7 @@ import { TestResult, newResult } from './utils'
import { diagDebug } from './debug'
import { diagProsody } from './prosody'
import { diagVideo } from './video'
import { diagExternalAuthCustomOIDC } from './external-auth-custom-oidc'
import { helpUrl } from '../../../shared/lib/help'
export async function diag (test: string, options: RegisterServerOptions): Promise<TestResult> {
@ -17,6 +18,8 @@ export async function diag (test: string, options: RegisterServerOptions): Promi
result = await diagVideo(test, options)
} else if (test === 'prosody') {
result = await diagProsody(test, options)
} else if (test === 'external-auth-custom-oidc') {
result = await diagExternalAuthCustomOIDC(test, options)
} else if (test === 'everything-ok') {
result = newResult(test)
result.label = 'Everything seems fine'

View File

@ -236,6 +236,6 @@ export async function diagProsody (test: string, options: RegisterServerOptions)
}
result.ok = true
result.next = 'everything-ok'
result.next = 'external-auth-custom-oidc'
return result
}

View File

@ -1,4 +1,4 @@
type nextValue = 'backend' | 'debug' | 'webchat-video' | 'prosody' | 'everything-ok'
type nextValue = 'backend' | 'debug' | 'webchat-video' | 'prosody' | 'external-auth-custom-oidc' | 'everything-ok'
interface MessageWithLevel {
level: 'info' | 'warning' | 'error'

View File

@ -0,0 +1,163 @@
import type { RegisterServerOptions } from '@peertube/peertube-types'
import { URL } from 'url'
import { Issuer } from 'openid-client'
let singleton: ExternalAuthOIDC | undefined
/**
* This class handles the external OpenId Connect provider, if defined.
*/
class ExternalAuthOIDC {
private readonly enabled: boolean
private readonly buttonLabel: string | undefined
private readonly discoveryUrl: string | undefined
private readonly clientId: string | undefined
private readonly clientSecret: string | undefined
private ok: boolean | undefined
protected readonly logger: {
debug: (s: string) => void
info: (s: string) => void
warn: (s: string) => void
error: (s: string) => void
}
constructor (
logger: RegisterServerOptions['peertubeHelpers']['logger'],
enabled: boolean,
buttonLabel: string | undefined,
discoveryUrl: string | undefined,
clientId: string | undefined,
clientSecret: string | undefined
) {
this.logger = {
debug: (s) => logger.debug('[ExternalAuthOIDC] ' + s),
info: (s) => logger.info('[ExternalAuthOIDC] ' + s),
warn: (s) => logger.warn('[ExternalAuthOIDC] ' + s),
error: (s) => logger.error('[ExternalAuthOIDC] ' + s)
}
this.enabled = !!enabled
if (this.enabled) {
this.buttonLabel = buttonLabel
this.discoveryUrl = discoveryUrl
this.clientId = clientId
this.clientSecret = clientSecret
}
}
/**
* Indicates that the OIDC is disabled.
* Caution: this does not indicate if it is enabled, but poorly configured.
* This method should only be used in the diagnostic tool.
*/
isDisabledBySettings (): boolean {
return !this.enabled
}
/**
* Indicates if the OIDC provider is correctly configured.
* @param force If true, all checks will be forced again.
*/
async isOk (force?: boolean): Promise<boolean> {
// If we already checked it, just return the previous value.
if (!force && this.ok !== undefined) { return this.ok }
this.ok = (await this.check()).length === 0
return this.ok
}
/**
* Check the configuration.
* Returns an error list.
* If error list is empty, consider the OIDC is correctly configured.
*/
async check (): Promise<string[]> {
if (!this.enabled) {
this.logger.debug('OIDC is disabled')
return ['OIDC disabled']
}
const errors: string[] = []
if (this.buttonLabel === undefined) {
errors.push('Missing button label')
}
if (this.discoveryUrl === undefined) {
errors.push('Missing discovery url')
} else {
try {
const uri = new URL(this.discoveryUrl)
this.logger.debug('OIDC Discovery url is valid: ' + uri.toString())
} catch (err) {
errors.push('Invalid discovery url')
}
}
if (this.clientId === undefined) {
errors.push('Missing client id')
}
if (this.clientSecret === undefined) {
errors.push('Missing client secret')
}
if (errors.length === 0) {
// Now we can try to use the discover service
try {
const issuer = await Issuer.discover(this.discoveryUrl as string)
this.logger.debug(`Discovered issuer, metadata are: ${JSON.stringify(issuer.metadata)}`)
} catch (err) {
this.logger.error(err as string)
errors.push(`Discovery URL non working: ${err as string}`)
}
}
if (errors.length) {
this.logger.error('OIDC is not ok: ' + JSON.stringify(errors))
}
return errors
}
/**
* frees the singleton
*/
public static async destroySingleton (): Promise<void> {
if (!singleton) { return }
singleton = undefined
}
/**
* Instanciate the singleton.
* Note: no need to destroy the singleton before creating a new one.
*/
public static async initSingleton (options: RegisterServerOptions): Promise<ExternalAuthOIDC> {
const settings = await options.settingsManager.getSettings([
'external-auth-custom-oidc',
'external-auth-custom-oidc-button-label',
'external-auth-custom-oidc-discovery-url',
'external-auth-custom-oidc-client-id',
'external-auth-custom-oidc-client-secret'
])
singleton = new ExternalAuthOIDC(
options.peertubeHelpers.logger,
settings['external-auth-custom-oidc'] as boolean,
settings['external-auth-custom-oidc-button-label'] as string | undefined,
settings['external-auth-custom-oidc-discovery-url'] as string | undefined,
settings['external-auth-custom-oidc-client-id'] as string | undefined,
settings['external-auth-custom-oidc-client-secret'] as string | undefined
)
return singleton
}
/**
* Gets the singleton, or raise an exception if it is too soon.
* @returns the singleton
*/
public static singleton (): ExternalAuthOIDC {
if (!singleton) {
throw new Error('ExternalAuthOIDC singleton is not initialized yet')
}
return singleton
}
}
export {
ExternalAuthOIDC
}

View File

@ -3,6 +3,7 @@ import type { ConverseJSTheme } from '../../shared/lib/types'
import { ensureProsodyRunning } from './prosody/ctl'
import { RoomChannel } from './room-channel'
import { BotsCtl } from './bots/ctl'
import { ExternalAuthOIDC } from './external-auth/oidc'
import { loc } from './loc'
type AvatarSet = 'sepia' | 'cat' | 'bird' | 'fenec' | 'abstract' | 'legacy'
@ -13,11 +14,14 @@ async function initSettings (options: RegisterServerOptions): Promise<void> {
initImportantNotesSettings(options)
initChatSettings(options)
initFederationSettings(options)
initExternalAuth(options)
initAdvancedChannelCustomizationSettings(options)
initChatBehaviourSettings(options)
initThemingSettings(options)
initChatServerAdvancedSettings(options)
await ExternalAuthOIDC.initSingleton(options)
let currentProsodyRoomtype = (await settingsManager.getSettings(['prosody-room-type']))['prosody-room-type']
// ********** settings changes management
@ -27,6 +31,8 @@ async function initSettings (options: RegisterServerOptions): Promise<void> {
await BotsCtl.destroySingleton()
await BotsCtl.initSingleton(options)
await ExternalAuthOIDC.initSingleton(options)
peertubeHelpers.logger.info('Saving settings, ensuring prosody is running')
await ensureProsodyRunning(options)
@ -135,6 +141,77 @@ function initFederationSettings ({ registerSetting }: RegisterServerOptions): vo
})
}
/**
* Registers settings related to the "External Authentication" section.
* @param param0 server options
*/
function initExternalAuth ({ registerSetting }: RegisterServerOptions): void {
registerSetting({
type: 'html',
private: true,
descriptionHTML: loc('external_auth_description')
})
registerSetting({
name: 'external-auth-custom-oidc',
label: loc('external_auth_custom_oidc_label'),
descriptionHTML: loc('external_auth_custom_oidc_description'),
type: 'input-checkbox',
default: false,
private: true
})
registerSetting({
name: 'external-auth-custom-oidc-button-label',
label: loc('external_auth_custom_oidc_button_label_label'),
descriptionHTML: loc('external_auth_custom_oidc_button_label_description'),
type: 'input',
default: '',
private: true
})
registerSetting({
name: 'external-auth-custom-oidc-discovery-url',
label: loc('external_auth_custom_oidc_discovery_url_label'),
// descriptionHTML: loc('external_auth_custom_oidc_discovery_url_description'),
type: 'input',
private: true
})
registerSetting({
name: 'external-auth-custom-oidc-client-id',
label: loc('external_auth_custom_oidc_client_id_label'),
// descriptionHTML: loc('external_auth_custom_oidc_client_id_description'),
type: 'input',
private: true
})
registerSetting({
name: 'external-auth-custom-oidc-client-secret',
label: loc('external_auth_custom_oidc_client_secret_label'),
// descriptionHTML: loc('external_auth_custom_oidc_client_secret_description'),
type: 'input-password',
private: true
})
// registerSetting({
// name: 'external-auth-custom-oidc-scope',
// label: loc('external_auth_custom_oidc_scope_label'),
// descriptionHTML: loc('external_auth_custom_oidc_scope_description'),
// type: 'input',
// private: true,
// default: 'openid profile'
// })
// registerSetting({
// name: 'username-property',
// label: 'Username property',
// type: 'input',
// private: true,
// default: 'preferred_username'
// })
// registerSetting({
// name: 'display-name-property',
// label: 'Display name property',
// type: 'input',
// private: true
// })
}
/**
* Registers settings related to the "Advanced channel customization" section.
* @param param0 server options