- ${!externalAuthOIDCButtonLabel
+ ${!externalAuthOIDCButtonLabel || !externalAuthOIDCUrl
? ''
: html`
console.log('ok, go')}
+ @click=${() => window.open(externalAuthOIDCUrl)}
>
${externalAuthOIDCButtonLabel}
diff --git a/conversejs/lib/plugins/livechat-specific.ts b/conversejs/lib/plugins/livechat-specific.ts
index 8844b38e..5ff95bbb 100644
--- a/conversejs/lib/plugins/livechat-specific.ts
+++ b/conversejs/lib/plugins/livechat-specific.ts
@@ -37,7 +37,7 @@ export const livechatSpecificsPlugin = {
for (const k of [
'hide_muc_participants',
'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'
]) {
_converse.api.settings.set(k, params[k])
diff --git a/conversejs/lib/plugins/livechat-viewer-mode.ts b/conversejs/lib/plugins/livechat-viewer-mode.ts
index 1157a2b4..5d89977b 100644
--- a/conversejs/lib/plugins/livechat-viewer-mode.ts
+++ b/conversejs/lib/plugins/livechat-viewer-mode.ts
@@ -9,7 +9,8 @@ export const livechatViewerModePlugin = {
livechat_enable_viewer_mode: false,
livechat_peertube_video_original_url: 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
diff --git a/languages/en.yml b/languages/en.yml
index 59989015..a13ef52a 100644
--- a/languages/en.yml
+++ b/languages/en.yml
@@ -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_client_id_label: "Client ID"
external_auth_custom_oidc_client_secret_label: "Client secret"
+external_auth_custom_oidc_redirect_uris_info_description: |
+
Callback/Redirect URI:
+ If you want to configure authorized redirection URI on the external Application, please add this url:
chat_behaviour_description: "
Chat behaviour "
diff --git a/server/lib/conversejs/params.ts b/server/lib/conversejs/params.ts
index 40702df3..71242bc6 100644
--- a/server/lib/conversejs/params.ts
+++ b/server/lib/conversejs/params.ts
@@ -77,13 +77,24 @@ async function getConverseJSParams (
roomJID
} = connectionInfos
- const oidc = ExternalAuthOIDC.singleton()
- // TODO:
- const externalAuthOIDC = await oidc.isOk()
- ? {
- buttonLabel: oidc.getButtonLabel() ?? '???'
+ let externalAuthOIDC
+ if (userIsConnected !== true) {
+ try {
+ const oidc = ExternalAuthOIDC.singleton()
+ if (await oidc.isOk()) {
+ const authUrl = oidc.getAuthUrl()
+ const buttonLabel = oidc.getButtonLabel()
+ if (authUrl && buttonLabel) {
+ externalAuthOIDC = {
+ buttonLabel: buttonLabel,
+ url: authUrl
+ }
+ }
}
- : undefined
+ } catch (err) {
+ options.peertubeHelpers.logger.error(err)
+ }
+ }
return {
peertubeVideoOriginalUrl: roomInfos.video?.url,
diff --git a/server/lib/diagnostic/external-auth-custom-oidc.ts b/server/lib/diagnostic/external-auth-custom-oidc.ts
index 495120ed..e901c50a 100644
--- a/server/lib/diagnostic/external-auth-custom-oidc.ts
+++ b/server/lib/diagnostic/external-auth-custom-oidc.ts
@@ -41,9 +41,9 @@ export async function diagExternalAuthCustomOIDC (test: string, _options: Regist
}
const oidc = ExternalAuthOIDC.singleton()
- const issuer = await oidc.loadIssuer()
- if (issuer) {
- result.messages.push('Discovery URL loaded: ' + JSON.stringify(issuer.metadata))
+ const oidcClient = await oidc.load()
+ if (oidcClient) {
+ result.messages.push('Discovery URL loaded: ' + JSON.stringify(oidcClient.issuer.metadata))
} else {
result.messages.push({
level: 'error',
diff --git a/server/lib/external-auth/oidc.ts b/server/lib/external-auth/oidc.ts
index e394ff79..52b8692b 100644
--- a/server/lib/external-auth/oidc.ts
+++ b/server/lib/external-auth/oidc.ts
@@ -1,6 +1,8 @@
import type { RegisterServerOptions } from '@peertube/peertube-types'
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
@@ -13,8 +15,14 @@ class ExternalAuthOIDC {
private readonly discoveryUrl: string | undefined
private readonly clientId: string | undefined
private readonly clientSecret: string | undefined
+ private readonly redirectUri: string
+
private ok: boolean | undefined
+
private issuer: Issuer | undefined | null
+ private client: BaseClient | undefined | null
+ private authorizationUrl: string | null
+
protected readonly logger: {
debug: (s: string) => void
info: (s: string) => void
@@ -28,7 +36,8 @@ class ExternalAuthOIDC {
buttonLabel: string | undefined,
discoveryUrl: string | undefined,
clientId: string | undefined,
- clientSecret: string | undefined
+ clientSecret: string | undefined,
+ redirectUri: string
) {
this.logger = {
debug: (s) => logger.debug('[ExternalAuthOIDC] ' + s),
@@ -38,6 +47,8 @@ class ExternalAuthOIDC {
}
this.enabled = !!enabled
+ this.redirectUri = redirectUri
+ this.authorizationUrl = null
if (this.enabled) {
this.buttonLabel = buttonLabel
this.discoveryUrl = discoveryUrl
@@ -55,6 +66,16 @@ class ExternalAuthOIDC {
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
* @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
*/
- async loadIssuer (): Promise
{
- // this.issuer === null means we already tried, but it failed.
- if (this.issuer !== undefined) { return this.issuer }
+ async load (): Promise {
+ // this.client === null means we already tried, but it failed.
+ 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 {
this.issuer = await Issuer.discover(this.discoveryUrl as string)
@@ -137,8 +165,38 @@ class ExternalAuthOIDC {
} catch (err) {
this.logger.error(err as string)
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-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
+ settings['external-auth-custom-oidc-client-secret'] as string | undefined,
+ ExternalAuthOIDC.redirectUri(options)
)
return singleton
@@ -183,6 +242,13 @@ class ExternalAuthOIDC {
}
return singleton
}
+
+ public static redirectUri (options: RegisterServerOptions): string {
+ const path = getBaseRouterRoute(options) + 'oidc/cb'
+ return canonicalizePluginUri(options, path, {
+ removePluginVersion: true
+ })
+ }
}
export {
diff --git a/server/lib/settings.ts b/server/lib/settings.ts
index 99c72c8d..63912745 100644
--- a/server/lib/settings.ts
+++ b/server/lib/settings.ts
@@ -5,11 +5,13 @@ import { RoomChannel } from './room-channel'
import { BotsCtl } from './bots/ctl'
import { ExternalAuthOIDC } from './external-auth/oidc'
import { loc } from './loc'
+const escapeHTML = require('escape-html')
type AvatarSet = 'sepia' | 'cat' | 'bird' | 'fenec' | 'abstract' | 'legacy'
async function initSettings (options: RegisterServerOptions): Promise {
const { peertubeHelpers, settingsManager } = options
+ const logger = peertubeHelpers.logger
initImportantNotesSettings(options)
initChatSettings(options)
@@ -21,6 +23,30 @@ async function initSettings (options: RegisterServerOptions): Promise {
initChatServerAdvancedSettings(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']
@@ -30,6 +56,7 @@ async function initSettings (options: RegisterServerOptions): Promise {
// To avoid race condition, we will just stop and start the bots at every settings saving.
await BotsCtl.destroySingleton()
await BotsCtl.initSingleton(options)
+ loadOidc() // we don't have to wait (can take time, it will do external http requests)
await ExternalAuthOIDC.initSingleton(options)
@@ -145,12 +172,21 @@ function initFederationSettings ({ registerSetting }: RegisterServerOptions): vo
* Registers settings related to the "External Authentication" section.
* @param param0 server options
*/
-function initExternalAuth ({ registerSetting }: RegisterServerOptions): void {
+function initExternalAuth (options: RegisterServerOptions): void {
+ const registerSetting = options.registerSetting
+
registerSetting({
type: 'html',
private: true,
descriptionHTML: loc('external_auth_description')
})
+
+ registerSetting({
+ type: 'html',
+ private: true,
+ descriptionHTML: loc('experimental_warning')
+ })
+
registerSetting({
name: 'external-auth-custom-oidc',
label: loc('external_auth_custom_oidc_label'),
@@ -159,6 +195,20 @@ function initExternalAuth ({ registerSetting }: RegisterServerOptions): void {
default: false,
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: `${escapeHTML(ExternalAuthOIDC.redirectUri(options)) as string} `
+ })
+
registerSetting({
name: 'external-auth-custom-oidc-button-label',
label: loc('external_auth_custom_oidc_button_label_label'),
diff --git a/shared/lib/types.ts b/shared/lib/types.ts
index cf9473e3..6a109dbb 100644
--- a/shared/lib/types.ts
+++ b/shared/lib/types.ts
@@ -24,6 +24,7 @@ interface InitConverseJSParams {
autofocus?: boolean
externalAuthOIDC?: {
buttonLabel: string
+ url: string
}
}