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

This commit is contained in:
John Livingston 2024-04-16 17:18:14 +02:00
parent e646ebfd69
commit 669b260307
No known key found for this signature in database
GPG Key ID: B17B5640CE66CDBC
11 changed files with 158 additions and 24 deletions

View File

@ -13,7 +13,7 @@ TODO: https://github.com/JohnXLivingston/peertube-plugin-livechat/issues/48
* For anonymous users: new "log in using an external account" dialog, with following options: * For anonymous users: new "log in using an external account" dialog, with following options:
* remote Peertube account, * remote Peertube account,
* #128: possibility to configure an OpenID Connect provider on the instance level. * #128 (**Experimental Feature**): possibility to configure an OpenID Connect provider on the instance level.
* #143: User colors: implementing [XEP-0392](https://xmpp.org/extensions/xep-0392.html) to have random colors on users nicknames * #143: User colors: implementing [XEP-0392](https://xmpp.org/extensions/xep-0392.html) to have random colors on users nicknames
* #330: Chat does no more use an iframe to display the chat besides the videos. * #330: Chat does no more use an iframe to display the chat besides the videos.
* #330: Fullscreen chat: now uses a custom page (in other words: when opening the chat in a new tab, you will have the Peertube menu). * #330: Fullscreen chat: now uses a custom page (in other words: when opening the chat in a new tab, you will have the Peertube menu).

View File

