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
This commit is contained in:
John Livingston 2021-05-04 13:00:44 +02:00
parent fb7e98d20e
commit 76adc7124f
8 changed files with 204 additions and 38 deletions

View File

@ -14,7 +14,12 @@ function inIframe (): boolean {
} }
} }
function authenticatedMode (): boolean { interface AuthentInfos {
jid: string
password: string
}
async function authenticatedMode (authenticationUrl: string): Promise<false | AuthentInfos> {
try {
if (!window.fetch) { if (!window.fetch) {
console.error('Your browser has not the fetch api, we cant log you in') console.error('Your browser has not the fetch api, we cant log you in')
return false return false
@ -31,7 +36,37 @@ function authenticatedMode (): boolean {
console.info('User seems not to be logged in.') console.info('User seems not to be logged in.')
return false return false
} }
return true
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
}
} }
interface InitConverseParams { interface InitConverseParams {
@ -40,15 +75,15 @@ interface InitConverseParams {
room: string room: string
boshServiceUrl: string boshServiceUrl: string
websocketServiceUrl: string websocketServiceUrl: string
tryAuthenticatedMode: string authenticationUrl: string
} }
window.initConverse = function initConverse ({ window.initConverse = async function initConverse ({
jid, jid,
assetsPath, assetsPath,
room, room,
boshServiceUrl, boshServiceUrl,
websocketServiceUrl, websocketServiceUrl,
tryAuthenticatedMode authenticationUrl
}: InitConverseParams) { }: InitConverseParams) {
const params: any = { const params: any = {
assets_path: assetsPath, assets_path: assetsPath,
@ -89,14 +124,18 @@ window.initConverse = function initConverse ({
allow_message_retraction: 'all' allow_message_retraction: 'all'
} }
if (tryAuthenticatedMode === 'true' && authenticatedMode()) { if (authenticationUrl !== '') {
const auth = await authenticatedMode(authenticationUrl)
if (auth) {
params.authentication = 'login' params.authentication = 'login'
params.auto_login = true params.auto_login = true
params.auto_reconnect = true params.auto_reconnect = true
params.jid = 'john@localhost' params.jid = auth.jid
params.password = 'password' params.password = auth.password
params.muc_nickname_from_jid = true
// FIXME: use params.oauth_providers? // FIXME: use params.oauth_providers?
} }
}
window.converse.initialize(params) window.converse.initialize(params)
} }

View File

@ -24,7 +24,7 @@
room: '{{ROOM}}', room: '{{ROOM}}',
boshServiceUrl: '{{BOSH_SERVICE_URL}}', boshServiceUrl: '{{BOSH_SERVICE_URL}}',
websocketServiceUrl: '{{WS_SERVICE_URL}}', websocketServiceUrl: '{{WS_SERVICE_URL}}',
tryAuthenticatedMode: '{{TRY_AUTHENTICATED_MODE}}' authenticationUrl: '{{AUTHENTICATION_URL}}'
}) })
</script> </script>
</body> </body>

View File

