From 6bb29d79f812ed9431ca348216b7659d2e6687ab Mon Sep 17 00:00:00 2001 From: John Livingston Date: Mon, 17 Jun 2024 12:44:26 +0200 Subject: [PATCH] Authentication token generation WIP (#98) * fix stored data format * implement user_exists and check_password --- server/lib/prosody/auth.ts | 168 ++++++++++++++++++++++++++++++------- 1 file changed, 137 insertions(+), 31 deletions(-) diff --git a/server/lib/prosody/auth.ts b/server/lib/prosody/auth.ts index 5018643f..674d281a 100644 --- a/server/lib/prosody/auth.ts +++ b/server/lib/prosody/auth.ts @@ -19,6 +19,16 @@ type SavedLivechatToken = Omit & encryptedPassword: string } +interface SavedUserData { + userId: number + tokens: SavedLivechatToken[] +} + +interface LivechatTokenInfos { + userId: number + tokens: LivechatToken[] +} + async function getRandomBytes (size: number): Promise { return new Promise((resolve, reject) => { randomBytes(size, (err, buf) => { @@ -55,7 +65,7 @@ export class LivechatProsodyAuth { private readonly _prosodyDomain: string private readonly _tokensPath: string private readonly _passwords: Map = new Map() - private readonly _jidTokens: Map = new Map() + private readonly _tokensInfoByJID: Map = new Map() private readonly _secretKey: string protected readonly _logger: { debug: (s: string) => void @@ -118,7 +128,23 @@ export class LivechatProsodyAuth { public async userRegistered (normalizedUsername: string): Promise { const entry = this._getAndClean(normalizedUsername) - return !!entry + if (entry) { + return true + } + try { + 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 } public async checkUserPassword (normalizedUsername: string, password: string): Promise { @@ -126,6 +152,24 @@ export class LivechatProsodyAuth { if (entry && entry.password === password) { return true } + try { + 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 false + } + + // Is the password in tokens? + if (tokensInfo.tokens.find((t) => t.password === password)) { + return true + } + } catch (err) { + this._logger.error(err as string) + return false + } return false } @@ -135,41 +179,80 @@ export class LivechatProsodyAuth { * @param user the user */ public async getUserTokens (user: MUserDefault): Promise { + if (!user || !user.id) { + return undefined + } + if (user.blocked) { + return undefined + } const normalizedUsername = this._normalizeUsername(user) if (!normalizedUsername) { return undefined } const nickname: string | undefined = await getUserNickname(this._options, user) const jid = normalizedUsername + '@' + this._prosodyDomain - const tokens = await this._getJIDTokens(jid) - for (const token of tokens) { - token.nickname = nickname + const tokensInfo = await this._getTokensInfoForJID(jid) + if (!tokensInfo) { return [] } + + if (tokensInfo.userId !== user.id) { + return undefined + } + + const tokens = [] + for (const token of tokensInfo.tokens) { + // Cloning, and adding the nickname. + tokens.push( + Object.assign({}, token, { + nickname + }) + ) } return tokens } public async createUserToken (user: MUserDefault, label: string): Promise { + if (!user || !user.id) { + return undefined + } + if (user.blocked) { + return undefined + } const normalizedUsername = this._normalizeUsername(user) if (!normalizedUsername) { return undefined } const nickname: string | undefined = await getUserNickname(this._options, user) const jid = normalizedUsername + '@' + this._prosodyDomain - const token = await this._createJIDToken(jid, label) + const token = await this._createToken(user.id, jid, label) + token.nickname = nickname return token } public async revokeUserToken (user: MUserDefault, id: number): Promise { + if (!user || !user.id) { + return false + } + if (user.blocked) { + return false + } const normalizedUsername = this._normalizeUsername(user) if (!normalizedUsername) { return false } const jid = normalizedUsername + '@' + this._prosodyDomain - let tokens = await this._getJIDTokens(jid) + const tokensInfo = await this._getTokensInfoForJID(jid) - tokens = tokens.filter(t => t.id !== id) - await this._saveJIDTokens(jid, tokens) + if (!tokensInfo) { + // No saved token, consider ok. + return true + } + + if (tokensInfo.userId !== user.id) { + return false + } + + await this._saveTokens(user.id, jid, tokensInfo.tokens.filter(t => t.id !== id)) return true } @@ -190,7 +273,7 @@ export class LivechatProsodyAuth { } private _normalizeUsername (user: MUserDefault): string | undefined { - if (!user) { + if (!user || !user.id) { return undefined } if (user.blocked) { @@ -204,17 +287,27 @@ export class LivechatProsodyAuth { return normalizedUsername } - private _getAndClean (user: string): Password | undefined { - const entry = this._passwords.get(user) + private _getAndClean (normalizedUsername: string): Password | undefined { + const entry = this._passwords.get(normalizedUsername) if (entry) { if (entry.validity > Date.now()) { return entry } - this._passwords.delete(user) + this._passwords.delete(normalizedUsername) } return undefined } + private async _userIdValid (userId: number): Promise { + try { + const user = await this._options.peertubeHelpers.user.loadById(userId) + if (!user || user.blocked) { return false } + return true + } catch (err) { + return false + } + } + private _jidTokenPath (jid: string): string { // Simple security check: if (jid === '.' || jid === '..' || jid.includes('/')) { @@ -223,22 +316,22 @@ export class LivechatProsodyAuth { return path.join(this._tokensPath, jid + '.json') } - private async _getJIDTokens (jid: string): Promise { + private async _getTokensInfoForJID (jid: string): Promise { try { - const cached = this._jidTokens.get(jid) + const cached = this._tokensInfoByJID.get(jid) if (cached) { return cached } const filePath = this._jidTokenPath(jid) const content = await fs.promises.readFile(filePath) - const json = JSON.parse(content.toString()) as SavedLivechatToken[] - if (!Array.isArray(json)) { + const json = JSON.parse(content.toString()) as SavedUserData + if ((typeof json !== 'object') || (typeof json.userId !== 'number') || (!Array.isArray(json.tokens))) { throw new Error('Invalid token file content') } const tokens: LivechatToken[] = [] - for (const entry of json) { + for (const entry of json.tokens) { const token: LivechatToken = { jid, password: await this._decrypt(entry.encryptedPassword), @@ -249,24 +342,29 @@ export class LivechatProsodyAuth { tokens.push(token) } - this._jidTokens.set(jid, tokens) - return tokens + const d = { + userId: json.userId, + tokens + } + this._tokensInfoByJID.set(jid, d) + return d } catch (err: any) { if (('code' in err) && err.code === 'ENOENT') { // User has no token, this is normal. - this._jidTokens.set(jid, []) - return [] + this._tokensInfoByJID.set(jid, undefined) + return undefined } throw err } } - private async _createJIDToken (jid: string, label: string): Promise { - const tokens = await this._getJIDTokens(jid) + private async _createToken (userId: number, jid: string, label: string): Promise { + const tokensInfo = (await this._getTokensInfoForJID(jid)) ?? { userId, tokens: [] } + // Using Date.now result as id, so we are pretty sure to not have 2 tokens with the same id. const now = Date.now() const id = now - if (tokens.find(t => t.id === id)) { + if (tokensInfo.tokens.find(t => t.id === id)) { throw new Error('There is already a token with this id.') } @@ -279,16 +377,24 @@ export class LivechatProsodyAuth { password, label } - tokens.push(newToken) - await this._saveJIDTokens(jid, tokens) + tokensInfo.tokens.push(newToken) + await this._saveTokens(userId, jid, tokensInfo.tokens) return newToken } - private async _saveJIDTokens (jid: string, tokens: LivechatToken[]): Promise { - this._jidTokens.set(jid, tokens) - const toSave: SavedLivechatToken[] = [] + private async _saveTokens (userId: number, jid: string, tokens: LivechatToken[]): Promise { + const ti = { + userId, + tokens + } + this._tokensInfoByJID.set(jid, ti) + + const toSave: SavedUserData = { + userId, + tokens: [] + } for (const t of tokens) { - toSave.push({ + toSave.tokens.push({ id: t.id, date: t.date, encryptedPassword: await this._encrypt(t.password),