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.
|
||||
* @throws Error
|
||||
* @returns the singleton
|
||||
*/
|
||||
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 { BotConfiguration } from '../configuration/bot'
|
||||
import { debugMucAdmins } from '../debug'
|
||||
import { ExternalAuthOIDC } from '../external-auth/oidc'
|
||||
|
||||
async function getWorkingDir (options: RegisterServerOptions): Promise<string> {
|
||||
const peertubeHelpers = options.peertubeHelpers
|
||||
@ -194,6 +195,17 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
|
||||
const useBots = !settings['disable-channel-configuration']
|
||||
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.
|
||||
// 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.
|
||||
@ -243,6 +255,11 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
|
||||
if (!disableAnon) {
|
||||
config.useAnonymous(autoBanIP)
|
||||
}
|
||||
|
||||
if (useExternal) {
|
||||
config.useExternal(apikey)
|
||||
}
|
||||
|
||||
config.useHttpAuthentication(authApiUrl)
|
||||
const useWS = !!options.registerWebSocketRoute // this comes with Peertube >=5.0.0, and is a prerequisite to websocket
|
||||
config.usePeertubeBoshAndWebsocket(prosodyDomain, port, publicServerUrl, useWS, useMultiplexing)
|
||||
|
@ -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) {
|
||||
|
@ -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<Router>
|
||||
}
|
||||
|
||||
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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user