feat(auth): harden authentication with argon2 passwords and rotating hashed refresh tokens
This commit is contained in:
@@ -2,6 +2,8 @@ import * as plugins from '../plugins.js';
|
||||
import { LoginSessionManager } from './classes.loginsessionmanager.js';
|
||||
import { User } from './classes.user.js';
|
||||
|
||||
export type TRefreshTokenValidationResult = 'current' | 'invalid' | 'invalidated' | 'reused';
|
||||
|
||||
/**
|
||||
* a LoginSession keeps track of a login over the whole time of the user being loggedin
|
||||
*/
|
||||
@@ -40,7 +42,14 @@ export class LoginSession extends plugins.smartdata.SmartDataDbDoc<
|
||||
}
|
||||
|
||||
public static async getLoginSessionByRefreshToken(refreshTokenArg: string) {
|
||||
const loginSession = await LoginSession.getInstance({
|
||||
const refreshTokenHash = await LoginSession.hashSessionToken(refreshTokenArg);
|
||||
let loginSession = await LoginSession.getInstance({
|
||||
'data.refreshTokenHash': refreshTokenHash,
|
||||
});
|
||||
if (loginSession) {
|
||||
return loginSession;
|
||||
}
|
||||
loginSession = await LoginSession.getInstance({
|
||||
data: {
|
||||
refreshToken: refreshTokenArg,
|
||||
},
|
||||
@@ -48,6 +57,14 @@ export class LoginSession extends plugins.smartdata.SmartDataDbDoc<
|
||||
return loginSession;
|
||||
}
|
||||
|
||||
public static async hashSessionToken(tokenArg: string) {
|
||||
return plugins.smarthash.sha256FromString(tokenArg);
|
||||
}
|
||||
|
||||
public static createOpaqueToken(prefixArg: string) {
|
||||
return `${prefixArg}${plugins.crypto.randomBytes(32).toString('base64url')}`;
|
||||
}
|
||||
|
||||
// ========
|
||||
// INSTANCE
|
||||
// ========
|
||||
@@ -60,13 +77,17 @@ export class LoginSession extends plugins.smartdata.SmartDataDbDoc<
|
||||
validUntil: Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ weeks: 1 }),
|
||||
invalidated: false,
|
||||
refreshToken: null,
|
||||
refreshTokenHash: null,
|
||||
rotatedRefreshTokenHashes: [],
|
||||
transferTokenHash: null,
|
||||
transferTokenExpiresAt: null,
|
||||
deviceId: null,
|
||||
deviceInfo: null,
|
||||
createdAt: Date.now(),
|
||||
lastActive: Date.now(),
|
||||
};
|
||||
|
||||
public transferToken: string;
|
||||
public transferToken: string | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -77,40 +98,99 @@ export class LoginSession extends plugins.smartdata.SmartDataDbDoc<
|
||||
*/
|
||||
public async invalidate() {
|
||||
this.data.invalidated = true;
|
||||
this.data.refreshToken = null;
|
||||
this.data.refreshTokenHash = null;
|
||||
this.data.transferTokenHash = null;
|
||||
this.data.transferTokenExpiresAt = null;
|
||||
await this.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* a refresh token is unique to a login session and ONLY created once per login session
|
||||
* a refresh token is unique to a login session and rotated whenever it is issued
|
||||
* @returns
|
||||
*/
|
||||
public async getRefreshToken() {
|
||||
if (this.data.invalidated) {
|
||||
console.log('login session is invalidated. no refresh token can be generated.');
|
||||
return null;
|
||||
}
|
||||
if (!this.data.refreshToken) {
|
||||
this.data.refreshToken = plugins.smartunique.uni('refresh_');
|
||||
const previousRefreshTokenHash =
|
||||
this.data.refreshTokenHash ||
|
||||
(this.data.refreshToken
|
||||
? await LoginSession.hashSessionToken(this.data.refreshToken)
|
||||
: null);
|
||||
|
||||
if (previousRefreshTokenHash) {
|
||||
this.data.rotatedRefreshTokenHashes = [
|
||||
...(this.data.rotatedRefreshTokenHashes || []),
|
||||
previousRefreshTokenHash,
|
||||
].slice(-5);
|
||||
}
|
||||
|
||||
const refreshToken = LoginSession.createOpaqueToken('refresh_');
|
||||
this.data.refreshTokenHash = await LoginSession.hashSessionToken(refreshToken);
|
||||
this.data.refreshToken = null;
|
||||
this.data.lastActive = Date.now();
|
||||
await this.save();
|
||||
return this.data.refreshToken;
|
||||
return refreshToken;
|
||||
}
|
||||
|
||||
public async getTransferToken() {
|
||||
this.transferToken = plugins.smartunique.uni('transfer_');
|
||||
this.transferToken = LoginSession.createOpaqueToken('transfer_');
|
||||
this.data.transferTokenHash = await LoginSession.hashSessionToken(this.transferToken);
|
||||
this.data.transferTokenExpiresAt =
|
||||
Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ minutes: 5 });
|
||||
await this.save();
|
||||
return this.transferToken;
|
||||
}
|
||||
|
||||
public async validateRefreshToken(refreshTokenArg: string) {
|
||||
return this.data.refreshToken === refreshTokenArg;
|
||||
public async validateRefreshToken(
|
||||
refreshTokenArg: string
|
||||
): Promise<TRefreshTokenValidationResult> {
|
||||
if (this.data.invalidated) {
|
||||
return 'invalidated';
|
||||
}
|
||||
|
||||
const refreshTokenHash = await LoginSession.hashSessionToken(refreshTokenArg);
|
||||
|
||||
if (
|
||||
this.data.refreshTokenHash === refreshTokenHash ||
|
||||
(!!this.data.refreshToken && this.data.refreshToken === refreshTokenArg)
|
||||
) {
|
||||
return 'current';
|
||||
}
|
||||
|
||||
if ((this.data.rotatedRefreshTokenHashes || []).includes(refreshTokenHash)) {
|
||||
return 'reused';
|
||||
}
|
||||
|
||||
return 'invalid';
|
||||
}
|
||||
|
||||
public async validateTransferToken(transferTokenArg: string) {
|
||||
const result = this.transferToken === transferTokenArg;
|
||||
if (this.data.invalidated || !this.data.transferTokenHash) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
this.data.transferTokenExpiresAt &&
|
||||
this.data.transferTokenExpiresAt < Date.now()
|
||||
) {
|
||||
this.data.transferTokenHash = null;
|
||||
this.data.transferTokenExpiresAt = null;
|
||||
await this.save();
|
||||
return false;
|
||||
}
|
||||
|
||||
const result =
|
||||
this.data.transferTokenHash ===
|
||||
(await LoginSession.hashSessionToken(transferTokenArg));
|
||||
|
||||
// a transfer token can only be used once, so we invalidate it here
|
||||
if (result) {
|
||||
this.transferToken = null;
|
||||
this.data.transferTokenHash = null;
|
||||
this.data.transferTokenExpiresAt = null;
|
||||
await this.save();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user