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 { 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; } }