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:
		| @ -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) | ||||||
| } | } | ||||||
|  | |||||||
| @ -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> | ||||||
|  | |||||||
							
								
								
									
										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 |   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 { | ||||||
|  | |||||||
| @ -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 | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										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 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') | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -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 | ||||||
|       } |       } | ||||||
|  | |||||||
| @ -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') | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user