Possibility to configure an OpenID Connect provider on the instance level WIP (#128).

This commit is contained in:
John Livingston 2024-04-17 16:35:26 +02:00
parent 8574ab581d
commit 3a5f27e751
No known key found for this signature in database
GPG Key ID: B17B5640CE66CDBC
7 changed files with 230 additions and 1 deletions

View File

@ -0,0 +1,5 @@
# mod_http_peertubelivechat_manage_users
This module is a custom module that allows Peertube server to manage users for some virtualhosts.
This module is part of peertube-plugin-livechat, and is under the same LICENSE.

View File

@ -0,0 +1,95 @@
local json = require "util.json";
local jid_split = require"util.jid".split;
local usermanager = require "core.usermanager";
module:depends"http";
local module_host = module:get_host(); -- this module is not global
function check_auth(routes)
local function check_request_auth(event)
local apikey = module:get_option_string("peertubelivechat_manage_users_apikey", "")
if apikey == "" then
return false, 500;
end
if event.request.headers.authorization ~= "Bearer " .. apikey then
return false, 401;
end
return true;
end
for route, handler in pairs(routes) do
routes[route] = function (event, ...)
local permit, code = check_request_auth(event);
if not permit then
return code;
end
return handler(event, ...);
end;
end
return routes;
end
local function ensure_user(event)
local request, response = event.request, event.response;
event.response.headers["Content-Type"] = "application/json";
local config = json.decode(request.body);
if not config.jid then
return json.encode({
result = "failed";
});
end
module:log("debug", "Calling ensure_user", config.jid);
local username, host = jid_split(config.jid);
if module_host ~= host then
module:log("error", "Wrong host", host);
return json.encode({
result = "failed";
message = "Wrong host"
});
end
-- TODO: handle avatars.
-- if user exists, just update.
if usermanager.user_exists(username, host) then
module:log("debug", "User already exists, updating", filename);
if not usermanager.set_password(username, config.password, host, nil) then
module:log("error", "Failed to update the password", host);
return json.encode({
result = "failed";
message = "Failed to update the password"
});
end
return json.encode({
result = "ok";
message = "User updated"
});
end
-- we must create the user.
module:log("debug", "User does not exists, creating", filename);
if (not usermanager.create_user(username, config.password, host)) then
module:log("error", "Failed to create the user", host);
return json.encode({
result = "failed";
message = "Failed to create the user"
});
end
return json.encode({
result = "ok";
message = "User created"
});
end
-- TODO: add a function to prune user that have not logged in since X days.
module:provides("http", {
route = check_auth {
["POST /" .. module_host .. "/ensure-user"] = ensure_user;
};
});

View File

@ -457,6 +457,7 @@ class ExternalAuthOIDC {
/** /**
* Gets the singleton, or raise an exception if it is too soon. * Gets the singleton, or raise an exception if it is too soon.
* @throws Error
* @returns the singleton * @returns the singleton
*/ */
public static singleton (): ExternalAuthOIDC { public static singleton (): ExternalAuthOIDC {

View File

@ -0,0 +1,66 @@
import type { RegisterServerOptions } from '@peertube/peertube-types'
import type { ExternalAccountInfos } from '../../external-auth/types'
import { getCurrentProsody } from './host'
import { getProsodyDomain } from '../config/domain'
import { getAPIKey } from '../../apikey'
const got = require('got')
/**
* Created or updates a user.
* Can be used to manage external accounts for example (create the user the first time, update infos next time).
* Uses an API provided by mod_http_peertubelivechat_manage_users.
*
* @param options Peertube server options
* @param data up-to-date user infos.
* @returns true if success
*/
async function ensureUser (options: RegisterServerOptions, infos: ExternalAccountInfos): Promise<boolean> {
const logger = options.peertubeHelpers.logger
const currentProsody = getCurrentProsody()
if (!currentProsody) {
throw new Error('It seems that prosody is not binded... Cant call API.')
}
const prosodyDomain = await getProsodyDomain(options)
logger.info('Calling ensureUser for ' + infos.jid)
// Requesting on localhost, because currentProsody.host does not always resolves correctly (docker use case, ...)
const apiUrl = `http://localhost:${currentProsody.port}/` +
'peertubelivechat_manage_users/' +
`external.${prosodyDomain}/` + // the virtual host name
'ensure-user'
const apiData = {
jid: infos.jid,
nickname: infos.nickname,
password: infos.password
}
try {
logger.debug('Calling ensure-user API on url: ' + apiUrl)
const result = await got(apiUrl, {
method: 'POST',
headers: {
authorization: 'Bearer ' + await getAPIKey(options),
host: currentProsody.host
},
json: apiData,
responseType: 'json',
resolveBodyOnly: true
})
logger.debug('ensure-user API response: ' + JSON.stringify(result))
if (result.result !== 'ok') {
logger.error('ensure-user API has failed: ' + JSON.stringify(result))
return false
}
} catch (err) {
logger.error(`ensure-user failed: ' ${err as string}`)
return false
}
return true
}
export {
ensureUser
}

View File

@ -13,6 +13,7 @@ import { parseExternalComponents } from './config/components'
import { getRemoteServerInfosDir } from '../federation/storage' import { getRemoteServerInfosDir } from '../federation/storage'
import { BotConfiguration } from '../configuration/bot' import { BotConfiguration } from '../configuration/bot'
import { debugMucAdmins } from '../debug' import { debugMucAdmins } from '../debug'
import { ExternalAuthOIDC } from '../external-auth/oidc'
async function getWorkingDir (options: RegisterServerOptions): Promise<string> { async function getWorkingDir (options: RegisterServerOptions): Promise<string> {
const peertubeHelpers = options.peertubeHelpers const peertubeHelpers = options.peertubeHelpers
@ -194,6 +195,17 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
const useBots = !settings['disable-channel-configuration'] const useBots = !settings['disable-channel-configuration']
const bots: ProsodyConfig['bots'] = {} const bots: ProsodyConfig['bots'] = {}
let useExternal: boolean = false
try {
const oidc = ExternalAuthOIDC.singleton()
if (await oidc.isOk()) {
useExternal = true
}
} catch (err) {
logger.error(err)
useExternal = false
}
// Note: for the bots to connect, we must allow multiplexing. // Note: for the bots to connect, we must allow multiplexing.
// This will be done on the http (BOSH/Websocket) port, as it only listen on localhost. // This will be done on the http (BOSH/Websocket) port, as it only listen on localhost.
// TODO: to improve performance, try to avoid multiplexing, and find a better way for bots to connect. // TODO: to improve performance, try to avoid multiplexing, and find a better way for bots to connect.
@ -243,6 +255,11 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
if (!disableAnon) { if (!disableAnon) {
config.useAnonymous(autoBanIP) config.useAnonymous(autoBanIP)
} }
if (useExternal) {
config.useExternal(apikey)
}
config.useHttpAuthentication(authApiUrl) config.useHttpAuthentication(authApiUrl)
const useWS = !!options.registerWebSocketRoute // this comes with Peertube >=5.0.0, and is a prerequisite to websocket const useWS = !!options.registerWebSocketRoute // this comes with Peertube >=5.0.0, and is a prerequisite to websocket
config.usePeertubeBoshAndWebsocket(prosodyDomain, port, publicServerUrl, useWS, useMultiplexing) config.usePeertubeBoshAndWebsocket(prosodyDomain, port, publicServerUrl, useWS, useMultiplexing)

View File

@ -140,6 +140,7 @@ class ProsodyConfigContent {
global: ProsodyConfigGlobal global: ProsodyConfigGlobal
authenticated?: ProsodyConfigVirtualHost authenticated?: ProsodyConfigVirtualHost
anon?: ProsodyConfigVirtualHost anon?: ProsodyConfigVirtualHost
external?: ProsodyConfigVirtualHost
muc: ProsodyConfigComponent muc: ProsodyConfigComponent
bot?: ProsodyConfigVirtualHost bot?: ProsodyConfigVirtualHost
externalComponents: ProsodyConfigComponent[] = [] externalComponents: ProsodyConfigComponent[] = []
@ -222,6 +223,19 @@ class ProsodyConfigContent {
} }
} }
/**
* Activates the virtual host for external account authentication (OpenID Connect, ...)
*/
useExternal (apikey: string): void {
this.external = new ProsodyConfigVirtualHost('external.' + this.prosodyDomain)
this.external.set('modules_enabled', [
'ping',
'http',
'http_peertubelivechat_manage_users'
])
this.external.set('peertubelivechat_manage_users_apikey', apikey)
}
useHttpAuthentication (url: string): void { useHttpAuthentication (url: string): void {
this.authenticated = new ProsodyConfigVirtualHost(this.prosodyDomain) this.authenticated = new ProsodyConfigVirtualHost(this.prosodyDomain)
@ -304,6 +318,17 @@ class ProsodyConfigContent {
this.authenticated.set('http_host', prosodyDomain) this.authenticated.set('http_host', prosodyDomain)
this.authenticated.set('http_external_url', 'http://' + prosodyDomain) this.authenticated.set('http_external_url', 'http://' + prosodyDomain)
} }
if (this.external) {
this.external.set('allow_anonymous_s2s', false)
this.external.add('modules_enabled', 'http')
this.external.add('modules_enabled', 'bosh')
if (useWS) {
this.external.add('modules_enabled', 'websocket')
}
this.external.set('http_host', prosodyDomain)
this.external.set('http_external_url', 'http://' + prosodyDomain)
}
} }
useC2S (c2sPort: string): void { useC2S (c2sPort: string): void {
@ -501,6 +526,10 @@ class ProsodyConfigContent {
content += this.bot.write() content += this.bot.write()
content += '\n\n' content += '\n\n'
} }
if (this.external) {
content += this.external.write()
content += '\n\n'
}
content += this.muc.write() content += this.muc.write()
content += '\n\n' content += '\n\n'
for (const externalComponent of this.externalComponents) { for (const externalComponent of this.externalComponents) {

View File

@ -4,6 +4,7 @@ import type { OIDCAuthResult } from '../../../shared/lib/types'
import { asyncMiddleware } from '../middlewares/async' import { asyncMiddleware } from '../middlewares/async'
import { ExternalAuthOIDC } from '../external-auth/oidc' import { ExternalAuthOIDC } from '../external-auth/oidc'
import { ExternalAuthenticationError } from '../external-auth/error' import { ExternalAuthenticationError } from '../external-auth/error'
import { ensureUser } from '../prosody/api/manage-users'
/** /**
* When using a popup for OIDC, writes the HTML/Javascript to close the popup * When using a popup for OIDC, writes the HTML/Javascript to close the popup
@ -65,7 +66,22 @@ async function initOIDCRouter (options: RegisterServerOptions): Promise<Router>
} }
const externalAccountInfos = await oidc.validateAuthenticationProcess(req) const externalAccountInfos = await oidc.validateAuthenticationProcess(req)
logger.info(JSON.stringify(externalAccountInfos)) // FIXME (normalize data type, process, ...) logger.debug(JSON.stringify(
Object.assign(
{},
externalAccountInfos,
{
password: '**removed**' // removing the password from logs!
}
)
))
// Now we create or update the user:
if (!await ensureUser(options, externalAccountInfos)) {
throw new ExternalAuthenticationError(
'Failing to create your account, please try again later or report this issue'
)
}
res.send(popupResultHTML({ res.send(popupResultHTML({
ok: true, ok: true,