@ -160,6 +160,7 @@ async function initConverse (
// no viewer mode if authenticated. // no viewer mode if authenticated.
params.livechat_enable_viewer_mode = autoViewerMode && !isAuthenticated && !isRemoteWithNicknameSet params.livechat_enable_viewer_mode = autoViewerMode && !isAuthenticated && !isRemoteWithNicknameSet
params.livechat_external_auth_oidc_button_label = initConverseParams.externalAuthOIDC?.buttonLabel params.livechat_external_auth_oidc_button_label = initConverseParams.externalAuthOIDC?.buttonLabel
params.livechat_external_auth_oidc_url = initConverseParams.externalAuthOIDC?.url
if (chatIncludeMode === 'peertube-video') { if (chatIncludeMode === 'peertube-video') {
params.livechat_mini_muc_head = true // we must replace the muc-head by the custom buttons toolbar. params.livechat_mini_muc_head = true // we must replace the muc-head by the custom buttons toolbar.

View File

@ -9,14 +9,15 @@ export const tplExternalLoginModal = (el, o) => {
const i18nRemotePeertubeUrl = __(LOC_login_remote_peertube_url) const i18nRemotePeertubeUrl = __(LOC_login_remote_peertube_url)
const i18nRemotePeertubeOpen = __('OK') const i18nRemotePeertubeOpen = __('OK')
const externalAuthOIDCButtonLabel = api.settings.get('livechat_external_auth_oidc_button_label') const externalAuthOIDCButtonLabel = api.settings.get('livechat_external_auth_oidc_button_label')
const externalAuthOIDCUrl = api.settings.get('livechat_external_auth_oidc_url')
return html`<div class="modal-body livechat-external-login-modal"> return html`<div class="modal-body livechat-external-login-modal">
${!externalAuthOIDCButtonLabel ${!externalAuthOIDCButtonLabel || !externalAuthOIDCUrl
? '' ? ''
: html` : html`
<div class="livechat-external-login-modal-external-auth-oidc"> <div class="livechat-external-login-modal-external-auth-oidc">
<button <button
class="btn btn-primary" class="btn btn-primary"
@click=${() => console.log('ok, go')} @click=${() => window.open(externalAuthOIDCUrl)}
> >
${externalAuthOIDCButtonLabel} ${externalAuthOIDCButtonLabel}
</button> </button>

View File

@ -37,7 +37,7 @@ export const livechatSpecificsPlugin = {
for (const k of [ for (const k of [
'hide_muc_participants', 'hide_muc_participants',
'livechat_enable_viewer_mode', 'livechat_enable_viewer_mode',
'livechat_external_auth_oidc_button_label', 'livechat_external_auth_oidc_button_label', 'livechat_external_auth_oidc_url',
'livechat_mini_muc_head' 'livechat_mini_muc_head'
]) { ]) {
_converse.api.settings.set(k, params[k]) _converse.api.settings.set(k, params[k])

View File

@ -9,7 +9,8 @@ export const livechatViewerModePlugin = {
livechat_enable_viewer_mode: false, livechat_enable_viewer_mode: false,
livechat_peertube_video_original_url: undefined, livechat_peertube_video_original_url: undefined,
livechat_peertube_video_uuid: undefined, livechat_peertube_video_uuid: undefined,
livechat_external_auth_oidc_button_label: undefined livechat_external_auth_oidc_button_label: undefined,
livechat_external_auth_oidc_url: undefined
}) })
const originalGetDefaultMUCNickname = _converse.getDefaultMUCNickname const originalGetDefaultMUCNickname = _converse.getDefaultMUCNickname

View File

@ -83,6 +83,9 @@ external_auth_custom_oidc_button_label_description: "This label will be displaye
external_auth_custom_oidc_discovery_url_label: "Discovery URL" external_auth_custom_oidc_discovery_url_label: "Discovery URL"
external_auth_custom_oidc_client_id_label: "Client ID" external_auth_custom_oidc_client_id_label: "Client ID"
external_auth_custom_oidc_client_secret_label: "Client secret" external_auth_custom_oidc_client_secret_label: "Client secret"
external_auth_custom_oidc_redirect_uris_info_description: |
<strong>Callback/Redirect URI:</strong>
If you want to configure authorized redirection URI on the external Application, please add this url:
chat_behaviour_description: "<h3>Chat behaviour</h3>" chat_behaviour_description: "<h3>Chat behaviour</h3>"

View File

@ -77,13 +77,24 @@ async function getConverseJSParams (
roomJID roomJID
} = connectionInfos } = connectionInfos
let externalAuthOIDC
if (userIsConnected !== true) {
try {
const oidc = ExternalAuthOIDC.singleton() const oidc = ExternalAuthOIDC.singleton()
// TODO: if (await oidc.isOk()) {
const externalAuthOIDC = await oidc.isOk() const authUrl = oidc.getAuthUrl()
? { const buttonLabel = oidc.getButtonLabel()
buttonLabel: oidc.getButtonLabel() ?? '???' if (authUrl && buttonLabel) {
externalAuthOIDC = {
buttonLabel: buttonLabel,
url: authUrl
}
}
}
} catch (err) {
options.peertubeHelpers.logger.error(err)
}
} }
: undefined
return { return {
peertubeVideoOriginalUrl: roomInfos.video?.url, peertubeVideoOriginalUrl: roomInfos.video?.url,

View File

@ -41,9 +41,9 @@ export async function diagExternalAuthCustomOIDC (test: string, _options: Regist
} }
const oidc = ExternalAuthOIDC.singleton() const oidc = ExternalAuthOIDC.singleton()
const issuer = await oidc.loadIssuer() const oidcClient = await oidc.load()
if (issuer) { if (oidcClient) {
result.messages.push('Discovery URL loaded: ' + JSON.stringify(issuer.metadata)) result.messages.push('Discovery URL loaded: ' + JSON.stringify(oidcClient.issuer.metadata))
} else { } else {
result.messages.push({ result.messages.push({
level: 'error', level: 'error',

View File

@ -1,6 +1,8 @@
import type { RegisterServerOptions } from '@peertube/peertube-types' import type { RegisterServerOptions } from '@peertube/peertube-types'
import { URL } from 'url' import { URL } from 'url'
import { Issuer } from 'openid-client' import { Issuer, BaseClient } from 'openid-client'
import { getBaseRouterRoute } from '../helpers'
import { canonicalizePluginUri } from '../uri/canonicalize'
let singleton: ExternalAuthOIDC | undefined let singleton: ExternalAuthOIDC | undefined
@ -13,8 +15,14 @@ class ExternalAuthOIDC {
private readonly discoveryUrl: string | undefined private readonly discoveryUrl: string | undefined
private readonly clientId: string | undefined private readonly clientId: string | undefined
private readonly clientSecret: string | undefined private readonly clientSecret: string | undefined
private readonly redirectUri: string
private ok: boolean | undefined private ok: boolean | undefined
private issuer: Issuer | undefined | null private issuer: Issuer | undefined | null
private client: BaseClient | undefined | null
private authorizationUrl: string | null
protected readonly logger: { protected readonly logger: {
debug: (s: string) => void debug: (s: string) => void
info: (s: string) => void info: (s: string) => void
@ -28,7 +36,8 @@ class ExternalAuthOIDC {
buttonLabel: string | undefined, buttonLabel: string | undefined,
discoveryUrl: string | undefined, discoveryUrl: string | undefined,
clientId: string | undefined, clientId: string | undefined,
clientSecret: string | undefined clientSecret: string | undefined,
redirectUri: string
) { ) {
this.logger = { this.logger = {
debug: (s) => logger.debug('[ExternalAuthOIDC] ' + s), debug: (s) => logger.debug('[ExternalAuthOIDC] ' + s),
@ -38,6 +47,8 @@ class ExternalAuthOIDC {
} }
this.enabled = !!enabled this.enabled = !!enabled
this.redirectUri = redirectUri
this.authorizationUrl = null
if (this.enabled) { if (this.enabled) {
this.buttonLabel = buttonLabel this.buttonLabel = buttonLabel
this.discoveryUrl = discoveryUrl this.discoveryUrl = discoveryUrl
@ -55,6 +66,16 @@ class ExternalAuthOIDC {
return !this.enabled return !this.enabled
} }
/**
* Get the url to open for external authentication.
* Note: If the singleton is not loaded yet, returns null.
* This means that the feature will only be available when the load as complete.
* @returns the url to open
*/
getAuthUrl (): string | null {
return this.authorizationUrl ?? null
}
/** /**
* Get the button * Get the button
* @returns Button label * @returns Button label
@ -122,14 +143,21 @@ class ExternalAuthOIDC {
} }
/** /**
* Ensure the issuer is loaded. * Ensure the issuer is loaded, and the client instanciated.
* @returns the issuer if enabled * @returns the issuer if enabled
*/ */
async loadIssuer (): Promise<Issuer | null> { async load (): Promise<BaseClient | null> {
// this.issuer === null means we already tried, but it failed. // this.client === null means we already tried, but it failed.
if (this.issuer !== undefined) { return this.issuer } if (this.client !== undefined) { return this.client }
if (!await this.isOk()) { return null } // First, reset the authentication url:
this.authorizationUrl = null
if (!await this.isOk()) {
this.issuer = null
this.client = null
return null
}
try { try {
this.issuer = await Issuer.discover(this.discoveryUrl as string) this.issuer = await Issuer.discover(this.discoveryUrl as string)
@ -137,8 +165,38 @@ class ExternalAuthOIDC {
} catch (err) { } catch (err) {
this.logger.error(err as string) this.logger.error(err as string)
this.issuer = null this.issuer = null
this.client = null
} }
return this.issuer
if (!this.issuer) {
this.client = null
return null
}
try {
this.client = new this.issuer.Client({
client_id: this.clientId as string,
client_secret: this.clientSecret as string,
redirect_uris: [this.redirectUri],
response_types: ['code']
})
} catch (err) {
this.logger.error(err as string)
this.client = null
}
if (!this.client) {
return null
}
try {
this.authorizationUrl = this.client.authorizationUrl()
} catch (err) {
this.logger.error(err as string)
this.authorizationUrl = null
}
return this.client
} }
/** /**
@ -167,7 +225,8 @@ class ExternalAuthOIDC {
settings['external-auth-custom-oidc-button-label'] as string | undefined, 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-discovery-url'] as string | undefined,
settings['external-auth-custom-oidc-client-id'] as string | undefined, settings['external-auth-custom-oidc-client-id'] as string | undefined,
settings['external-auth-custom-oidc-client-secret'] as string | undefined settings['external-auth-custom-oidc-client-secret'] as string | undefined,
ExternalAuthOIDC.redirectUri(options)
) )
return singleton return singleton
@ -183,6 +242,13 @@ class ExternalAuthOIDC {
} }
return singleton return singleton
} }
public static redirectUri (options: RegisterServerOptions): string {
const path = getBaseRouterRoute(options) + 'oidc/cb'
return canonicalizePluginUri(options, path, {
removePluginVersion: true
})
}
} }
export { export {

View File

@ -5,11 +5,13 @@ import { RoomChannel } from './room-channel'
import { BotsCtl } from './bots/ctl' import { BotsCtl } from './bots/ctl'
import { ExternalAuthOIDC } from './external-auth/oidc' import { ExternalAuthOIDC } from './external-auth/oidc'
import { loc } from './loc' import { loc } from './loc'
const escapeHTML = require('escape-html')
type AvatarSet = 'sepia' | 'cat' | 'bird' | 'fenec' | 'abstract' | 'legacy' type AvatarSet = 'sepia' | 'cat' | 'bird' | 'fenec' | 'abstract' | 'legacy'
async function initSettings (options: RegisterServerOptions): Promise<void> { async function initSettings (options: RegisterServerOptions): Promise<void> {
const { peertubeHelpers, settingsManager } = options const { peertubeHelpers, settingsManager } = options
const logger = peertubeHelpers.logger
initImportantNotesSettings(options) initImportantNotesSettings(options)
initChatSettings(options) initChatSettings(options)
@ -21,6 +23,30 @@ async function initSettings (options: RegisterServerOptions): Promise<void> {
initChatServerAdvancedSettings(options) initChatServerAdvancedSettings(options)
await ExternalAuthOIDC.initSingleton(options) await ExternalAuthOIDC.initSingleton(options)
const loadOidc = (): void => {
try {
const oidc = ExternalAuthOIDC.singleton()
oidc.isOk().then(
() => {
logger.info('Loading External Auth OIDC...')
oidc.load().then(
() => {
logger.info('External Auth OIDC loaded')
},
() => {
logger.error('Loading the External Auth OIDC failed')
}
)
},
() => {
logger.info('No valid External Auth OIDC, nothing loaded')
}
)
} catch (err) {
logger.error(err as string)
}
}
loadOidc() // we don't have to wait (can take time, it will do external http requests)
let currentProsodyRoomtype = (await settingsManager.getSettings(['prosody-room-type']))['prosody-room-type'] let currentProsodyRoomtype = (await settingsManager.getSettings(['prosody-room-type']))['prosody-room-type']
@ -30,6 +56,7 @@ async function initSettings (options: RegisterServerOptions): Promise<void> {
// To avoid race condition, we will just stop and start the bots at every settings saving. // To avoid race condition, we will just stop and start the bots at every settings saving.
await BotsCtl.destroySingleton() await BotsCtl.destroySingleton()
await BotsCtl.initSingleton(options) await BotsCtl.initSingleton(options)
loadOidc() // we don't have to wait (can take time, it will do external http requests)
await ExternalAuthOIDC.initSingleton(options) await ExternalAuthOIDC.initSingleton(options)
@ -145,12 +172,21 @@ function initFederationSettings ({ registerSetting }: RegisterServerOptions): vo
* Registers settings related to the "External Authentication" section. * Registers settings related to the "External Authentication" section.
* @param param0 server options * @param param0 server options
*/ */
function initExternalAuth ({ registerSetting }: RegisterServerOptions): void { function initExternalAuth (options: RegisterServerOptions): void {
const registerSetting = options.registerSetting
registerSetting({ registerSetting({
type: 'html', type: 'html',
private: true, private: true,
descriptionHTML: loc('external_auth_description') descriptionHTML: loc('external_auth_description')
}) })
registerSetting({
type: 'html',
private: true,
descriptionHTML: loc('experimental_warning')
})
registerSetting({ registerSetting({
name: 'external-auth-custom-oidc', name: 'external-auth-custom-oidc',
label: loc('external_auth_custom_oidc_label'), label: loc('external_auth_custom_oidc_label'),
@ -159,6 +195,20 @@ function initExternalAuth ({ registerSetting }: RegisterServerOptions): void {
default: false, default: false,
private: true private: true
}) })
registerSetting({
type: 'html',
name: 'external-auth-custom-oidc-redirect-uris-info',
private: true,
descriptionHTML: loc('external_auth_custom_oidc_redirect_uris_info_description')
})
registerSetting({
type: 'html',
name: 'external-auth-custom-oidc-redirect-uris',
private: true,
descriptionHTML: `<ul><li>${escapeHTML(ExternalAuthOIDC.redirectUri(options)) as string}</li></ul>`
})
registerSetting({ registerSetting({
name: 'external-auth-custom-oidc-button-label', name: 'external-auth-custom-oidc-button-label',
label: loc('external_auth_custom_oidc_button_label_label'), label: loc('external_auth_custom_oidc_button_label_label'),

View File

@ -24,6 +24,7 @@ interface InitConverseJSParams {
autofocus?: boolean autofocus?: boolean
externalAuthOIDC?: { externalAuthOIDC?: {
buttonLabel: string buttonLabel: string
url: string
} }
} }