fix: the refresh token flow

This commit is contained in:
mahyargdz
2025-03-07 13:05:05 +03:30
parent 1391deecf0
commit 84317ebe99
3 changed files with 81 additions and 37 deletions
+2
View File
@@ -1,4 +1,6 @@
// export const AUTH_THROTTLE = "AUTH_THROTTLE"; // export const AUTH_THROTTLE = "AUTH_THROTTLE";
export const AUTH_THROTTLE_TTL = 5 * 60 * 1000; export const AUTH_THROTTLE_TTL = 5 * 60 * 1000;
export const AUTH_THROTTLE_LIMIT = 5; export const AUTH_THROTTLE_LIMIT = 5;
export const AUTH__REFRESH_THROTTLE_TTL = 10 * 60 * 1000;
export const AUTH__REFRESH_THROTTLE_LIMIT = 10;
export const JWT_STRATEGY_NAME = "jwt_Strategy"; export const JWT_STRATEGY_NAME = "jwt_Strategy";
+2 -1
View File
@@ -9,7 +9,7 @@ import { RefreshTokenDto } from "./DTO/refresh-token.dto";
import { RequestOtpDto } from "./DTO/request-otp.dto"; import { RequestOtpDto } from "./DTO/request-otp.dto";
import { VerifyOtpDto } from "./DTO/verify-otp.dto"; import { VerifyOtpDto } from "./DTO/verify-otp.dto";
import { AuthService } from "./providers/auth.service"; import { AuthService } from "./providers/auth.service";
import { AUTH_THROTTLE_LIMIT, AUTH_THROTTLE_TTL } from "../../common/constants"; import { AUTH_THROTTLE_LIMIT, AUTH_THROTTLE_TTL, AUTH__REFRESH_THROTTLE_LIMIT, AUTH__REFRESH_THROTTLE_TTL } from "../../common/constants";
import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
import { UserDec } from "../../common/decorators/user.decorator"; import { UserDec } from "../../common/decorators/user.decorator";
@@ -84,6 +84,7 @@ export class AuthController {
return this.authService.changePassword(userId, changePasswordDto); return this.authService.changePassword(userId, changePasswordDto);
} }
@Throttle({ default: { limit: AUTH__REFRESH_THROTTLE_LIMIT, ttl: AUTH__REFRESH_THROTTLE_TTL } })
@ApiOperation({ summary: "refresh the user access token / refresh token" }) @ApiOperation({ summary: "refresh the user access token / refresh token" })
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@Post("refresh") @Post("refresh")
+59 -18
View File
@@ -1,7 +1,8 @@
import { Injectable, UnauthorizedException } from "@nestjs/common"; import { Injectable, Logger, UnauthorizedException } from "@nestjs/common";
import { ConfigService } from "@nestjs/config"; import { ConfigService } from "@nestjs/config";
import { JwtService } from "@nestjs/jwt"; import { JwtService } from "@nestjs/jwt";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { DataSource, QueryRunner } from "typeorm";
import { AuthMessage } from "../../../common/enums/message.enum"; import { AuthMessage } from "../../../common/enums/message.enum";
import { User } from "../../users/entities/user.entity"; import { User } from "../../users/entities/user.entity";
@@ -10,77 +11,117 @@ import { ITokenPayload } from "../interfaces/IToken-payload";
@Injectable() @Injectable()
export class TokensService { export class TokensService {
private readonly logger = new Logger(TokensService.name);
constructor( constructor(
private readonly configService: ConfigService, private readonly configService: ConfigService,
private readonly jwtService: JwtService, private readonly jwtService: JwtService,
private readonly refreshTokensRepository: RefreshTokensRepository, private readonly refreshTokensRepository: RefreshTokensRepository,
private readonly dataSource: DataSource,
) {} ) {}
// ----------- generate token ----------------- // ----------- generate token -----------------
async generateTokens(user: User) { async generateTokens(user: User, queryRunner?: QueryRunner) {
return this.generateAccessAndRefreshToken({ return this.generateAccessAndRefreshToken(
{
id: user.id, id: user.id,
isAdmin: user.roles.some((r) => r.isAdmin), isAdmin: user.roles.some((r) => r.isAdmin),
permissions: user.roles.flatMap((r) => (r?.permissions?.length ? r.permissions.map((p) => p.name) : [])), permissions: user.roles.flatMap((r) => (r?.permissions?.length ? r.permissions.map((p) => p.name) : [])),
}); },
queryRunner,
);
} }
//**************************************************************** */ //**************************************************************** */
private async generateAccessAndRefreshToken(payload: ITokenPayload) { private async generateAccessAndRefreshToken(payload: ITokenPayload, queryRunner?: QueryRunner) {
const accessExpire = this.configService.getOrThrow<number>("ACCESS_TOKEN_EXPIRE"); const accessExpire = this.configService.getOrThrow<number>("ACCESS_TOKEN_EXPIRE");
const refreshExpire = this.configService.getOrThrow<number>("REFRESH_TOKEN_EXPIRE"); const refreshExpire = this.configService.getOrThrow<number>("REFRESH_TOKEN_EXPIRE");
const accessToken = this.jwtService.sign(payload, { expiresIn: `${accessExpire}m` }); const accessToken = this.jwtService.sign(payload, { expiresIn: `${accessExpire}m` });
const refreshToken = this.jwtService.sign(payload, { expiresIn: `${refreshExpire}d` }); const refreshToken = this.jwtService.sign({ id: payload.id }, { expiresIn: `${refreshExpire}d` });
await this.storeRefreshToken(payload.id, refreshToken); await this.storeRefreshToken(payload.id, refreshToken, queryRunner);
return { return {
accessToken: { token: accessToken, expire: Date.now() + 1000 * 60 * 60 * accessExpire }, accessToken: { token: accessToken, expire: dayjs().add(accessExpire, "minute").valueOf() },
refreshToken: { token: refreshToken, expire: Date.now() + 1000 * 60 * 60 * 24 * refreshExpire }, refreshToken: { token: refreshToken, expire: dayjs().add(refreshExpire, "day").valueOf() },
}; };
} }
//**************************************************************** */ //**************************************************************** */
async storeRefreshToken(userId: string, refreshToken: string) { async storeRefreshToken(userId: string, refreshToken: string, queryRunner = this.dataSource.createQueryRunner()) {
let transactionStarted = false;
try {
if (!queryRunner.isTransactionActive) {
await queryRunner.connect();
await queryRunner.startTransaction();
transactionStarted = true;
}
const refreshExpire = this.configService.getOrThrow<number>("REFRESH_TOKEN_EXPIRE"); const refreshExpire = this.configService.getOrThrow<number>("REFRESH_TOKEN_EXPIRE");
const expiresAt = dayjs().add(refreshExpire, "day").toDate(); const expiresAt = dayjs().add(refreshExpire, "day").toDate();
const existingToken = await this.refreshTokensRepository.findOne({ const existingToken = await queryRunner.manager.findOne(this.refreshTokensRepository.target, {
where: { user: { id: userId } }, where: { user: { id: userId } },
}); });
if (existingToken) { if (existingToken) {
existingToken.token = refreshToken; existingToken.token = refreshToken;
existingToken.expiresAt = expiresAt; existingToken.expiresAt = expiresAt;
await this.refreshTokensRepository.save(existingToken); await queryRunner.manager.save(this.refreshTokensRepository.target, existingToken);
//
} else { } else {
const token = this.refreshTokensRepository.create({ const token = queryRunner.manager.create(this.refreshTokensRepository.target, {
token: refreshToken, token: refreshToken,
user: { id: userId }, user: { id: userId },
expiresAt, expiresAt,
}); });
await queryRunner.manager.save(this.refreshTokensRepository.target, token);
}
await this.refreshTokensRepository.save(token); if (transactionStarted) await queryRunner.commitTransaction();
} catch (error) {
this.logger.error(error);
if (transactionStarted) await queryRunner.rollbackTransaction();
throw error;
} finally {
if (transactionStarted) await queryRunner.release();
} }
} }
/************************************************************ */ /************************************************************ */
async refreshToken(oldRefreshToken: string) { async refreshToken(oldRefreshToken: string) {
const token = await this.refreshTokensRepository.findOne({ const queryRunner = this.dataSource.createQueryRunner();
try {
await queryRunner.connect();
await queryRunner.startTransaction();
const token = await queryRunner.manager.findOne(this.refreshTokensRepository.target, {
where: { token: oldRefreshToken }, where: { token: oldRefreshToken },
relations: { user: { roles: { permissions: true } } }, relations: { user: { roles: { permissions: true } } },
}); });
if (!token) throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN); if (!token) throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN);
if (dayjs(token.expiresAt).isBefore(dayjs())) throw new UnauthorizedException(AuthMessage.REFRESH_TOKEN_EXPIRED); if (dayjs(token.expiresAt).isBefore(dayjs())) {
await queryRunner.manager.delete(this.refreshTokensRepository.target, { id: token.id });
throw new UnauthorizedException(AuthMessage.REFRESH_TOKEN_EXPIRED);
}
await this.refreshTokensRepository.delete({ id: token.id }); await queryRunner.manager.delete(this.refreshTokensRepository.target, { id: token.id });
return await this.generateTokens(token.user); const tokens = await this.generateTokens(token.user, queryRunner);
await queryRunner.commitTransaction();
return tokens;
} catch (error) {
this.logger.error(error);
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
}
} }
/************************************************************ */ /************************************************************ */