diff --git a/prosody-modules/mod_http_peertubelivechat_manage_users/README.md b/prosody-modules/mod_http_peertubelivechat_manage_users/README.md new file mode 100644 index 00000000..da2c039c --- /dev/null +++ b/prosody-modules/mod_http_peertubelivechat_manage_users/README.md @@ -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. diff --git a/prosody-modules/mod_http_peertubelivechat_manage_users/mod_http_peertubelivechat_manage_users.lua b/prosody-modules/mod_http_peertubelivechat_manage_users/mod_http_peertubelivechat_manage_users.lua new file mode 100644 index 00000000..52cf7cbc --- /dev/null +++ b/prosody-modules/mod_http_peertubelivechat_manage_users/mod_http_peertubelivechat_manage_users.lua @@ -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; + }; +}); diff --git a/server/lib/external-auth/oidc.ts b/server/lib/external-auth/oidc.ts index d391a74d..b07dc4f8 100644 --- a/server/lib/external-auth/oidc.ts +++ b/server/lib/external-auth/oidc.ts @@ -457,6 +457,7 @@ class ExternalAuthOIDC { /** * Gets the singleton, or raise an exception if it is too soon. + * @throws Error * @returns the singleton */ public static singleton (): ExternalAuthOIDC { diff --git a/server/lib/prosody/api/manage-users.ts b/server/lib/prosody/api/manage-users.ts new file mode 100644 index 00000000..b62d0daa --- /dev/null +++ b/server/lib/prosody/api/manage-users.ts @@ -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 { + 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 +} diff --git a/server/lib/prosody/config.ts b/server/lib/prosody/config.ts index f26a108e..74fbb87b 100644 --- a/server/lib/prosody/config.ts +++ b/server/lib/prosody/config.ts @@ -13,6 +13,7 @@ import { parseExternalComponents } from './config/components' import { getRemoteServerInfosDir } from '../federation/storage' import { BotConfiguration } from '../configuration/bot' import { debugMucAdmins } from '../debug' +import { ExternalAuthOIDC } from '../external-auth/oidc' async function getWorkingDir (options: RegisterServerOptions): Promise { const peertubeHelpers = options.peertubeHelpers @@ -194,6 +195,17 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise=5.0.0, and is a prerequisite to websocket config.usePeertubeBoshAndWebsocket(prosodyDomain, port, publicServerUrl, useWS, useMultiplexing) diff --git a/server/lib/prosody/config/content.ts b/server/lib/prosody/config/content.ts index 4c847bab..2dd71342 100644 --- a/server/lib/prosody/config/content.ts +++ b/server/lib/prosody/config/content.ts @@ -140,6 +140,7 @@ class ProsodyConfigContent { global: ProsodyConfigGlobal authenticated?: ProsodyConfigVirtualHost anon?: ProsodyConfigVirtualHost + external?: ProsodyConfigVirtualHost muc: ProsodyConfigComponent bot?: ProsodyConfigVirtualHost 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 { this.authenticated = new ProsodyConfigVirtualHost(this.prosodyDomain) @@ -304,6 +318,17 @@ class ProsodyConfigContent { this.authenticated.set('http_host', 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 { @@ -501,6 +526,10 @@ class ProsodyConfigContent { content += this.bot.write() content += '\n\n' } + if (this.external) { + content += this.external.write() + content += '\n\n' + } content += this.muc.write() content += '\n\n' for (const externalComponent of this.externalComponents) { diff --git a/server/lib/routers/oidc.ts b/server/lib/routers/oidc.ts index f5a8878e..e23d15f4 100644 --- a/server/lib/routers/oidc.ts +++ b/server/lib/routers/oidc.ts @@ -4,6 +4,7 @@ import type { OIDCAuthResult } from '../../../shared/lib/types' import { asyncMiddleware } from '../middlewares/async' import { ExternalAuthOIDC } from '../external-auth/oidc' 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 @@ -65,7 +66,22 @@ async function initOIDCRouter (options: RegisterServerOptions): Promise } 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({ ok: true,