Possibility to configure an OpenID Connect provider on the instance level WIP (#128).
This commit is contained in:
parent
8574ab581d
commit
3a5f27e751
@ -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.
|
@ -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;
|
||||||
|
};
|
||||||
|
});
|
@ -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 {
|
||||||
|
66
server/lib/prosody/api/manage-users.ts
Normal file
66
server/lib/prosody/api/manage-users.ts
Normal 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
|
||||||
|
}
|
@ -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)
|
||||||
|
@ -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) {
|
||||||
|
@ -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,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user