diff --git a/server/src/core/auth/services/token.service.ts b/server/src/core/auth/services/token.service.ts index 5347f77a2..c4674daa3 100644 --- a/server/src/core/auth/services/token.service.ts +++ b/server/src/core/auth/services/token.service.ts @@ -114,6 +114,7 @@ export class TokenService { async verifyRefreshToken(refreshToken: string) { const secret = this.environmentService.getRefreshTokenSecret(); + const coolDown = this.environmentService.getRefreshTokenCoolDown(); const jwtPayload = await this.verifyJwt(refreshToken, secret); assert( @@ -139,7 +140,11 @@ export class TokenService { assert(user, 'User not found', NotFoundException); - if (token.isRevoked) { + // Check if revokedAt is less than coolDown + if ( + token.revokedAt && + token.revokedAt.getTime() <= Date.now() - ms(coolDown) + ) { // Revoke all user refresh tokens await this.prismaService.client.refreshToken.updateMany({ where: { @@ -148,16 +153,14 @@ export class TokenService { }, }, data: { - isRevoked: true, + revokedAt: new Date(), }, }); - } - assert( - !token.isRevoked, - 'Suspicious activity detected, this refresh token has been revoked. All tokens has been revoked.', - ForbiddenException, - ); + throw new ForbiddenException( + 'Suspicious activity detected, this refresh token has been revoked. All tokens has been revoked.', + ); + } return { user, token }; } @@ -177,7 +180,7 @@ export class TokenService { id, }, data: { - isRevoked: true, + revokedAt: new Date(), }, }); diff --git a/server/src/database/migrations/20230926150534_alter_table_refresh_token_add_revoked_at/migration.sql b/server/src/database/migrations/20230926150534_alter_table_refresh_token_add_revoked_at/migration.sql new file mode 100644 index 000000000..d2ea6379e --- /dev/null +++ b/server/src/database/migrations/20230926150534_alter_table_refresh_token_add_revoked_at/migration.sql @@ -0,0 +1,8 @@ +-- Step 1: Add the new column +ALTER TABLE "refresh_tokens" ADD COLUMN "revokedAt" TIMESTAMP(3); + +-- Step 2: Update the new column based on the isRevoked column value +UPDATE "refresh_tokens" SET "revokedAt" = NOW() - INTERVAL '1 day' WHERE "isRevoked" = TRUE; + +-- Step 3: Drop the isRevoked column +ALTER TABLE "refresh_tokens" DROP COLUMN "isRevoked"; diff --git a/server/src/database/schema.prisma b/server/src/database/schema.prisma index 7ed6da7b7..aacd03681 100644 --- a/server/src/database/schema.prisma +++ b/server/src/database/schema.prisma @@ -331,10 +331,7 @@ model Person { model RefreshToken { /// @Validator.IsString() /// @Validator.IsOptional() - id String @id @default(uuid()) - /// @Validator.IsBoolean() - /// @Validator.IsOptional() - isRevoked Boolean @default(false) + id String @id @default(uuid()) /// @TypeGraphQL.omit(input: true, output: true) user User @relation(fields: [userId], references: [id]) @@ -344,6 +341,8 @@ model RefreshToken { expiresAt DateTime /// @TypeGraphQL.omit(input: true, output: true) deletedAt DateTime? + /// @TypeGraphQL.omit(input: true, output: true) + revokedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt diff --git a/server/src/integrations/environment/environment.service.ts b/server/src/integrations/environment/environment.service.ts index d57606f33..ed19468dd 100644 --- a/server/src/integrations/environment/environment.service.ts +++ b/server/src/integrations/environment/environment.service.ts @@ -61,6 +61,10 @@ export class EnvironmentService { return this.configService.get('REFRESH_TOKEN_EXPIRES_IN') ?? '90d'; } + getRefreshTokenCoolDown(): string { + return this.configService.get('REFRESH_TOKEN_COOL_DOWN') ?? '1m'; + } + getLoginTokenSecret(): string { return this.configService.get('LOGIN_TOKEN_SECRET')!; } diff --git a/server/src/integrations/environment/environment.validation.ts b/server/src/integrations/environment/environment.validation.ts index 956dbad26..5a1135092 100644 --- a/server/src/integrations/environment/environment.validation.ts +++ b/server/src/integrations/environment/environment.validation.ts @@ -76,6 +76,9 @@ export class EnvironmentVariables { @IsDuration() @IsOptional() REFRESH_TOKEN_EXPIRES_IN: string; + @IsDuration() + @IsOptional() + REFRESH_TOKEN_COOL_DOWN: string; @IsString() LOGIN_TOKEN_SECRET: string;