feat(auth): harden authentication with argon2 passwords and rotating hashed refresh tokens
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user