feat: add cooldown to refresh token security (#1736)

This commit is contained in:
Jérémy M
2023-09-27 15:03:50 +02:00
committed by GitHub
parent 96865b0fec
commit a4cde44b13
5 changed files with 30 additions and 13 deletions

View File

@ -114,6 +114,7 @@ export class TokenService {
async verifyRefreshToken(refreshToken: string) { async verifyRefreshToken(refreshToken: string) {
const secret = this.environmentService.getRefreshTokenSecret(); const secret = this.environmentService.getRefreshTokenSecret();
const coolDown = this.environmentService.getRefreshTokenCoolDown();
const jwtPayload = await this.verifyJwt(refreshToken, secret); const jwtPayload = await this.verifyJwt(refreshToken, secret);
assert( assert(
@ -139,7 +140,11 @@ export class TokenService {
assert(user, 'User not found', NotFoundException); 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 // Revoke all user refresh tokens
await this.prismaService.client.refreshToken.updateMany({ await this.prismaService.client.refreshToken.updateMany({
where: { where: {
@ -148,16 +153,14 @@ export class TokenService {
}, },
}, },
data: { data: {
isRevoked: true, revokedAt: new Date(),
}, },
}); });
}
assert( throw new ForbiddenException(
!token.isRevoked, 'Suspicious activity detected, this refresh token has been revoked. All tokens has been revoked.',
'Suspicious activity detected, this refresh token has been revoked. All tokens has been revoked.', );
ForbiddenException, }
);
return { user, token }; return { user, token };
} }
@ -177,7 +180,7 @@ export class TokenService {
id, id,
}, },
data: { data: {
isRevoked: true, revokedAt: new Date(),
}, },
}); });

View File

@ -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";

View File

@ -331,10 +331,7 @@ model Person {
model RefreshToken { model RefreshToken {
/// @Validator.IsString() /// @Validator.IsString()
/// @Validator.IsOptional() /// @Validator.IsOptional()
id String @id @default(uuid()) id String @id @default(uuid())
/// @Validator.IsBoolean()
/// @Validator.IsOptional()
isRevoked Boolean @default(false)
/// @TypeGraphQL.omit(input: true, output: true) /// @TypeGraphQL.omit(input: true, output: true)
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])
@ -344,6 +341,8 @@ model RefreshToken {
expiresAt DateTime expiresAt DateTime
/// @TypeGraphQL.omit(input: true, output: true) /// @TypeGraphQL.omit(input: true, output: true)
deletedAt DateTime? deletedAt DateTime?
/// @TypeGraphQL.omit(input: true, output: true)
revokedAt DateTime?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt

View File

@ -61,6 +61,10 @@ export class EnvironmentService {
return this.configService.get<string>('REFRESH_TOKEN_EXPIRES_IN') ?? '90d'; return this.configService.get<string>('REFRESH_TOKEN_EXPIRES_IN') ?? '90d';
} }
getRefreshTokenCoolDown(): string {
return this.configService.get<string>('REFRESH_TOKEN_COOL_DOWN') ?? '1m';
}
getLoginTokenSecret(): string { getLoginTokenSecret(): string {
return this.configService.get<string>('LOGIN_TOKEN_SECRET')!; return this.configService.get<string>('LOGIN_TOKEN_SECRET')!;
} }

View File

@ -76,6 +76,9 @@ export class EnvironmentVariables {
@IsDuration() @IsDuration()
@IsOptional() @IsOptional()
REFRESH_TOKEN_EXPIRES_IN: string; REFRESH_TOKEN_EXPIRES_IN: string;
@IsDuration()
@IsOptional()
REFRESH_TOKEN_COOL_DOWN: string;
@IsString() @IsString()
LOGIN_TOKEN_SECRET: string; LOGIN_TOKEN_SECRET: string;