From 76adc7124fbb5c675c094812bca35b3df166f776 Mon Sep 17 00:00:00 2001 From: John Livingston Date: Tue, 4 May 2021 13:00:44 +0200 Subject: [PATCH] Prosody auth, first working code: * generated password on an api call * use this password to authenticate on prosody * using helper getAuthUser when available, else fallback to custom code --- conversejs/builtin.ts | 91 ++++++++++++++++++++++++---------- conversejs/index.html | 2 +- server/@types/peertube.d.ts | 19 +++++++ server/lib/helpers.ts | 30 ++++++++--- server/lib/prosody/auth.ts | 66 ++++++++++++++++++++++++ server/lib/routers/api.ts | 26 +++++++++- server/lib/routers/settings.ts | 2 +- server/lib/routers/webchat.ts | 6 ++- 8 files changed, 204 insertions(+), 38 deletions(-) create mode 100644 server/lib/prosody/auth.ts diff --git a/conversejs/builtin.ts b/conversejs/builtin.ts index 1e9e9b03..6da5cef3 100644 --- a/conversejs/builtin.ts +++ b/conversejs/builtin.ts @@ -14,24 +14,59 @@ function inIframe (): boolean { } } -function authenticatedMode (): boolean { - if (!window.fetch) { - console.error('Your browser has not the fetch api, we cant log you in') +interface AuthentInfos { + jid: string + password: string +} +async function authenticatedMode (authenticationUrl: string): Promise { + try { + if (!window.fetch) { + console.error('Your browser has not the fetch api, we cant log you in') + return false + } + if (!window.localStorage) { + // FIXME: is the Peertube token always in localStorage? + console.error('Your browser has no localStorage, we cant log you in') + return false + } + const tokenType = window.localStorage.getItem('token_type') ?? '' + const accessToken = window.localStorage.getItem('access_token') ?? '' + const refreshToken = window.localStorage.getItem('refresh_token') ?? '' + if (tokenType === '' && accessToken === '' && refreshToken === '') { + console.info('User seems not to be logged in.') + return false + } + + const response = await window.fetch(authenticationUrl, { + method: 'GET', + headers: new Headers({ + Authorization: tokenType + ' ' + accessToken, + 'content-type': 'application/json;charset=UTF-8' + }) + }) + + if (!response.ok) { + console.error('Failed fetching user informations') + return false + } + const data = await response.json() + if ((typeof data) !== 'object') { + console.error('Failed reading user informations') + return false + } + + if (!data.jid || !data.password) { + console.error('User informations does not contain required fields') + return false + } + return { + jid: data.jid, + password: data.password + } + } catch (error) { + console.error(error) return false } - if (!window.localStorage) { - // FIXME: is the Peertube token always in localStorage? - console.error('Your browser has no localStorage, we cant log you in') - return false - } - const tokenType = window.localStorage.getItem('token_type') ?? '' - const accessToken = window.localStorage.getItem('access_token') ?? '' - const refreshToken = window.localStorage.getItem('refresh_token') ?? '' - if (tokenType === '' && accessToken === '' && refreshToken === '') { - console.info('User seems not to be logged in.') - return false - } - return true } interface InitConverseParams { @@ -40,15 +75,15 @@ interface InitConverseParams { room: string boshServiceUrl: string websocketServiceUrl: string - tryAuthenticatedMode: string + authenticationUrl: string } -window.initConverse = function initConverse ({ +window.initConverse = async function initConverse ({ jid, assetsPath, room, boshServiceUrl, websocketServiceUrl, - tryAuthenticatedMode + authenticationUrl }: InitConverseParams) { const params: any = { assets_path: assetsPath, @@ -89,13 +124,17 @@ window.initConverse = function initConverse ({ allow_message_retraction: 'all' } - if (tryAuthenticatedMode === 'true' && authenticatedMode()) { - params.authentication = 'login' - params.auto_login = true - params.auto_reconnect = true - params.jid = 'john@localhost' - params.password = 'password' - // FIXME: use params.oauth_providers? + if (authenticationUrl !== '') { + const auth = await authenticatedMode(authenticationUrl) + if (auth) { + params.authentication = 'login' + params.auto_login = true + params.auto_reconnect = true + params.jid = auth.jid + params.password = auth.password + params.muc_nickname_from_jid = true + // FIXME: use params.oauth_providers? + } } window.converse.initialize(params) diff --git a/conversejs/index.html b/conversejs/index.html index fa6ecb2d..509b3e4b 100644 --- a/conversejs/index.html +++ b/conversejs/index.html @@ -24,7 +24,7 @@ room: '{{ROOM}}', boshServiceUrl: '{{BOSH_SERVICE_URL}}', websocketServiceUrl: '{{WS_SERVICE_URL}}', - tryAuthenticatedMode: '{{TRY_AUTHENTICATED_MODE}}' + authenticationUrl: '{{AUTHENTICATION_URL}}' }) diff --git a/server/@types/peertube.d.ts b/server/@types/peertube.d.ts index 1fd5d6ae..66fb7608 100644 --- a/server/@types/peertube.d.ts +++ b/server/@types/peertube.d.ts @@ -77,6 +77,21 @@ interface MVideoThumbnail { // FIXME: this interface is not complete. state: VideoState } +// Keep the order +enum UserRole { + ADMINISTRATOR = 0, + MODERATOR = 1, + USER = 2 +} + +interface MUserAccountUrl { // FIXME: this interface is not complete + id?: string + username: string + email: string + blocked: boolean + role: UserRole +} + interface VideoBlacklistCreate { reason?: string unfederate?: boolean @@ -111,6 +126,10 @@ interface PeerTubeHelpers { server: { getServerActor: () => Promise } + // Added in Peertube 3.2.0 + user?: { + getAuthUser: (res: express.Response) => MUserAccountUrl | undefined + } } interface RegisterServerOptions { diff --git a/server/lib/helpers.ts b/server/lib/helpers.ts index e4f4947a..52637edd 100644 --- a/server/lib/helpers.ts +++ b/server/lib/helpers.ts @@ -21,22 +21,38 @@ function getBaseStaticRoute (): string { return '/plugins/' + pluginShortName + '/' + version + '/static/' } -// FIXME: Peertube <= 3.1.0 has no way to test that current user is admin -// This is a hack. -function isUserAdmin (res: Response): boolean { - if (!res.locals?.authenticated) { +// Peertube <= 3.1.0 has no way to test that current user is admin +// Peertube >= 3.2.0 has getAuthUser helper +function isUserAdmin (options: RegisterServerOptions, res: Response): boolean { + const user = getAuthUser(options, res) + if (!user) { return false } - if (res.locals?.oauth?.token?.User?.role === 0) { - return true + if (user.blocked) { + return false } - return false + if (user.role !== UserRole.ADMINISTRATOR) { + return false + } + return true +} + +// Peertube <= 3.1.0 has no way to get user informations. +// This is a hack. +// Peertube >= 3.2.0 has getAuthUser helper +function getAuthUser ({ peertubeHelpers }: RegisterServerOptions, res: Response): MUserAccountUrl | undefined { + if (peertubeHelpers.user?.getAuthUser) { + return peertubeHelpers.user.getAuthUser(res) + } + peertubeHelpers.logger.debug('Peertube does not provide getAuthUser for now, fallback on hack') + return res.locals.oauth?.token?.User } export { getBaseRouter, getBaseStaticRoute, isUserAdmin, + getAuthUser, pluginName, pluginShortName } diff --git a/server/lib/prosody/auth.ts b/server/lib/prosody/auth.ts new file mode 100644 index 00000000..cac22ea7 --- /dev/null +++ b/server/lib/prosody/auth.ts @@ -0,0 +1,66 @@ +/* +This module provides user credential for the builtin prosody module. + +A user can get a password thanks to a call to prosodyRegisterUser (see api user/auth). + +Then, we can test that the user exists with prosodyUserRegistered, and test password with prosodyCheckUserPassword. + +Passwords are randomly generated. + +These password are stored internally in a global variable, and are valid for 24h. +Each call to registerUser extends the validity by 24h. + +*/ + +interface Password { + password: string + validity: number +} + +const PASSWORDS: Map = new Map() + +function _getAndClean (user: string): Password | undefined { + const entry = PASSWORDS.get(user) + if (entry) { + if (entry.validity > Date.now()) { + return entry + } + PASSWORDS.delete(user) + } + return undefined +} + +async function prosodyRegisterUser (user: string): Promise { + const entry = _getAndClean(user) + const validity = Date.now() + (24 * 60 * 60 * 1000) // 24h + if (entry) { + entry.validity = validity + return entry.password + } + + const password = Math.random().toString(36).slice(2, 12) + Math.random().toString(36).slice(2, 12) + PASSWORDS.set(user, { + password: password, + validity: validity + }) + return password +} + +async function prosodyUserRegistered (user: string): Promise { + const entry = _getAndClean(user) + return !!entry +} + +async function prosodyCheckUserPassword (user: string, password: string): Promise { + const entry = _getAndClean(user) + if (entry && entry.password === password) { + return true + } + return false +} + +export { + prosodyRegisterUser, + prosodyUserRegistered, + prosodyCheckUserPassword +} diff --git a/server/lib/routers/api.ts b/server/lib/routers/api.ts index fec04b68..bf751348 100644 --- a/server/lib/routers/api.ts +++ b/server/lib/routers/api.ts @@ -1,6 +1,8 @@ import type { Router, Request, Response, NextFunction } from 'express' import { videoHasWebchat } from '../../../shared/lib/video' import { asyncMiddleware } from '../middlewares/async' +import { prosodyCheckUserPassword, prosodyRegisterUser, prosodyUserRegistered } from '../prosody/auth' +import { getAuthUser } from '../helpers' // See here for description: https://modules.prosody.im/mod_muc_http_defaults.html interface RoomDefaults { @@ -79,6 +81,25 @@ async function initApiRouter (options: RegisterServerOptions): Promise { } )) + router.get('/auth', asyncMiddleware( + async (req: Request, res: Response, _next: NextFunction) => { + const user = getAuthUser(options, res) + if (!user) { + res.sendStatus(403) + return + } + if (user.blocked) { + res.sendStatus(403) + return + } + const password: string = await prosodyRegisterUser(user.username) + res.status(200).json({ + jid: user.username + '@localhost', + password: password + }) + } + )) + router.post('/user/register', asyncMiddleware( async (req: Request, res: Response, _next: NextFunction) => { res.sendStatus(501) @@ -107,7 +128,7 @@ async function initApiRouter (options: RegisterServerOptions): Promise { res.status(200).send('false') return } - if (user === 'john' && pass === 'password') { + if (user && pass && await prosodyCheckUserPassword(user as string, pass as string)) { res.status(200).send('true') return } @@ -136,8 +157,9 @@ async function initApiRouter (options: RegisterServerOptions): Promise { res.status(200).send('false') return } - if (user === 'john') { + if (user && await prosodyUserRegistered(user as string)) { res.status(200).send('true') + return } res.status(200).send('false') } diff --git a/server/lib/routers/settings.ts b/server/lib/routers/settings.ts index e0bb1efb..c5b5a903 100644 --- a/server/lib/routers/settings.ts +++ b/server/lib/routers/settings.ts @@ -24,7 +24,7 @@ async function initSettingsRouter (options: RegisterServerOptions): Promise