From e5a0c649e36e0db419d04446affe2564c45cf321 Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Tue, 16 Jul 2024 19:17:53 +0200 Subject: [PATCH] fix: store only 10 share tokens in the cookies and clear the expired ones --- backend/src/share/share.controller.ts | 38 ++++++++++++++++++++++++++- backend/src/share/share.service.ts | 26 +++++++++++------- 2 files changed, 53 insertions(+), 11 deletions(-) diff --git a/backend/src/share/share.controller.ts b/backend/src/share/share.controller.ts index 230ca137..2abb9399 100644 --- a/backend/src/share/share.controller.ts +++ b/backend/src/share/share.controller.ts @@ -10,9 +10,11 @@ import { Res, UseGuards, } from "@nestjs/common"; +import { JwtService } from "@nestjs/jwt"; import { Throttle } from "@nestjs/throttler"; import { User } from "@prisma/client"; import { Request, Response } from "express"; +import * as moment from "moment"; import { GetUser } from "src/auth/decorator/getUser.decorator"; import { AdministratorGuard } from "src/auth/guard/isAdmin.guard"; import { JwtGuard } from "src/auth/guard/jwt.guard"; @@ -29,7 +31,10 @@ import { ShareTokenSecurity } from "./guard/shareTokenSecurity.guard"; import { ShareService } from "./share.service"; @Controller("shares") export class ShareController { - constructor(private shareService: ShareService) {} + constructor( + private shareService: ShareService, + private jwtService: JwtService, + ) {} @Get("all") @UseGuards(JwtGuard, AdministratorGuard) @@ -121,10 +126,13 @@ export class ShareController { @Post(":id/token") async getShareToken( @Param("id") id: string, + @Req() request: Request, @Res({ passthrough: true }) response: Response, @Body() body: SharePasswordDto, ) { const token = await this.shareService.getShareToken(id, body.password); + + this.clearShareTokenCookies(request, response); response.cookie(`share_${id}_token`, token, { path: "/", httpOnly: true, @@ -132,4 +140,32 @@ export class ShareController { return { token }; } + + /** + * Keeps the 10 most recent share token cookies and deletes the rest and all expired ones + */ + private clearShareTokenCookies(request: Request, response: Response) { + const shareTokenCookies = Object.entries(request.cookies) + .filter(([key]) => key.startsWith("share_") && key.endsWith("_token")) + .map(([key, value]) => ({ + key, + payload: this.jwtService.decode(value), + })); + + const expiredTokens = shareTokenCookies.filter( + (cookie) => cookie.payload.exp < moment().unix(), + ); + const validTokens = shareTokenCookies.filter( + (cookie) => cookie.payload.exp >= moment().unix(), + ); + + expiredTokens.forEach((cookie) => response.clearCookie(cookie.key)); + + if (validTokens.length > 10) { + validTokens + .sort((a, b) => a.payload.exp - b.payload.exp) + .slice(0, -10) + .forEach((cookie) => response.clearCookie(cookie.key)); + } + } } diff --git a/backend/src/share/share.service.ts b/backend/src/share/share.service.ts index 7489b202..ad38a9da 100644 --- a/backend/src/share/share.service.ts +++ b/backend/src/share/share.service.ts @@ -4,7 +4,7 @@ import { Injectable, NotFoundException, } from "@nestjs/common"; -import { JwtService } from "@nestjs/jwt"; +import { JwtService, JwtSignOptions } from "@nestjs/jwt"; import { Share, User } from "@prisma/client"; import * as archiver from "archiver"; import * as argon from "argon2"; @@ -328,15 +328,21 @@ export class ShareService { const { expiration } = await this.prisma.share.findUnique({ where: { id: shareId }, }); - return this.jwtService.sign( - { - shareId, - }, - { - expiresIn: moment(expiration).diff(new Date(), "seconds") + "s", - secret: this.config.get("internal.jwtSecret"), - }, - ); + + const tokenPayload = { + shareId, + iat: moment().unix(), + }; + + const tokenOptions: JwtSignOptions = { + secret: this.config.get("internal.jwtSecret"), + }; + + if (!moment(expiration).isSame(0)) { + tokenOptions.expiresIn = moment(expiration).diff(new Date(), "seconds"); + } + + return this.jwtService.sign(tokenPayload, tokenOptions); } async verifyShareToken(shareId: string, token: string) {