diff --git a/CHANGELOG.md b/CHANGELOG.md
index eafdf245..d9066ae7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,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:
* remote Peertube account,
* #128, #363 (**Experimental Feature**): possibility to configure an OpenID Connect provider on the instance level.
+ * #128: adding some standard OpenID Connect providers (Google, Facebook).
* #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: Fullscreen chat: now uses a custom page (in other words: when opening the chat in a new tab, you will have the Peertube menu).
diff --git a/client/admin-plugin-client-plugin.ts b/client/admin-plugin-client-plugin.ts
index 9ae4667b..faf8cd0f 100644
--- a/client/admin-plugin-client-plugin.ts
+++ b/client/admin-plugin-client-plugin.ts
@@ -236,8 +236,11 @@ function register (clientOptions: RegisterClientOptions): void {
return options.formValues['chat-no-anonymous'] !== false
}
- if (name?.startsWith('external-auth-custom-oidc-')) {
- return options.formValues['external-auth-custom-oidc'] !== true
+ if (name?.startsWith('external-auth-')) {
+ const m = name.match(/^external-auth-(\w+)-oidc-/)
+ if (m) {
+ return options.formValues['external-auth-' + m[1] + '-oidc'] !== true
+ }
}
return false
diff --git a/languages/de.yml b/languages/de.yml
index ea4821a7..843a0e8f 100644
--- a/languages/de.yml
+++ b/languages/de.yml
@@ -401,8 +401,8 @@ login_using_external_account: Mit einem externem Account anmelden
external_auth_custom_oidc_label: Verwenden eines OpenID Connect Anbieters
external_auth_custom_oidc_button_label_label: Name für die Anmeldungsschaltfläche
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_oidc_client_id_label: Client ID
+external_auth_oidc_client_secret_label: Client secret
external_auth_custom_oidc_title:
OpenID Connect
external_auth_custom_oidc_button_label_description: Diese Bezeichnung wird den Nutzern
als Name der Schaltfläche zur Authentifizierung bei diesem OIDC-Anbieter angezeigt.
@@ -414,6 +414,6 @@ external_auth_description: "Externe Authentifizierung
\nFür Benutzer, d
kein Peertubekonto haben, können Sie verschiedene Authentifizierungsmodi auf der
Grundlage von externen Authentifizierungsanbietern aktivieren.\n"
login_external_auth_alert_message: Authentifizierung fehlgeschlagen
-external_auth_custom_oidc_redirect_uris_info_description: "Callback/Redirect
+external_auth_oidc_redirect_uris_info_description: "Callback/Redirect
URI:\nWenn Sie eine autorisierte Umleitungs-URI für die externe Anwendung
konfigurieren möchten, fügen Sie bitte diese URL hinzu:\n"
diff --git a/languages/en.yml b/languages/en.yml
index fb6a5c3c..aa9582ff 100644
--- a/languages/en.yml
+++ b/languages/en.yml
@@ -83,12 +83,21 @@ external_auth_custom_oidc_button_label_label: "Label for the connection button"
external_auth_custom_oidc_button_label_description: "This label will be displayed to users, as the button label to authenticate with this OIDC provider."
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: |
+external_auth_oidc_client_id_label: "Client ID"
+external_auth_oidc_client_secret_label: "Client secret"
+external_auth_oidc_redirect_uris_info_description: |
Callback/Redirect URI:
If you want to configure authorized redirection URI on the external Application, please add this url:
+external_auth_google_oidc_label: 'Use Google'
+external_auth_google_oidc_description: |
+ Enabling this adds a "login with Google" button.
+ You have to configure a Google OAuth application.
+external_auth_facebook_oidc_label: 'Use Facebook'
+external_auth_facebook_oidc_description: |
+ Enabling this adds a "login with Facebook" button.
+ You have to configure a Facebook OAuth application.
+
chat_behaviour_description: "Chat behaviour
"
room_type_label: "Room type"
diff --git a/languages/hr.yml b/languages/hr.yml
index 26c5ddeb..a9b930ab 100644
--- a/languages/hr.yml
+++ b/languages/hr.yml
@@ -2,8 +2,8 @@ login_remote_peertube_video_not_found_try_anyway_button: Svejedno pokušaj otvor
video na Peertube instanci
external_auth_custom_oidc_button_label_label: Oznaka za gumb povezivanja
external_auth_custom_oidc_discovery_url_label: Discovery URL
-external_auth_custom_oidc_client_id_label: ID klijenta
-external_auth_custom_oidc_client_secret_label: Tajna klijenta
+external_auth_oidc_client_id_label: ID klijenta
+external_auth_oidc_client_secret_label: Tajna klijenta
videos_list_label: Aktiviraj chat za ova videa
prosody_peertube_uri_label: Peertube URL za API pozive
save: Spremi
diff --git a/server/lib/conversejs/params.ts b/server/lib/conversejs/params.ts
index f25335a2..b97d0b38 100644
--- a/server/lib/conversejs/params.ts
+++ b/server/lib/conversejs/params.ts
@@ -85,10 +85,10 @@ async function getConverseJSParams (
)
} else {
try {
- const oidc = ExternalAuthOIDC.singleton()
- if (await oidc.isOk()) {
- const authUrl = oidc.getConnectUrl()
- const buttonLabel = oidc.getButtonLabel()
+ const customOidc = ExternalAuthOIDC.singleton('custom')
+ if (await customOidc.isOk()) {
+ const authUrl = customOidc.getConnectUrl()
+ const buttonLabel = customOidc.getButtonLabel()
if (authUrl && buttonLabel) {
externalAuthOIDC = {
buttonLabel: buttonLabel,
diff --git a/server/lib/diagnostic/external-auth-custom-oidc.ts b/server/lib/diagnostic/external-auth-oidc.ts
similarity index 74%
rename from server/lib/diagnostic/external-auth-custom-oidc.ts
rename to server/lib/diagnostic/external-auth-oidc.ts
index e901c50a..da3616cc 100644
--- a/server/lib/diagnostic/external-auth-custom-oidc.ts
+++ b/server/lib/diagnostic/external-auth-oidc.ts
@@ -1,14 +1,19 @@
import type { RegisterServerOptions } from '@peertube/peertube-types'
import { newResult, TestResult } from './utils'
-import { ExternalAuthOIDC } from '../external-auth/oidc'
+import { ExternalAuthOIDC, ExternalAuthOIDCType } from '../external-auth/oidc'
-export async function diagExternalAuthCustomOIDC (test: string, _options: RegisterServerOptions): Promise {
+export async function diagExternalAuthOIDC (
+ test: string,
+ _options: RegisterServerOptions,
+ singletonType: ExternalAuthOIDCType,
+ next: TestResult['next']
+): Promise {
const result = newResult(test)
- result.label = 'Test External Auth Custom OIDC'
- result.next = 'everything-ok'
+ result.label = 'Test External Auth OIDC: ' + singletonType
+ result.next = next
try {
- const oidc = ExternalAuthOIDC.singleton()
+ const oidc = ExternalAuthOIDC.singleton(singletonType)
if (oidc.isDisabledBySettings()) {
result.ok = true
@@ -40,7 +45,7 @@ export async function diagExternalAuthCustomOIDC (test: string, _options: Regist
return result
}
- const oidc = ExternalAuthOIDC.singleton()
+ const oidc = ExternalAuthOIDC.singleton(singletonType)
const oidcClient = await oidc.load()
if (oidcClient) {
result.messages.push('Discovery URL loaded: ' + JSON.stringify(oidcClient.issuer.metadata))
diff --git a/server/lib/diagnostic/index.ts b/server/lib/diagnostic/index.ts
index 497bca58..f3ce0436 100644
--- a/server/lib/diagnostic/index.ts
+++ b/server/lib/diagnostic/index.ts
@@ -4,7 +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 { diagExternalAuthOIDC } from './external-auth-oidc'
import { helpUrl } from '../../../shared/lib/help'
export async function diag (test: string, options: RegisterServerOptions): Promise {
@@ -19,7 +19,11 @@ export async function diag (test: string, options: RegisterServerOptions): Promi
} else if (test === 'prosody') {
result = await diagProsody(test, options)
} else if (test === 'external-auth-custom-oidc') {
- result = await diagExternalAuthCustomOIDC(test, options)
+ result = await diagExternalAuthOIDC(test, options, 'custom', 'external-auth-google-oidc')
+ } else if (test === 'external-auth-google-oidc') {
+ result = await diagExternalAuthOIDC(test, options, 'google', 'external-auth-facebook-oidc')
+ } else if (test === 'external-auth-facebook-oidc') {
+ result = await diagExternalAuthOIDC(test, options, 'facebook', 'everything-ok')
} else if (test === 'everything-ok') {
result = newResult(test)
result.label = 'Everything seems fine'
diff --git a/server/lib/diagnostic/utils.ts b/server/lib/diagnostic/utils.ts
index 5bb4211e..ce8f1a70 100644
--- a/server/lib/diagnostic/utils.ts
+++ b/server/lib/diagnostic/utils.ts
@@ -1,4 +1,6 @@
-type nextValue = 'backend' | 'debug' | 'webchat-video' | 'prosody' | 'external-auth-custom-oidc' | 'everything-ok'
+type NextValue = 'backend' | 'debug' | 'webchat-video' | 'prosody'
+| 'external-auth-custom-oidc' | 'external-auth-google-oidc' | 'external-auth-facebook-oidc'
+| 'everything-ok'
interface MessageWithLevel {
level: 'info' | 'warning' | 'error'
@@ -15,7 +17,7 @@ export interface TestResult {
title: string
message: string
}>
- next: nextValue | null
+ next: NextValue | null
ok: boolean
test: string
}
diff --git a/server/lib/external-auth/oidc.ts b/server/lib/external-auth/oidc.ts
index 46dd25b0..a9fd04e4 100644
--- a/server/lib/external-auth/oidc.ts
+++ b/server/lib/external-auth/oidc.ts
@@ -49,13 +49,14 @@ function getMimeTypeFromArrayBuffer (arrayBuffer: ArrayBuffer): AcceptableAvatar
type UserInfoField = 'username' | 'last_name' | 'first_name' | 'nickname' | 'picture'
interface UnserializedToken {
+ type: ExternalAuthOIDCType
jid: string
password: string
nickname: string
expire: Date
}
-let singleton: ExternalAuthOIDC | undefined
+let singletons: Map | undefined
async function getRandomBytes (size: number): Promise {
return new Promise((resolve, reject) => {
@@ -67,10 +68,13 @@ async function getRandomBytes (size: number): Promise {
})
}
+type ExternalAuthOIDCType = 'custom' | 'google' | 'facebook'
+
/**
* This class handles the external OpenId Connect provider, if defined.
*/
class ExternalAuthOIDC {
+ private readonly singletonType: ExternalAuthOIDCType
private readonly enabled: boolean
private readonly buttonLabel: string | undefined
private readonly discoveryUrl: string | undefined
@@ -82,7 +86,6 @@ class ExternalAuthOIDC {
private readonly externalVirtualhost: string
private readonly avatarsDir: string
private readonly avatarsFiles: string[]
- private pruneTimer?: NodeJS.Timer
private readonly encryptionOptions = {
algorithm: 'aes256' as string,
@@ -113,6 +116,7 @@ class ExternalAuthOIDC {
constructor (params: {
logger: RegisterServerOptions['peertubeHelpers']['logger']
+ singletonType: ExternalAuthOIDCType
enabled: boolean
buttonLabel: string | undefined
discoveryUrl: string | undefined
@@ -132,6 +136,7 @@ class ExternalAuthOIDC {
error: (s) => params.logger.error('[ExternalAuthOIDC] ' + s)
}
+ this.singletonType = params.singletonType
this.enabled = !!params.enabled
this.secretKey = params.secretKey
this.redirectUrl = params.redirectUrl
@@ -148,6 +153,13 @@ class ExternalAuthOIDC {
}
}
+ /**
+ * This singleton type.
+ */
+ public get type (): ExternalAuthOIDCType {
+ return this.singletonType
+ }
+
/**
* Indicates that the OIDC is disabled.
* Caution: this does not indicate if it is enabled, but poorly configured.
@@ -390,13 +402,15 @@ class ExternalAuthOIDC {
// Now we will encrypt jid + password, and return it to the browser.
// The browser will be able to use this encrypted data with the api/configuration/room API.
const tokenContent: UnserializedToken = {
+ type: this.type,
jid,
password,
nickname,
// expires in 12 hours (user will just have to do the whole process again).
expire: (new Date(Date.now() + 12 * 3600 * 1000))
}
- const token = await this.encrypt(JSON.stringify(tokenContent))
+ // Token is prefixed by the type, so we can get the correct singleton for deserializing.
+ const token = this.type + '-' + await this.encrypt(JSON.stringify(tokenContent))
let avatar = await this.readUserInfoPicture(userInfo)
if (!avatar) {
@@ -447,12 +461,24 @@ class ExternalAuthOIDC {
*/
public async unserializeToken (token: string): Promise {
try {
+ // First, check the prefix:
+ if (!token.startsWith(this.type + '-')) {
+ throw new Error('Wrong token prefix')
+ }
+ // Removing the prefix:
+ token = token.substring(this.type.length + 1)
+
const decrypted = await this.decrypt(token)
const o = JSON.parse(decrypted) // can fail
if (typeof o !== 'object') {
throw new Error('Invalid encrypted data')
}
+
+ if (o.type !== this.type) {
+ throw new Error('Token type is not the expected one')
+ }
+
if (typeof o.jid !== 'string' || o.jid === '') {
throw new Error('No jid')
}
@@ -473,6 +499,7 @@ class ExternalAuthOIDC {
}
return {
+ type: o.type,
jid: o.jid,
password: o.password,
nickname: o.nickname,
@@ -623,107 +650,146 @@ class ExternalAuthOIDC {
}
/**
- * Starts an interval timer to prune external users from Prosody.
- * @param options Peertube server options.
+ * frees the singletons
*/
- public startPruneTimer (options: RegisterServerOptions): void {
- this.stopPruneTimer() // just in case...
+ public static async destroySingletons (): Promise {
+ if (!singletons) { return }
- // every hour (every minutes in debug mode)
- const pruneInterval = debugNumericParameter(options, 'externalAccountPruneInterval', 60 * 1000, 60 * 60 * 1000)
- this.logger.info(`Creating a timer for external account pruning, every ${Math.round(pruneInterval / 1000)}s.`)
+ stopPruneTimer()
- // eslint-disable-next-line @typescript-eslint/no-misused-promises
- this.pruneTimer = setInterval(async () => {
- try {
- if (!await this.isOk()) { return }
+ const keys = singletons.keys()
+ for (const key of keys) {
+ const singleton = singletons.get(key)
+ if (!singleton) { continue }
+ singletons.delete(key)
+ }
- this.logger.info('Pruning external users...')
- await pruneUsers(options)
- } catch (err) {
- this.logger.error('Error while pruning external users: ' + (err as string))
- }
- }, pruneInterval)
+ singletons = undefined
}
/**
- * Stops the prune timer.
+ * Instanciate all singletons.
+ * Note: no need to destroy singletons before creating new ones.
*/
- public stopPruneTimer (): void {
- if (!this.pruneTimer) { return }
- clearInterval(this.pruneTimer)
- this.pruneTimer = undefined
- }
+ public static async initSingletons (options: RegisterServerOptions): Promise {
+ const prosodyDomain = await getProsodyDomain(options)
+ // FIXME: this is not optimal to call here.
+ const prosodyFilePaths = await getProsodyFilePaths(options)
- /**
- * frees the singleton
- */
- public static async destroySingleton (): Promise {
- if (!singleton) { return }
- singleton.stopPruneTimer()
- 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'
+ 'external-auth-custom-oidc-client-secret',
+ 'external-auth-google-oidc',
+ 'external-auth-google-oidc-client-id',
+ 'external-auth-google-oidc-client-secret',
+ 'external-auth-facebook-oidc',
+ 'external-auth-facebook-oidc-client-id',
+ 'external-auth-facebook-oidc-client-secret'
])
- // Generating a secret key that will be used for the authenticatio process (can change on restart).
- const secretKey = (await getRandomBytes(16)).toString('hex')
+ const init = async function initSingleton (
+ singletonType: ExternalAuthOIDCType,
+ buttonLabel: string | undefined,
+ discoveryUrl: string | undefined
+ ): Promise {
+ // Generating a secret key that will be used for the authenticatio process (can change on restart).
+ const secretKey = (await getRandomBytes(16)).toString('hex')
- const prosodyDomain = await getProsodyDomain(options)
+ const singleton = new ExternalAuthOIDC({
+ logger: options.peertubeHelpers.logger,
+ singletonType,
+ enabled: settings['external-auth-' + singletonType + '-oidc'] as boolean,
+ buttonLabel,
+ discoveryUrl,
+ clientId: settings['external-auth-' + singletonType + '-oidc-client-id'] as string | undefined,
+ clientSecret: settings['external-auth-' + singletonType + '-oidc-client-secret'] as string | undefined,
+ secretKey,
+ connectUrl: ExternalAuthOIDC.connectUrl(options, singletonType),
+ redirectUrl: ExternalAuthOIDC.redirectUrl(options, singletonType),
+ externalVirtualhost: 'external.' + prosodyDomain,
+ avatarsDir: prosodyFilePaths.avatars,
+ avatarsFiles: prosodyFilePaths.avatarsFiles
+ })
- // FIXME: this is not optimal to call here.
- const prosodyFilePaths = await getProsodyFilePaths(options)
+ singletons ??= new Map()
+ singletons.set(singletonType, singleton)
+ }
- singleton = new ExternalAuthOIDC({
- logger: options.peertubeHelpers.logger,
- enabled: settings['external-auth-custom-oidc'] as boolean,
- buttonLabel: settings['external-auth-custom-oidc-button-label'] as string | undefined,
- discoveryUrl: settings['external-auth-custom-oidc-discovery-url'] as string | undefined,
- clientId: settings['external-auth-custom-oidc-client-id'] as string | undefined,
- clientSecret: settings['external-auth-custom-oidc-client-secret'] as string | undefined,
- secretKey,
- connectUrl: ExternalAuthOIDC.connectUrl(options),
- redirectUrl: ExternalAuthOIDC.redirectUrl(options),
- externalVirtualhost: 'external.' + prosodyDomain,
- avatarsDir: prosodyFilePaths.avatars,
- avatarsFiles: prosodyFilePaths.avatarsFiles
- })
+ await Promise.all([
+ init(
+ 'custom',
+ settings['external-auth-custom-oidc-button-label'] as string | undefined,
+ settings['external-auth-custom-oidc-discovery-url'] as string | undefined
+ ),
+ init(
+ 'google',
+ 'Google',
+ 'https://accounts.google.com'
+ ),
+ init(
+ 'facebook',
+ 'Facebook',
+ 'https://www.facebook.com'
+ )
+ ])
- singleton.startPruneTimer(options)
-
- return singleton
+ startPruneTimer(options)
}
/**
* Gets the singleton, or raise an exception if it is too soon.
+ * @param ExternalAuthOIDCType The singleton type.
* @throws Error
* @returns the singleton
*/
- public static singleton (): ExternalAuthOIDC {
+ public static singleton (singletonType: ExternalAuthOIDCType | string): ExternalAuthOIDC {
+ if (!singletons) {
+ throw new Error('ExternalAuthOIDC singletons are not initialized yet')
+ }
+ const singleton = singletons.get(singletonType as ExternalAuthOIDCType)
if (!singleton) {
- throw new Error('ExternalAuthOIDC singleton is not initialized yet')
+ throw new Error(`ExternalAuthOIDC singleton "${singletonType}" is not initiliazed yet`)
}
return singleton
}
+ /**
+ * Get all initialiazed singletons.
+ */
+ public static allSingletons (): ExternalAuthOIDC[] {
+ if (!singletons) { return [] }
+ return Array.from(singletons.values())
+ }
+
+ /**
+ * Reading header X-Peertube-Plugin-Livechat-External-Auth-OIDC-Token,
+ * got the singleton that is supposed to read the token.
+ * Note: the token must be unserialized before supposing it is valid!
+ * @param token the authentication token
+ */
+ public static singletonForToken (token: string): ExternalAuthOIDC | null {
+ try {
+ const m = token.match(/^(\w+)-/)
+ if (!m) { return null }
+ return ExternalAuthOIDC.singleton(m[1])
+ } catch (err) {
+ return null
+ }
+ }
+
/**
* Get the uri to start the authentication process.
* @param options Peertube server options
* @returns the uri
*/
- public static connectUrl (options: RegisterServerOptions): string {
- const path = getBaseRouterRoute(options) + 'oidc/connect'
+ public static connectUrl (options: RegisterServerOptions, type: ExternalAuthOIDCType): string {
+ if (!/^\w+$/.test(type)) {
+ throw new Error('Invalid singleton type')
+ }
+ const path = getBaseRouterRoute(options) + 'oidc/' + type + '/connect'
return canonicalizePluginUri(options, path, {
removePluginVersion: true
})
@@ -734,14 +800,67 @@ class ExternalAuthOIDC {
* @param options Peertube server optiosn
* @returns the uri
*/
- public static redirectUrl (options: RegisterServerOptions): string {
- const path = getBaseRouterRoute(options) + 'oidc/cb'
+ public static redirectUrl (options: RegisterServerOptions, type: ExternalAuthOIDCType): string {
+ if (!/^\w+$/.test(type)) {
+ throw new Error('Invalid singleton type')
+ }
+ const path = getBaseRouterRoute(options) + 'oidc/' + type + '/cb'
return canonicalizePluginUri(options, path, {
removePluginVersion: true
})
}
}
-export {
- ExternalAuthOIDC
+let pruneTimer: NodeJS.Timer | undefined
+
+/**
+ * Starts an interval timer to prune external users from Prosody.
+ * @param options Peertube server options.
+ */
+function startPruneTimer (options: RegisterServerOptions): void {
+ stopPruneTimer() // just in case...
+
+ const logger = {
+ debug: (s: string) => options.peertubeHelpers.logger.debug('[ExternalAuthOIDC startPruneTimer] ' + s),
+ info: (s: string) => options.peertubeHelpers.logger.info('[ExternalAuthOIDC startPruneTimer] ' + s),
+ warn: (s: string) => options.peertubeHelpers.logger.warn('[ExternalAuthOIDC startPruneTimer] ' + s),
+ error: (s: string) => options.peertubeHelpers.logger.error('[ExternalAuthOIDC startPruneTimer] ' + s)
+ }
+
+ // every hour (every minutes in debug mode)
+ const pruneInterval = debugNumericParameter(options, 'externalAccountPruneInterval', 60 * 1000, 60 * 60 * 1000)
+ logger.info(`Creating a timer for external account pruning, every ${Math.round(pruneInterval / 1000)}s.`)
+
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
+ pruneTimer = setInterval(async () => {
+ try {
+ // Checking if at least one active singleton
+ let ok = false
+ for (const oidc of ExternalAuthOIDC.allSingletons()) {
+ if (!await oidc.isOk()) { continue }
+ ok = true
+ break
+ }
+ if (!ok) { return }
+
+ logger.info('Pruning external users...')
+ await pruneUsers(options)
+ } catch (err) {
+ logger.error('Error while pruning external users: ' + (err as string))
+ }
+ }, pruneInterval)
+}
+
+/**
+ * Stops the prune timer.
+ */
+function stopPruneTimer (): void {
+ if (!pruneTimer) { return }
+ clearInterval(pruneTimer)
+ pruneTimer = undefined
+}
+
+export {
+ ExternalAuthOIDC,
+ ExternalAuthOIDCType
}
diff --git a/server/lib/prosody/config.ts b/server/lib/prosody/config.ts
index 74fbb87b..b7b8b269 100644
--- a/server/lib/prosody/config.ts
+++ b/server/lib/prosody/config.ts
@@ -197,9 +197,13 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise we must enable the external virtual host.
+ useExternal = true
+ break
+ }
}
} catch (err) {
logger.error(err)
diff --git a/server/lib/routers/api/auth.ts b/server/lib/routers/api/auth.ts
index c8237e85..b59c4990 100644
--- a/server/lib/routers/api/auth.ts
+++ b/server/lib/routers/api/auth.ts
@@ -21,8 +21,8 @@ async function initAuthApiRouter (options: RegisterServerOptions, router: Router
const token = req.header('X-Peertube-Plugin-Livechat-External-Auth-OIDC-Token')
if (token) {
try {
- const oidc = ExternalAuthOIDC.singleton()
- if (await oidc.isOk()) {
+ const oidc = ExternalAuthOIDC.singletonForToken(token)
+ if (oidc && await oidc.isOk()) {
const unserializedToken = await oidc.unserializeToken(token)
if (unserializedToken) {
res.status(200).json({
diff --git a/server/lib/routers/oidc.ts b/server/lib/routers/oidc.ts
index a9b3a6e2..c778e842 100644
--- a/server/lib/routers/oidc.ts
+++ b/server/lib/routers/oidc.ts
@@ -36,11 +36,12 @@ async function initOIDCRouter (options: RegisterServerOptions): Promise
const router = getRouter()
const logger = peertubeHelpers.logger
- router.get('/connect', asyncMiddleware(
+ router.get('/:type?/connect', asyncMiddleware(
async (req: Request, res: Response, next: NextFunction) => {
- logger.info('[oidc router] OIDC connect call')
+ const singletonType = req.params.type ?? 'custom'
+ logger.info('[oidc router] OIDC connect call (' + singletonType + ')')
try {
- const oidc = ExternalAuthOIDC.singleton()
+ const oidc = ExternalAuthOIDC.singleton(singletonType)
const oidcClient = await oidc.load()
if (!oidcClient) {
throw new Error('[oidc router] External Auth OIDC not loaded yet')
@@ -57,9 +58,10 @@ async function initOIDCRouter (options: RegisterServerOptions): Promise
const cbHandler = asyncMiddleware(
async (req: Request, res: Response, _next: NextFunction) => {
- logger.info('[oidc router] OIDC callback call')
+ const singletonType = req.params.type ?? 'custom'
+ logger.info('[oidc router] OIDC callback call (' + singletonType + ')')
try {
- const oidc = ExternalAuthOIDC.singleton()
+ const oidc = ExternalAuthOIDC.singleton(singletonType)
const oidcClient = await oidc.load()
if (!oidcClient) {
throw new Error('[oidc router] External Auth OIDC not loaded yet')
@@ -102,8 +104,8 @@ async function initOIDCRouter (options: RegisterServerOptions): Promise
}
}
)
- router.get('/cb', cbHandler)
- router.post('/cb', cbHandler)
+ router.get('/:type?/cb', cbHandler)
+ router.post('/:type?/cb', cbHandler)
return router
}
diff --git a/server/lib/settings.ts b/server/lib/settings.ts
index 4ccc5b7f..64c62844 100644
--- a/server/lib/settings.ts
+++ b/server/lib/settings.ts
@@ -3,7 +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 { ExternalAuthOIDC, ExternalAuthOIDCType } from './external-auth/oidc'
import { loc } from './loc'
const escapeHTML = require('escape-html')
@@ -22,31 +22,35 @@ async function initSettings (options: RegisterServerOptions): Promise {
initThemingSettings(options)
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)
+ await ExternalAuthOIDC.initSingletons(options)
+ const loadOidcs = (): void => {
+ const oidcs = ExternalAuthOIDC.allSingletons()
+ for (const oidc of oidcs) {
+ try {
+ const type = oidc.type
+ oidc.isOk().then(
+ () => {
+ logger.info(`Loading External Auth OIDC ${type}...`)
+ oidc.load().then(
+ () => {
+ logger.info(`External Auth OIDC ${type} loaded`)
+ },
+ () => {
+ logger.error(`Loading the External Auth OIDC ${type} failed`)
+ }
+ )
+ },
+ () => {
+ logger.info(`No valid External Auth OIDC ${type}, nothing loaded`)
+ }
+ )
+ } catch (err) {
+ logger.error(err as string)
+ continue
+ }
}
}
- loadOidc() // we don't have to wait (can take time, it will do external http requests)
+ loadOidcs() // 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']
@@ -56,9 +60,9 @@ 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)
+ loadOidcs() // we don't have to wait (can take time, it will do external http requests)
- await ExternalAuthOIDC.initSingleton(options)
+ await ExternalAuthOIDC.initSingletons(options)
peertubeHelpers.logger.info('Saving settings, ensuring prosody is running')
await ensureProsodyRunning(options)
@@ -206,13 +210,13 @@ function initExternalAuth (options: RegisterServerOptions): void {
type: 'html',
name: 'external-auth-custom-oidc-redirect-uris-info',
private: true,
- descriptionHTML: loc('external_auth_custom_oidc_redirect_uris_info_description')
+ descriptionHTML: loc('external_auth_oidc_redirect_uris_info_description')
})
registerSetting({
type: 'html',
name: 'external-auth-custom-oidc-redirect-uris',
private: true,
- descriptionHTML: `- ${escapeHTML(ExternalAuthOIDC.redirectUrl(options)) as string}
`
+ descriptionHTML: `- ${escapeHTML(ExternalAuthOIDC.redirectUrl(options, 'custom')) as string}
`
})
registerSetting({
@@ -232,15 +236,15 @@ function initExternalAuth (options: RegisterServerOptions): void {
})
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'),
+ label: loc('external_auth_oidc_client_id_label'),
+ // descriptionHTML: loc('external_auth_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'),
+ label: loc('external_auth_oidc_client_secret_label'),
+ // descriptionHTML: loc('external_auth_oidc_client_secret_description'),
type: 'input-password',
private: true
})
@@ -250,6 +254,52 @@ function initExternalAuth (options: RegisterServerOptions): void {
// - user-name property
// - display-name property
// - picture property
+
+ // Standard providers....
+ for (const provider of ['google', 'facebook']) {
+ let redirectUrl
+ try {
+ redirectUrl = ExternalAuthOIDC.redirectUrl(options, provider as ExternalAuthOIDCType)
+ } catch (err) {
+ options.peertubeHelpers.logger.error('Cant load redirect url for provider ' + provider)
+ options.peertubeHelpers.logger.error(err)
+ continue
+ }
+ registerSetting({
+ name: 'external-auth-' + provider + '-oidc',
+ label: loc('external_auth_' + provider + '_oidc_label'),
+ descriptionHTML: loc('external_auth_' + provider + '_oidc_description'),
+ type: 'input-checkbox',
+ default: false,
+ private: true
+ })
+ registerSetting({
+ type: 'html',
+ name: 'external-auth-' + provider + '-oidc-redirect-uris-info',
+ private: true,
+ descriptionHTML: loc('external_auth_oidc_redirect_uris_info_description')
+ })
+ registerSetting({
+ type: 'html',
+ name: 'external-auth-' + provider + '-oidc-redirect-uris',
+ private: true,
+ descriptionHTML: `- ${escapeHTML(redirectUrl) as string}
`
+ })
+ registerSetting({
+ name: 'external-auth-' + provider + '-oidc-client-id',
+ label: loc('external_auth_oidc_client_id_label'),
+ // descriptionHTML: loc('external_auth_' + provider + '_oidc_client_id_description'),
+ type: 'input',
+ private: true
+ })
+ registerSetting({
+ name: 'external-auth-' + provider + '-oidc-client-secret',
+ label: loc('external_auth_oidc_client_secret_label'),
+ // descriptionHTML: loc('external_auth_' + provider + '_oidc_client_secret_description'),
+ type: 'input-password',
+ private: true
+ })
+ }
}
/**
diff --git a/server/main.ts b/server/main.ts
index ae37b674..d18bddbb 100644
--- a/server/main.ts
+++ b/server/main.ts
@@ -94,7 +94,7 @@ async function unregister (): Promise {
await RoomChannel.destroySingleton()
await BotConfiguration.destroySingleton()
- await ExternalAuthOIDC.destroySingleton()
+ await ExternalAuthOIDC.destroySingletons()
const module = __filename
OPTIONS?.peertubeHelpers.logger.info(`Unloading module ${module}...`)
diff --git a/support/documentation/content/en/documentation/admin/external_auth.md b/support/documentation/content/en/documentation/admin/external_auth.md
index f3b7fa46..333c0d10 100644
--- a/support/documentation/content/en/documentation/admin/external_auth.md
+++ b/support/documentation/content/en/documentation/admin/external_auth.md
@@ -63,11 +63,11 @@ Just set here the discovery url, that should be something like `https://example.
Note: if your provider use the standard `/.well-known/openid-configuration` path, you can omit it.
For example `https://accounts.google.com` will work.
-### {{% livechat_label external_auth_custom_oidc_client_id_label %}}
+### {{% livechat_label external_auth_oidc_client_id_label %}}
Your application Client ID.
-### {{% livechat_label external_auth_custom_oidc_client_secret_label %}}
+### {{% livechat_label external_auth_oidc_client_secret_label %}}
You application Client secret.