2024-10-01 13:49:18 +02:00
|
|
|
import * as plugins from '../plugins.js';
|
2024-09-29 13:56:38 +02:00
|
|
|
import { LoginSessionManager } from './classes.loginsessionmanager.js';
|
|
|
|
|
import { User } from './classes.user.js';
|
|
|
|
|
|
2026-04-20 08:12:07 +00:00
|
|
|
export type TRefreshTokenValidationResult = 'current' | 'invalid' | 'invalidated' | 'reused';
|
|
|
|
|
|
2024-09-29 13:56:38 +02:00
|
|
|
/**
|
|
|
|
|
* a LoginSession keeps track of a login over the whole time of the user being loggedin
|
|
|
|
|
*/
|
|
|
|
|
@plugins.smartdata.Manager()
|
|
|
|
|
export class LoginSession extends plugins.smartdata.SmartDataDbDoc<
|
|
|
|
|
LoginSession,
|
2024-10-07 10:26:21 +02:00
|
|
|
plugins.idpInterfaces.data.ILoginSession,
|
2024-09-29 13:56:38 +02:00
|
|
|
LoginSessionManager
|
|
|
|
|
> {
|
|
|
|
|
// ======
|
|
|
|
|
// static
|
|
|
|
|
// ======
|
|
|
|
|
public static async createLoginSessionForUser(userArg: User, deleteOtherSessions = false) {
|
|
|
|
|
const loginSession = new LoginSession();
|
|
|
|
|
loginSession.id = plugins.smartunique.shortId();
|
|
|
|
|
loginSession.data.userId = userArg.id;
|
|
|
|
|
await loginSession.save();
|
|
|
|
|
return loginSession;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static async clearLoginSessionsForUser(userArg: User) {
|
|
|
|
|
// lets find existing sessions
|
|
|
|
|
const existingSessions = await LoginSession.getInstances({
|
|
|
|
|
id: userArg.id,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
for (const existingSession of existingSessions) {
|
|
|
|
|
await existingSession.delete();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static async getLoginSessionBySessionId(sessionIdArg: string) {
|
|
|
|
|
return await LoginSession.getInstance({
|
|
|
|
|
id: sessionIdArg,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static async getLoginSessionByRefreshToken(refreshTokenArg: string) {
|
2026-04-20 08:12:07 +00:00
|
|
|
const refreshTokenHash = await LoginSession.hashSessionToken(refreshTokenArg);
|
|
|
|
|
let loginSession = await LoginSession.getInstance({
|
|
|
|
|
'data.refreshTokenHash': refreshTokenHash,
|
|
|
|
|
});
|
|
|
|
|
if (loginSession) {
|
|
|
|
|
return loginSession;
|
|
|
|
|
}
|
|
|
|
|
loginSession = await LoginSession.getInstance({
|
2024-09-29 13:56:38 +02:00
|
|
|
data: {
|
|
|
|
|
refreshToken: refreshTokenArg,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
return loginSession;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 08:12:07 +00:00
|
|
|
public static async hashSessionToken(tokenArg: string) {
|
|
|
|
|
return plugins.smarthash.sha256FromString(tokenArg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static createOpaqueToken(prefixArg: string) {
|
|
|
|
|
return `${prefixArg}${plugins.crypto.randomBytes(32).toString('base64url')}`;
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-29 13:56:38 +02:00
|
|
|
// ========
|
|
|
|
|
// INSTANCE
|
|
|
|
|
// ========
|
|
|
|
|
@plugins.smartdata.unI()
|
|
|
|
|
public id: string;
|
|
|
|
|
|
|
|
|
|
@plugins.smartdata.svDb()
|
2024-10-07 10:26:21 +02:00
|
|
|
public data: plugins.idpInterfaces.data.ILoginSession['data'] = {
|
2024-09-29 13:56:38 +02:00
|
|
|
userId: null,
|
|
|
|
|
validUntil: Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ weeks: 1 }),
|
|
|
|
|
invalidated: false,
|
|
|
|
|
refreshToken: null,
|
2026-04-20 08:12:07 +00:00
|
|
|
refreshTokenHash: null,
|
|
|
|
|
rotatedRefreshTokenHashes: [],
|
|
|
|
|
transferTokenHash: null,
|
|
|
|
|
transferTokenExpiresAt: null,
|
2025-12-01 18:56:16 +00:00
|
|
|
deviceId: null,
|
|
|
|
|
deviceInfo: null,
|
|
|
|
|
createdAt: Date.now(),
|
|
|
|
|
lastActive: Date.now(),
|
2024-09-29 13:56:38 +02:00
|
|
|
};
|
|
|
|
|
|
2026-04-20 08:12:07 +00:00
|
|
|
public transferToken: string | null = null;
|
2024-09-29 13:56:38 +02:00
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
super();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* invalidates a session
|
|
|
|
|
*/
|
|
|
|
|
public async invalidate() {
|
|
|
|
|
this.data.invalidated = true;
|
2026-04-20 08:12:07 +00:00
|
|
|
this.data.refreshToken = null;
|
|
|
|
|
this.data.refreshTokenHash = null;
|
|
|
|
|
this.data.transferTokenHash = null;
|
|
|
|
|
this.data.transferTokenExpiresAt = null;
|
2024-09-29 13:56:38 +02:00
|
|
|
await this.save();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-04-20 08:12:07 +00:00
|
|
|
* a refresh token is unique to a login session and rotated whenever it is issued
|
2024-09-29 13:56:38 +02:00
|
|
|
* @returns
|
|
|
|
|
*/
|
|
|
|
|
public async getRefreshToken() {
|
|
|
|
|
if (this.data.invalidated) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2026-04-20 08:12:07 +00:00
|
|
|
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);
|
2024-09-29 13:56:38 +02:00
|
|
|
}
|
2026-04-20 08:12:07 +00:00
|
|
|
|
|
|
|
|
const refreshToken = LoginSession.createOpaqueToken('refresh_');
|
|
|
|
|
this.data.refreshTokenHash = await LoginSession.hashSessionToken(refreshToken);
|
|
|
|
|
this.data.refreshToken = null;
|
|
|
|
|
this.data.lastActive = Date.now();
|
2024-09-29 13:56:38 +02:00
|
|
|
await this.save();
|
2026-04-20 08:12:07 +00:00
|
|
|
return refreshToken;
|
2024-09-29 13:56:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async getTransferToken() {
|
2026-04-20 08:12:07 +00:00
|
|
|
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();
|
2024-09-29 13:56:38 +02:00
|
|
|
return this.transferToken;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 08:12:07 +00:00
|
|
|
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';
|
2024-09-29 13:56:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async validateTransferToken(transferTokenArg: string) {
|
2026-04-20 08:12:07 +00:00
|
|
|
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));
|
2024-09-29 13:56:38 +02:00
|
|
|
|
|
|
|
|
// a transfer token can only be used once, so we invalidate it here
|
|
|
|
|
if (result) {
|
|
|
|
|
this.transferToken = null;
|
2026-04-20 08:12:07 +00:00
|
|
|
this.data.transferTokenHash = null;
|
|
|
|
|
this.data.transferTokenExpiresAt = null;
|
|
|
|
|
await this.save();
|
2024-09-29 13:56:38 +02:00
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
}
|