Adding some standard OpenID Connect providers (Google, Facebook) (WIP):

* refactoring, to allow several OIDC singletons
* settings for google and facebook
* backend code
This commit is contained in:
John Livingston 2024-04-22 13:03:31 +02:00
parent 4bc2d4fd51
commit 024186ba2c
No known key found for this signature in database
GPG Key ID: B17B5640CE66CDBC
16 changed files with 341 additions and 142 deletions

View File

@ -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).

View File

@ -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

View File

@ -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: <h4>OpenID Connect</h4>
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: "<h3>Externe Authentifizierung</h3>\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: "<strong>Callback/Redirect
external_auth_oidc_redirect_uris_info_description: "<strong>Callback/Redirect
URI:</strong>\nWenn Sie eine autorisierte Umleitungs-URI für die externe Anwendung
konfigurieren möchten, fügen Sie bitte diese URL hinzu:\n"

View File

@ -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: |
<strong>Callback/Redirect URI:</strong>
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: "<h3>Chat behaviour</h3>"
room_type_label: "Room type"

View File

@ -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

View File

@ -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,

View File

@ -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<TestResult> {
export async function diagExternalAuthOIDC (
test: string,
_options: RegisterServerOptions,
singletonType: ExternalAuthOIDCType,
next: TestResult['next']
): Promise<TestResult> {
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))

View File

@ -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<TestResult> {
@ -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'

View File

@ -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
}

View File

@ -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<ExternalAuthOIDCType, ExternalAuthOIDC> | undefined
async function getRandomBytes (size: number): Promise<Buffer> {
return new Promise((resolve, reject) => {
@ -67,10 +68,13 @@ async function getRandomBytes (size: number): Promise<Buffer> {
})
}
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<UnserializedToken | null> {
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<void> {
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<void> {
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<void> {
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<ExternalAuthOIDC> {
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<void> {
// 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<ExternalAuthOIDCType, ExternalAuthOIDC>()
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
}

View File

@ -197,9 +197,13 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
let useExternal: boolean = false
try {
const oidc = ExternalAuthOIDC.singleton()
if (await oidc.isOk()) {
useExternal = true
const oidcs = ExternalAuthOIDC.allSingletons()
for (const oidc of oidcs) {
if (await oidc.isOk()) {
// At least one external authentcation => we must enable the external virtual host.
useExternal = true
break
}
}
} catch (err) {
logger.error(err)

View File

@ -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({

View File

@ -36,11 +36,12 @@ async function initOIDCRouter (options: RegisterServerOptions): Promise<Router>
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<Router>
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>
}
}
)
router.get('/cb', cbHandler)
router.post('/cb', cbHandler)
router.get('/:type?/cb', cbHandler)
router.post('/:type?/cb', cbHandler)
return router
}

View File

@ -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<void> {
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<void> {
// 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: `<ul><li>${escapeHTML(ExternalAuthOIDC.redirectUrl(options)) as string}</li></ul>`
descriptionHTML: `<ul><li>${escapeHTML(ExternalAuthOIDC.redirectUrl(options, 'custom')) as string}</li></ul>`
})
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: `<ul><li>${escapeHTML(redirectUrl) as string}</li></ul>`
})
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
})
}
}
/**

View File

@ -94,7 +94,7 @@ async function unregister (): Promise<any> {
await RoomChannel.destroySingleton()
await BotConfiguration.destroySingleton()
await ExternalAuthOIDC.destroySingleton()
await ExternalAuthOIDC.destroySingletons()
const module = __filename
OPTIONS?.peertubeHelpers.logger.info(`Unloading module ${module}...`)

View File

@ -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.