Authentication token generation WIP (#98)

* fix stored data format
* implement user_exists and check_password
This commit is contained in:
John Livingston 2024-06-17 12:44:26 +02:00
parent 90afdafbd9
commit 6bb29d79f8
No known key found for this signature in database
GPG Key ID: B17B5640CE66CDBC

View File

@ -19,6 +19,16 @@ type SavedLivechatToken = Omit<LivechatToken, 'jid' | 'nickname' | 'password'> &
encryptedPassword: string encryptedPassword: string
} }
interface SavedUserData {
userId: number
tokens: SavedLivechatToken[]
}
interface LivechatTokenInfos {
userId: number
tokens: LivechatToken[]
}
async function getRandomBytes (size: number): Promise<Buffer> { async function getRandomBytes (size: number): Promise<Buffer> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
randomBytes(size, (err, buf) => { randomBytes(size, (err, buf) => {
@ -55,7 +65,7 @@ export class LivechatProsodyAuth {
private readonly _prosodyDomain: string private readonly _prosodyDomain: string
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 _jidTokens: Map<string, LivechatToken[]> = new Map() private readonly _tokensInfoByJID: Map<string, LivechatTokenInfos | undefined> = new Map()
private readonly _secretKey: string private readonly _secretKey: string
protected readonly _logger: { protected readonly _logger: {
debug: (s: string) => void debug: (s: string) => void
@ -118,7 +128,23 @@ export class LivechatProsodyAuth {
public async userRegistered (normalizedUsername: string): Promise<boolean> { public async userRegistered (normalizedUsername: string): Promise<boolean> {
const entry = this._getAndClean(normalizedUsername) 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<boolean> { public async checkUserPassword (normalizedUsername: string, password: string): Promise<boolean> {
@ -126,6 +152,24 @@ export class LivechatProsodyAuth {
if (entry && entry.password === password) { if (entry && entry.password === password) {
return true 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 return false
} }
@ -135,41 +179,80 @@ 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 (!user || !user.id) {
return undefined
}
if (user.blocked) {
return undefined
}
const normalizedUsername = this._normalizeUsername(user) const normalizedUsername = this._normalizeUsername(user)
if (!normalizedUsername) { if (!normalizedUsername) {
return undefined return undefined
} }
const nickname: string | undefined = await getUserNickname(this._options, user) const nickname: string | undefined = await getUserNickname(this._options, user)
const jid = normalizedUsername + '@' + this._prosodyDomain const jid = normalizedUsername + '@' + this._prosodyDomain
const tokens = await this._getJIDTokens(jid) const tokensInfo = await this._getTokensInfoForJID(jid)
for (const token of tokens) { if (!tokensInfo) { return [] }
token.nickname = nickname
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 return tokens
} }
public async createUserToken (user: MUserDefault, label: string): Promise<LivechatToken | undefined> { public async createUserToken (user: MUserDefault, label: string): Promise<LivechatToken | undefined> {
if (!user || !user.id) {
return undefined
}
if (user.blocked) {
return undefined
}
const normalizedUsername = this._normalizeUsername(user) const normalizedUsername = this._normalizeUsername(user)
if (!normalizedUsername) { if (!normalizedUsername) {
return undefined return undefined
} }
const nickname: string | undefined = await getUserNickname(this._options, user) const nickname: string | undefined = await getUserNickname(this._options, user)
const jid = normalizedUsername + '@' + this._prosodyDomain const jid = normalizedUsername + '@' + this._prosodyDomain
const token = await this._createJIDToken(jid, label) const token = await this._createToken(user.id, jid, label)
token.nickname = nickname token.nickname = nickname
return token return token
} }
public async revokeUserToken (user: MUserDefault, id: number): Promise<boolean> { public async revokeUserToken (user: MUserDefault, id: number): Promise<boolean> {
if (!user || !user.id) {
return false
}
if (user.blocked) {
return false
}
const normalizedUsername = this._normalizeUsername(user) const normalizedUsername = this._normalizeUsername(user)
if (!normalizedUsername) { if (!normalizedUsername) {
return false return false
} }
const jid = normalizedUsername + '@' + this._prosodyDomain const jid = normalizedUsername + '@' + this._prosodyDomain
let tokens = await this._getJIDTokens(jid) const tokensInfo = await this._getTokensInfoForJID(jid)
tokens = tokens.filter(t => t.id !== id) if (!tokensInfo) {
await this._saveJIDTokens(jid, tokens) // 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 return true
} }
@ -190,7 +273,7 @@ export class LivechatProsodyAuth {
} }
private _normalizeUsername (user: MUserDefault): string | undefined { private _normalizeUsername (user: MUserDefault): string | undefined {
if (!user) { if (!user || !user.id) {
return undefined return undefined
} }
if (user.blocked) { if (user.blocked) {
@ -204,17 +287,27 @@ export class LivechatProsodyAuth {
return normalizedUsername return normalizedUsername
} }
private _getAndClean (user: string): Password | undefined { private _getAndClean (normalizedUsername: string): Password | undefined {
const entry = this._passwords.get(user) const entry = this._passwords.get(normalizedUsername)
if (entry) { if (entry) {
if (entry.validity > Date.now()) { if (entry.validity > Date.now()) {
return entry return entry
} }
this._passwords.delete(user) this._passwords.delete(normalizedUsername)
} }
return undefined return undefined
} }
private async _userIdValid (userId: number): Promise<boolean> {
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 { private _jidTokenPath (jid: string): string {
// Simple security check: // Simple security check:
if (jid === '.' || jid === '..' || jid.includes('/')) { if (jid === '.' || jid === '..' || jid.includes('/')) {
@ -223,22 +316,22 @@ export class LivechatProsodyAuth {
return path.join(this._tokensPath, jid + '.json') return path.join(this._tokensPath, jid + '.json')
} }
private async _getJIDTokens (jid: string): Promise<LivechatToken[]> { private async _getTokensInfoForJID (jid: string): Promise<LivechatTokenInfos | undefined> {
try { try {
const cached = this._jidTokens.get(jid) const cached = this._tokensInfoByJID.get(jid)
if (cached) { if (cached) {
return cached return cached
} }
const filePath = this._jidTokenPath(jid) const filePath = this._jidTokenPath(jid)
const content = await fs.promises.readFile(filePath) const content = await fs.promises.readFile(filePath)
const json = JSON.parse(content.toString()) as SavedLivechatToken[] const json = JSON.parse(content.toString()) as SavedUserData
if (!Array.isArray(json)) { if ((typeof json !== 'object') || (typeof json.userId !== 'number') || (!Array.isArray(json.tokens))) {
throw new Error('Invalid token file content') throw new Error('Invalid token file content')
} }
const tokens: LivechatToken[] = [] const tokens: LivechatToken[] = []
for (const entry of json) { for (const entry of json.tokens) {
const token: LivechatToken = { const token: LivechatToken = {
jid, jid,
password: await this._decrypt(entry.encryptedPassword), password: await this._decrypt(entry.encryptedPassword),
@ -249,24 +342,29 @@ export class LivechatProsodyAuth {
tokens.push(token) tokens.push(token)
} }
this._jidTokens.set(jid, tokens) const d = {
return tokens userId: json.userId,
tokens
}
this._tokensInfoByJID.set(jid, d)
return d
} catch (err: any) { } catch (err: any) {
if (('code' in err) && err.code === 'ENOENT') { if (('code' in err) && err.code === 'ENOENT') {
// User has no token, this is normal. // User has no token, this is normal.
this._jidTokens.set(jid, []) this._tokensInfoByJID.set(jid, undefined)
return [] return undefined
} }
throw err throw err
} }
} }
private async _createJIDToken (jid: string, label: string): Promise<LivechatToken> { private async _createToken (userId: number, jid: string, label: string): Promise<LivechatToken> {
const tokens = await this._getJIDTokens(jid) 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. // 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 now = Date.now()
const id = 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.') throw new Error('There is already a token with this id.')
} }
@ -279,16 +377,24 @@ export class LivechatProsodyAuth {
password, password,
label label
} }
tokens.push(newToken) tokensInfo.tokens.push(newToken)
await this._saveJIDTokens(jid, tokens) await this._saveTokens(userId, jid, tokensInfo.tokens)
return newToken return newToken
} }
private async _saveJIDTokens (jid: string, tokens: LivechatToken[]): Promise<void> { private async _saveTokens (userId: number, jid: string, tokens: LivechatToken[]): Promise<void> {
this._jidTokens.set(jid, tokens) const ti = {
const toSave: SavedLivechatToken[] = [] userId,
tokens
}
this._tokensInfoByJID.set(jid, ti)
const toSave: SavedUserData = {
userId,
tokens: []
}
for (const t of tokens) { for (const t of tokens) {
toSave.push({ toSave.tokens.push({
id: t.id, id: t.id,
date: t.date, date: t.date,
encryptedPassword: await this._encrypt(t.password), encryptedPassword: await this._encrypt(t.password),