feat(auth): harden authentication with argon2 passwords and rotating hashed refresh tokens

This commit is contained in:
2026-04-20 08:12:07 +00:00
parent ad3e51a9e8
commit 98e614a945
27 changed files with 4225 additions and 2258 deletions
+99 -44
View File
@@ -1,5 +1,5 @@
import * as plugins from '../plugins.js';
import { LoginSession } from './classes.loginsession.js';
import { LoginSession, type TRefreshTokenValidationResult } from './classes.loginsession.js';
import { Reception } from './classes.reception.js';
import { logger } from './logging.js';
@@ -32,9 +32,6 @@ export class LoginSessionManager {
let user = await this.receptionRef.userManager.CUser.getInstance({
data: {
username: requestData.username,
passwordHash: await this.receptionRef.userManager.CUser.hashPassword(
requestData.password
),
},
});
@@ -42,33 +39,30 @@ export class LoginSessionManager {
user = await this.receptionRef.userManager.CUser.getInstance({
data: {
email: requestData.username,
passwordHash: await this.receptionRef.userManager.CUser.hashPassword(
requestData.password
),
},
});
}
if (user) {
// lets recheck
if (
(user.data.username !== requestData.username &&
user.data.email !== requestData.username) ||
user.data.passwordHash !==
(await this.receptionRef.userManager.CUser.hashPassword(requestData.password))
) {
throw new Error(
'database returned a user that does not match wanted criterea. CRITICAL!'
if (user && (await this.receptionRef.userManager.CUser.verifyPassword(
requestData.password,
user.data.passwordHash
))) {
if (this.receptionRef.userManager.CUser.shouldUpgradePasswordHash(user.data.passwordHash)) {
user.data.passwordHash = await this.receptionRef.userManager.CUser.hashPassword(
requestData.password
);
await user.save();
}
const loginSession = await LoginSession.createLoginSessionForUser(user);
this.loginSessions.add(loginSession);
const refreshToken = await loginSession.getRefreshToken();
if (!refreshToken) {
throw new plugins.typedrequest.TypedResponseError('Could not create login session');
}
return {
status: 'ok',
refreshToken: refreshToken,
refreshToken,
twoFaNeeded: false,
};
} else {
@@ -109,12 +103,14 @@ export class LoginSessionManager {
} else {
logger.log('info', `loginWithEmail did not find user: ${requestDataArg.email}`);
}
const testOnlyToken =
process.env.TEST_MODE && existingUser
? this.emailTokenMap.findSync((itemArg) => itemArg.email === existingUser.data.email)
?.token
: undefined;
return {
status: 'ok',
testOnlyToken: process.env.TEST_MODE
? this.emailTokenMap.findSync((itemArg) => itemArg.email === existingUser.data.email)
.token
: null,
testOnlyToken,
};
}
)
@@ -133,10 +129,17 @@ export class LoginSessionManager {
email: requestArg.email,
},
});
if (!user) {
throw new plugins.typedrequest.TypedResponseError('User not found');
}
const loginSession = await LoginSession.createLoginSessionForUser(user);
this.loginSessions.add(loginSession);
const refreshToken = await loginSession.getRefreshToken();
if (!refreshToken) {
throw new plugins.typedrequest.TypedResponseError('Could not create login session');
}
return {
refreshToken: await loginSession.getRefreshToken(),
refreshToken,
};
} else {
throw new plugins.typedrequest.TypedResponseError('Validation Token not found');
@@ -147,8 +150,11 @@ export class LoginSessionManager {
this.typedRouter.addTypedHandler<plugins.idpInterfaces.request.ILogoutRequest>(
new plugins.typedrequest.TypedHandler('logout', async (requestDataArg) => {
const loginSession = await this.CLoginSession.getLoginSessionByRefreshToken(requestDataArg.refreshToken);
await loginSession.invalidate();
const sessionLookup = await this.findLoginSessionByRefreshToken(requestDataArg.refreshToken);
if (!sessionLookup || sessionLookup.validationStatus !== 'current') {
throw new plugins.typedrequest.TypedResponseError('Invalid refresh token');
}
await sessionLookup.loginSession.invalidate();
return {}
})
);
@@ -158,31 +164,39 @@ export class LoginSessionManager {
'exchangeRefreshTokenAndTransferToken',
async (requestDataArg) => {
switch (true) {
case !!requestDataArg.refreshToken:
const loginSession = await this.loginSessions.find(async (loginSessionArg) => {
return loginSessionArg.validateRefreshToken(requestDataArg.refreshToken);
});
if (!loginSession) {
case !!requestDataArg.refreshToken: {
const sessionLookup = await this.findLoginSessionByRefreshToken(
requestDataArg.refreshToken
);
if (!sessionLookup || sessionLookup.validationStatus !== 'current') {
if (sessionLookup?.validationStatus === 'reused') {
await sessionLookup.loginSession.invalidate();
}
throw new plugins.typedrequest.TypedResponseError('your refresh token is invalid');
}
return {
transferToken: await loginSession.getTransferToken(),
transferToken: await sessionLookup.loginSession.getTransferToken(),
};
break;
case !!requestDataArg.transferToken:
let transferToken: string;
const loginSession2 = await this.loginSessions.find(async (loginSessionArg) => {
return loginSessionArg.validateTransferToken(requestDataArg.transferToken);
});
}
case !!requestDataArg.transferToken: {
const loginSession2 = await this.findLoginSessionByTransferToken(
requestDataArg.transferToken
);
if (!loginSession2) {
throw new plugins.typedrequest.TypedResponseError(
'Your transfer token is not valid.'
);
}
const refreshToken = await loginSession2.getRefreshToken();
if (!refreshToken) {
throw new plugins.typedrequest.TypedResponseError('Could not create login session');
}
return {
refreshToken: await loginSession2.getRefreshToken(),
refreshToken,
};
break;
}
default:
throw new plugins.typedrequest.TypedResponseError('Invalid token exchange request');
}
}
)
@@ -271,8 +285,7 @@ export class LoginSessionManager {
throw new plugins.typedrequest.TypedResponseError('Invalid JWT');
}
// Get the current session's refresh token to identify the current session
const currentRefreshToken = jwt.data.refreshToken;
const currentLoginSession = await jwt.getLoginSession();
// Get all sessions for this user
const sessions = await this.CLoginSession.getInstances({
@@ -290,7 +303,7 @@ export class LoginSessionManager {
ip: session.data.deviceInfo?.ip || 'Unknown',
lastActive: session.data.lastActive || session.data.createdAt || Date.now(),
createdAt: session.data.createdAt || Date.now(),
isCurrent: session.data.refreshToken === currentRefreshToken,
isCurrent: session.id === currentLoginSession?.id,
})),
};
}
@@ -317,8 +330,10 @@ export class LoginSessionManager {
throw new plugins.typedrequest.TypedResponseError('Session not found');
}
const currentLoginSession = await jwt.getLoginSession();
// Don't allow revoking the current session via this method
if (sessionToRevoke.data.refreshToken === jwt.data.refreshToken) {
if (sessionToRevoke.id === currentLoginSession?.id) {
throw new plugins.typedrequest.TypedResponseError(
'Cannot revoke current session. Use logout instead.'
);
@@ -338,4 +353,44 @@ export class LoginSessionManager {
)
);
}
public async findLoginSessionByRefreshToken(refreshTokenArg: string): Promise<{
loginSession: LoginSession;
validationStatus: TRefreshTokenValidationResult;
} | null> {
const directMatch = await this.CLoginSession.getLoginSessionByRefreshToken(refreshTokenArg);
if (directMatch) {
return {
loginSession: directMatch,
validationStatus: await directMatch.validateRefreshToken(refreshTokenArg),
};
}
const loginSessions = await this.CLoginSession.getInstances({});
for (const loginSession of loginSessions) {
const validationStatus = await loginSession.validateRefreshToken(refreshTokenArg);
if (validationStatus !== 'invalid') {
return {
loginSession,
validationStatus,
};
}
}
return null;
}
public async findLoginSessionByTransferToken(transferTokenArg: string) {
const transferTokenHash = await LoginSession.hashSessionToken(transferTokenArg);
const loginSession = await this.CLoginSession.getInstance({
'data.transferTokenHash': transferTokenHash,
});
if (!loginSession) {
return null;
}
const isValid = await loginSession.validateTransferToken(transferTokenArg);
return isValid ? loginSession : null;
}
}