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 private issuer: Issuer | undefined | null 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 } /** * Get the button * @returns Button label */ getButtonLabel (): string | undefined { return this.buttonLabel } /** * Get the discovery URL * @returns discoveryURL */ getDiscoveryUrl (): string | undefined { return this.discoveryUrl } /** * Indicates if the OIDC provider is correctly configured. * @param force If true, all checks will be forced again. */ async isOk (force?: boolean): Promise { // 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 { if (!this.enabled) { this.logger.debug('OIDC is disabled') return ['OIDC disabled'] } const errors: string[] = [] if ((this.buttonLabel ?? '') === '') { errors.push('Missing button label') } if ((this.discoveryUrl ?? '') === '') { errors.push('Missing discovery url') } else { try { const uri = new URL(this.discoveryUrl ?? 'wrong url') this.logger.debug('OIDC Discovery url is valid: ' + uri.toString()) } catch (err) { errors.push('Invalid discovery url') } } if ((this.clientId ?? '') === '') { errors.push('Missing client id') } if ((this.clientSecret ?? '') === '') { errors.push('Missing client secret') } if (errors.length) { this.logger.error('OIDC is not ok: ' + JSON.stringify(errors)) } return errors } /** * Ensure the issuer is loaded. * @returns the issuer if enabled */ async loadIssuer (): Promise { // this.issuer === null means we already tried, but it failed. if (this.issuer !== undefined) { return this.issuer } if (!await this.isOk()) { return null } try { this.issuer = await Issuer.discover(this.discoveryUrl as string) this.logger.debug(`Discovered issuer, metadata are: ${JSON.stringify(this.issuer.metadata)}`) } catch (err) { this.logger.error(err as string) this.issuer = null } return this.issuer } /** * frees the singleton */ public static async destroySingleton (): Promise { 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 { 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 }