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:
parent
fb7e98d20e
commit
76adc7124f
@ -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<false | AuthentInfos> {
|
||||
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)
|
||||
|
@ -24,7 +24,7 @@
|
||||
room: '{{ROOM}}',
|
||||
boshServiceUrl: '{{BOSH_SERVICE_URL}}',
|
||||
websocketServiceUrl: '{{WS_SERVICE_URL}}',
|
||||
tryAuthenticatedMode: '{{TRY_AUTHENTICATED_MODE}}'
|
||||
authenticationUrl: '{{AUTHENTICATION_URL}}'
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
|
19
server/@types/peertube.d.ts
vendored
19
server/@types/peertube.d.ts
vendored
@ -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<ActorModel>
|
||||
}
|
||||
// Added in Peertube 3.2.0
|
||||
user?: {
|
||||
getAuthUser: (res: express.Response) => MUserAccountUrl | undefined
|
||||
}
|
||||
}
|
||||
|
||||
interface RegisterServerOptions {
|
||||
|
@ -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
|
||||
}
|
||||
|
66
server/lib/prosody/auth.ts
Normal file
66
server/lib/prosody/auth.ts
Normal 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
|
||||
}
|
@ -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> {
|
||||
}
|
||||
))
|
||||
|
||||
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<Router> {
|
||||
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<Router> {
|
||||
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')
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ async function initSettingsRouter (options: RegisterServerOptions): Promise<Rout
|
||||
res.sendStatus(403)
|
||||
return
|
||||
}
|
||||
if (!isUserAdmin(res)) {
|
||||
if (!isUserAdmin(options, res)) {
|
||||
res.sendStatus(403)
|
||||
return
|
||||
}
|
||||
|
@ -32,11 +32,15 @@ async function initWebchatRouter (options: RegisterServerOptions): Promise<Route
|
||||
let room: string
|
||||
let boshUri: string
|
||||
let wsUri: string
|
||||
let authenticationUrl: string = ''
|
||||
if (settings['chat-use-prosody']) {
|
||||
server = 'anon.localhost'
|
||||
room = '{{VIDEO_UUID}}@room.localhost'
|
||||
boshUri = getBaseRouter() + 'webchat/http-bind'
|
||||
wsUri = ''
|
||||
authenticationUrl = options.peertubeHelpers.config.getWebserverUrl() +
|
||||
getBaseRouter() +
|
||||
'api/auth'
|
||||
} else if (settings['chat-use-builtin']) {
|
||||
if (!settings['chat-server']) {
|
||||
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(/{{BOSH_SERVICE_URL}}/g, boshUri)
|
||||
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.type('html')
|
||||
|
Loading…
x
Reference in New Issue
Block a user