Possibility to configure an OpenID Connect provider on the instance level WIP (#128).
This commit is contained in:
parent
e646ebfd69
commit
669b260307
@ -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).
|
||||||
|
@ -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.
|
||||||
|
@ -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>
|
||||||
|
@ -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])
|
||||||
|
@ -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
|
||||||
|
@ -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>"
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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',
|
||||||
|
@ -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 {
|
||||||
|
@ -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'),
|
||||||
|
@ -24,6 +24,7 @@ interface InitConverseJSParams {
|
|||||||
autofocus?: boolean
|
autofocus?: boolean
|
||||||
externalAuthOIDC?: {
|
externalAuthOIDC?: {
|
||||||
buttonLabel: string
|
buttonLabel: string
|
||||||
|
url: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user