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:
parent
4bc2d4fd51
commit
024186ba2c
@ -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).
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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))
|
@ -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'
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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({
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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}...`)
|
||||
|
@ -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.
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user