@ -77,6 +77,21 @@ interface MVideoThumbnail { // FIXME: this interface is not complete.
state: VideoState 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 { interface VideoBlacklistCreate {
reason?: string reason?: string
unfederate?: boolean unfederate?: boolean
@ -111,6 +126,10 @@ interface PeerTubeHelpers {
server: { server: {
getServerActor: () => Promise<ActorModel> getServerActor: () => Promise<ActorModel>
} }
// Added in Peertube 3.2.0
user?: {
getAuthUser: (res: express.Response) => MUserAccountUrl | undefined
}
} }
interface RegisterServerOptions { interface RegisterServerOptions {

View File

@ -21,22 +21,38 @@ function getBaseStaticRoute (): string {
return '/plugins/' + pluginShortName + '/' + version + '/static/' return '/plugins/' + pluginShortName + '/' + version + '/static/'
} }
// FIXME: Peertube <= 3.1.0 has no way to test that current user is admin // Peertube <= 3.1.0 has no way to test that current user is admin
// This is a hack. // Peertube >= 3.2.0 has getAuthUser helper
function isUserAdmin (res: Response): boolean { function isUserAdmin (options: RegisterServerOptions, res: Response): boolean {
if (!res.locals?.authenticated) { const user = getAuthUser(options, res)
if (!user) {
return false
}
if (user.blocked) {
return false
}
if (user.role !== UserRole.ADMINISTRATOR) {
return false return false
} }
if (res.locals?.oauth?.token?.User?.role === 0) {
return true 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)
} }
return false peertubeHelpers.logger.debug('Peertube does not provide getAuthUser for now, fallback on hack')
return res.locals.oauth?.token?.User
} }
export { export {
getBaseRouter, getBaseRouter,
getBaseStaticRoute, getBaseStaticRoute,
isUserAdmin, isUserAdmin,
getAuthUser,
pluginName, pluginName,
pluginShortName pluginShortName
} }

View File

@ -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<string, Password> = 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<string> {
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<boolean> {
const entry = _getAndClean(user)
return !!entry
}
async function prosodyCheckUserPassword (user: string, password: string): Promise<boolean> {
const entry = _getAndClean(user)
if (entry && entry.password === password) {
return true
}
return false
}
export {
prosodyRegisterUser,
prosodyUserRegistered,
prosodyCheckUserPassword
}

View File

@ -1,6 +1,8 @@
import type { Router, Request, Response, NextFunction } from 'express' import type { Router, Request, Response, NextFunction } from 'express'
import { videoHasWebchat } from '../../../shared/lib/video' import { videoHasWebchat } from '../../../shared/lib/video'
import { asyncMiddleware } from '../middlewares/async' 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 // See here for description: https://modules.prosody.im/mod_muc_http_defaults.html
interface RoomDefaults { interface RoomDefaults {
@ -79,6 +81,25 @@ async function initApiRouter (options: RegisterServerOptions): Promise<Router> {
} }
)) ))
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( router.post('/user/register', asyncMiddleware(
async (req: Request, res: Response, _next: NextFunction) => { async (req: Request, res: Response, _next: NextFunction) => {
res.sendStatus(501) res.sendStatus(501)
@ -107,7 +128,7 @@ async function initApiRouter (options: RegisterServerOptions): Promise<Router> {
res.status(200).send('false') res.status(200).send('false')
return return
} }
if (user === 'john' && pass === 'password') { if (user && pass && await prosodyCheckUserPassword(user as string, pass as string)) {
res.status(200).send('true') res.status(200).send('true')
return return
} }
@ -136,8 +157,9 @@ async function initApiRouter (options: RegisterServerOptions): Promise<Router> {
res.status(200).send('false') res.status(200).send('false')
return return
} }
if (user === 'john') { if (user && await prosodyUserRegistered(user as string)) {
res.status(200).send('true') res.status(200).send('true')
return
} }
res.status(200).send('false') res.status(200).send('false')
} }

View File

@ -24,7 +24,7 @@ async function initSettingsRouter (options: RegisterServerOptions): Promise<Rout
res.sendStatus(403) res.sendStatus(403)
return return
} }
if (!isUserAdmin(res)) { if (!isUserAdmin(options, res)) {
res.sendStatus(403) res.sendStatus(403)
return return
} }

View File

@ -32,11 +32,15 @@ async function initWebchatRouter (options: RegisterServerOptions): Promise<Route
let room: string let room: string
let boshUri: string let boshUri: string
let wsUri: string let wsUri: string
let authenticationUrl: string = ''
if (settings['chat-use-prosody']) { if (settings['chat-use-prosody']) {
server = 'anon.localhost' server = 'anon.localhost'
room = '{{VIDEO_UUID}}@room.localhost' room = '{{VIDEO_UUID}}@room.localhost'
boshUri = getBaseRouter() + 'webchat/http-bind' boshUri = getBaseRouter() + 'webchat/http-bind'
wsUri = '' wsUri = ''
authenticationUrl = options.peertubeHelpers.config.getWebserverUrl() +
getBaseRouter() +
'api/auth'
} else if (settings['chat-use-builtin']) { } else if (settings['chat-use-builtin']) {
if (!settings['chat-server']) { if (!settings['chat-server']) {
throw new Error('Missing chat-server settings.') throw new Error('Missing chat-server settings.')
@ -70,7 +74,7 @@ async function initWebchatRouter (options: RegisterServerOptions): Promise<Route
page = page.replace(/{{ROOM}}/g, room) page = page.replace(/{{ROOM}}/g, room)
page = page.replace(/{{BOSH_SERVICE_URL}}/g, boshUri) page = page.replace(/{{BOSH_SERVICE_URL}}/g, boshUri)
page = page.replace(/{{WS_SERVICE_URL}}/g, wsUri) page = page.replace(/{{WS_SERVICE_URL}}/g, wsUri)
page = page.replace(/{{TRY_AUTHENTICATED_MODE}}/g, settings['chat-use-prosody'] ? 'true' : 'false') page = page.replace(/{{AUTHENTICATION_URL}}/g, authenticationUrl)
res.status(200) res.status(200)
res.type('html') res.type('html')