Authentication token generation WIP (#98)
* fix stored data format * implement user_exists and check_password
This commit is contained in:
parent
90afdafbd9
commit
6bb29d79f8
@ -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),
|
||||||
|
Loading…
Reference in New Issue
Block a user