Authentication token generation (#98):

* setting to disable the feature (aka "panic button")
This commit is contained in:
John Livingston 2024-06-17 15:21:22 +02:00
parent a9b6474b8f
commit 257fdbd2c2
No known key found for this signature in database
GPG Key ID: B17B5640CE66CDBC
7 changed files with 111 additions and 30 deletions

View File

@ -18,6 +18,7 @@ export type LiveChatSettings = SettingEntries & {
'prosody-room-allow-s2s': boolean 'prosody-room-allow-s2s': boolean
'converse-theme': ConverseJSTheme 'converse-theme': ConverseJSTheme
'prosody-room-type': string 'prosody-room-type': string
'livechat-token-disabled': boolean
} }
export class PtContext { export class PtContext {

View File

@ -111,7 +111,12 @@ export class ShareChatElement extends LivechatElement {
// Note: for dockEnabled, we check: // Note: for dockEnabled, we check:
// * that the user is logged in // * that the user is logged in
// * that the video is local (for remote video, tests case are too complicated, and it's not the main use case, so…) // * that the video is local (for remote video, tests case are too complicated, and it's not the main use case, so…)
this.dockEnabled = !isAnonymousUser(this.ptContext.ptOptions) && this._video.isLocal // * settings is not disabled
this.dockEnabled = (
!isAnonymousUser(this.ptContext.ptOptions) &&
this._video.isLocal &&
!settings['livechat-token-disabled']
)
this.autocolorsAvailable = isAutoColorsAvailable(settings['converse-theme']) this.autocolorsAvailable = isAutoColorsAvailable(settings['converse-theme'])
this._restorePreviousState() this._restorePreviousState()

View File

@ -536,3 +536,11 @@ token_action_create: Create a new token
token_action_revoke: Revoke the token token_action_revoke: Revoke the token
token_default_label: Token generated from the web interface token_default_label: Token generated from the web interface
token_action_revoke_confirm: Are you sure you want to revoke this token? token_action_revoke_confirm: Are you sure you want to revoke this token?
auth_description: |
<h3>Authentication</h3>
livechat_token_disabled_label: 'Disable livechat tokens'
livechat_token_disabled_description: |
Users can generate long term tokens to connect to the chat.
These tokens can for example be used to include the chat in OBS web docks.
Check <a href="https://johnxlivingston.github.io/peertube-plugin-livechat/documentation/user/obs" target="_blank">the documentation</a> for more information.
You can disable this feature by checking this setting.

View File

@ -63,6 +63,7 @@ let singleton: LivechatProsodyAuth | undefined
export class LivechatProsodyAuth { export class LivechatProsodyAuth {
private readonly _options: RegisterServerOptions private readonly _options: RegisterServerOptions
private readonly _prosodyDomain: string private readonly _prosodyDomain: string
private _userTokensEnabled: boolean
private readonly _tokensPath: string private readonly _tokensPath: string
private readonly _passwords: Map<string, Password> = new Map() private readonly _passwords: Map<string, Password> = new Map()
private readonly _tokensInfoByJID: Map<string, LivechatTokenInfos | undefined> = new Map() private readonly _tokensInfoByJID: Map<string, LivechatTokenInfos | undefined> = new Map()
@ -80,9 +81,10 @@ export class LivechatProsodyAuth {
outputEncoding: 'hex' as Encoding outputEncoding: 'hex' as Encoding
} }
constructor (options: RegisterServerOptions, prosodyDomain: string, secretKey: string) { constructor (options: RegisterServerOptions, prosodyDomain: string, userTokensEnabled: boolean, secretKey: string) {
this._options = options this._options = options
this._prosodyDomain = prosodyDomain this._prosodyDomain = prosodyDomain
this._userTokensEnabled = userTokensEnabled
this._secretKey = secretKey this._secretKey = secretKey
this._tokensPath = path.join( this._tokensPath = path.join(
options.peertubeHelpers.plugin.getDataDirectoryPath(), options.peertubeHelpers.plugin.getDataDirectoryPath(),
@ -131,18 +133,20 @@ export class LivechatProsodyAuth {
if (entry) { if (entry) {
return true return true
} }
try { if (this._userTokensEnabled) {
const tokensInfo = await this._getTokensInfoForJID(normalizedUsername + '@' + this._prosodyDomain) try {
if (!tokensInfo || !tokensInfo.tokens.length) { const tokensInfo = await this._getTokensInfoForJID(normalizedUsername + '@' + this._prosodyDomain)
if (!tokensInfo || !tokensInfo.tokens.length) {
return false
}
// Checking that the user is valid:
if (await this._userIdValid(tokensInfo.userId)) {
return true
}
} catch (err) {
this._logger.error(err as string)
return false return false
} }
// Checking that the user is valid:
if (await this._userIdValid(tokensInfo.userId)) {
return true
}
} catch (err) {
this._logger.error(err as string)
return false
} }
return false return false
} }
@ -152,23 +156,25 @@ export class LivechatProsodyAuth {
if (entry && entry.password === password) { if (entry && entry.password === password) {
return true return true
} }
try { if (this._userTokensEnabled) {
const tokensInfo = await this._getTokensInfoForJID(normalizedUsername + '@' + this._prosodyDomain) try {
if (!tokensInfo || !tokensInfo.tokens.length) { const tokensInfo = await this._getTokensInfoForJID(normalizedUsername + '@' + this._prosodyDomain)
return false if (!tokensInfo || !tokensInfo.tokens.length) {
} return false
// Checking that the user is valid: }
if (!await this._userIdValid(tokensInfo.userId)) { // Checking that the user is valid:
return false if (!await this._userIdValid(tokensInfo.userId)) {
} return false
}
// Is the password in tokens? // Is the password in tokens?
if (tokensInfo.tokens.find((t) => t.password === password)) { if (tokensInfo.tokens.find((t) => t.password === password)) {
return true return true
}
} catch (err) {
this._logger.error(err as string)
return false
} }
} catch (err) {
this._logger.error(err as string)
return false
} }
return false return false
} }
@ -179,6 +185,9 @@ export class LivechatProsodyAuth {
* @param user the user * @param user the user
*/ */
public async getUserTokens (user: MUserDefault): Promise<LivechatToken[] | undefined> { public async getUserTokens (user: MUserDefault): Promise<LivechatToken[] | undefined> {
if (!this._userTokensEnabled) {
return undefined
}
if (!user || !user.id) { if (!user || !user.id) {
return undefined return undefined
} }
@ -210,7 +219,22 @@ export class LivechatProsodyAuth {
return tokens return tokens
} }
/**
* Enable or disable user tokens. Must be called when the settings change.
* @param enabled
*/
public setUserTokensEnabled (enabled: boolean): void {
this._userTokensEnabled = !!enabled
if (!this.userRegistered) {
// Empty the cache:
this._tokensInfoByJID.clear()
}
}
public async createUserToken (user: MUserDefault, label: string): Promise<LivechatToken | undefined> { public async createUserToken (user: MUserDefault, label: string): Promise<LivechatToken | undefined> {
if (!this._userTokensEnabled) {
return undefined
}
if (!user || !user.id) { if (!user || !user.id) {
return undefined return undefined
} }
@ -230,6 +254,9 @@ export class LivechatProsodyAuth {
} }
public async revokeUserToken (user: MUserDefault, id: number): Promise<boolean> { public async revokeUserToken (user: MUserDefault, id: number): Promise<boolean> {
if (!this._userTokensEnabled) {
return false
}
if (!user || !user.id) { if (!user || !user.id) {
return false return false
} }
@ -448,12 +475,13 @@ export class LivechatProsodyAuth {
await options.storageManager.storeData('livechat-prosody-auth-secretkey', secretKey) await options.storageManager.storeData('livechat-prosody-auth-secretkey', secretKey)
} }
singleton = new LivechatProsodyAuth(options, prosodyDomain, secretKey) const userTokenDisabled = await options.settingsManager.getSetting('livechat-token-disabled')
singleton = new LivechatProsodyAuth(options, prosodyDomain, !userTokenDisabled, secretKey)
return singleton return singleton
} }
public static async destroySingleton (): Promise<void> { public static async destroySingleton (): Promise<void> {
// TODO: sync to disk
singleton = undefined singleton = undefined
} }
} }

View File

@ -9,6 +9,7 @@ import { RoomChannel } from './room-channel'
import { BotsCtl } from './bots/ctl' import { BotsCtl } from './bots/ctl'
import { ExternalAuthOIDC, ExternalAuthOIDCType } from './external-auth/oidc' import { ExternalAuthOIDC, ExternalAuthOIDCType } from './external-auth/oidc'
import { Emojis } from './emojis' import { Emojis } from './emojis'
import { LivechatProsodyAuth } from './prosody/auth'
import { loc } from './loc' import { loc } from './loc'
const escapeHTML = require('escape-html') const escapeHTML = require('escape-html')
@ -21,6 +22,7 @@ async function initSettings (options: RegisterServerOptions): Promise<void> {
initImportantNotesSettings(options) initImportantNotesSettings(options)
initChatSettings(options) initChatSettings(options)
initFederationSettings(options) initFederationSettings(options)
initAuth(options)
initExternalAuth(options) initExternalAuth(options)
initAdvancedChannelCustomizationSettings(options) initAdvancedChannelCustomizationSettings(options)
initChatBehaviourSettings(options) initChatBehaviourSettings(options)
@ -73,6 +75,8 @@ async function initSettings (options: RegisterServerOptions): Promise<void> {
await Emojis.destroySingleton() await Emojis.destroySingleton()
await Emojis.initSingleton(options) await Emojis.initSingleton(options)
LivechatProsodyAuth.singleton().setUserTokensEnabled(!settings['livechat-token-disabled'])
peertubeHelpers.logger.info('Saving settings, ensuring prosody is running') peertubeHelpers.logger.info('Saving settings, ensuring prosody is running')
await ensureProsodyRunning(options) await ensureProsodyRunning(options)
@ -181,6 +185,35 @@ function initFederationSettings ({ registerSetting }: RegisterServerOptions): vo
}) })
} }
/**
* Initialize settings related to authentication.
* @param options peertube server options
*/
function initAuth (options: RegisterServerOptions): void {
const registerSetting = options.registerSetting
registerSetting({
type: 'html',
private: true,
descriptionHTML: loc('auth_description')
})
registerSetting({
type: 'html',
private: true,
descriptionHTML: loc('experimental_warning')
})
registerSetting({
name: 'livechat-token-disabled',
label: loc('livechat_token_disabled_label'),
descriptionHTML: loc('livechat_token_disabled_description'),
type: 'input-checkbox',
default: false,
private: false
})
}
/** /**
* Registers settings related to the "External Authentication" section. * Registers settings related to the "External Authentication" section.
* @param param0 server options * @param param0 server options

View File

@ -52,7 +52,7 @@ async function register (options: RegisterServerOptions): Promise<any> {
await initSettings(options) await initSettings(options)
await Emojis.initSingleton(options) // after settings, before routes await Emojis.initSingleton(options) // after settings, before routes
await LivechatProsodyAuth.initSingleton(options) await LivechatProsodyAuth.initSingleton(options) // after settings, before routes
await initCustomFields(options) await initCustomFields(options)
await initRouters(options) await initRouters(options)

View File

@ -24,6 +24,12 @@ Following settings concern the federation with other Peertube instances, and oth
{{% livechat_label federation_dont_publish_remotely_description %}} {{% livechat_label federation_dont_publish_remotely_description %}}
## Authentication
### {{% livechat_label livechat_token_disabled_label %}}
In case you have any trouble with the long term authentication tokens, you can disable the feature here.
## External Authentication ## External Authentication
See the detailed documentation page: See the detailed documentation page: