Files
app/ts/reception/classes.loginsession.ts
T

198 lines
5.5 KiB
TypeScript

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
*/
@plugins.smartdata.Manager()
export class LoginSession extends plugins.smartdata.SmartDataDbDoc<
LoginSession,
plugins.idpInterfaces.data.ILoginSession,
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) {
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,
},
});
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
// ========
@plugins.smartdata.unI()
public id: string;
@plugins.smartdata.svDb()
public data: plugins.idpInterfaces.data.ILoginSession['data'] = {
userId: null,
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 | null = null;
constructor() {
super();
}
/**
* invalidates a session
*/
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 rotated whenever it is issued
* @returns
*/
public async getRefreshToken() {
if (this.data.invalidated) {
return null;
}
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 refreshToken;
}
public async getTransferToken() {
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
): 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) {
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;
}
}