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_TTL = 5 * 60 * 1000;
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";
+2 -1
View File
@@ -9,7 +9,7 @@ import { RefreshTokenDto } from "./DTO/refresh-token.dto";
import { RequestOtpDto } from "./DTO/request-otp.dto";
import { VerifyOtpDto } from "./DTO/verify-otp.dto";
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 { UserDec } from "../../common/decorators/user.decorator";
@@ -84,6 +84,7 @@ export class AuthController {
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" })
@HttpCode(HttpStatus.OK)
@Post("refresh")
+77 -36
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 { JwtService } from "@nestjs/jwt";
import dayjs from "dayjs";
import { DataSource, QueryRunner } from "typeorm";
import { AuthMessage } from "../../../common/enums/message.enum";
import { User } from "../../users/entities/user.entity";
@@ -10,77 +11,117 @@ import { ITokenPayload } from "../interfaces/IToken-payload";
@Injectable()
export class TokensService {
private readonly logger = new Logger(TokensService.name);
constructor(
private readonly configService: ConfigService,
private readonly jwtService: JwtService,
private readonly refreshTokensRepository: RefreshTokensRepository,
private readonly dataSource: DataSource,
) {}
// ----------- generate token -----------------
async generateTokens(user: User) {
return this.generateAccessAndRefreshToken({
id: user.id,
isAdmin: user.roles.some((r) => r.isAdmin),
permissions: user.roles.flatMap((r) => (r?.permissions?.length ? r.permissions.map((p) => p.name) : [])),
});
async generateTokens(user: User, queryRunner?: QueryRunner) {
return this.generateAccessAndRefreshToken(
{
id: user.id,
isAdmin: user.roles.some((r) => r.isAdmin),
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 refreshExpire = this.configService.getOrThrow<number>("REFRESH_TOKEN_EXPIRE");
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 {
accessToken: { token: accessToken, expire: Date.now() + 1000 * 60 * 60 * accessExpire },
refreshToken: { token: refreshToken, expire: Date.now() + 1000 * 60 * 60 * 24 * refreshExpire },
accessToken: { token: accessToken, expire: dayjs().add(accessExpire, "minute").valueOf() },
refreshToken: { token: refreshToken, expire: dayjs().add(refreshExpire, "day").valueOf() },
};
}
//**************************************************************** */
async storeRefreshToken(userId: string, refreshToken: string) {
const refreshExpire = this.configService.getOrThrow<number>("REFRESH_TOKEN_EXPIRE");
const expiresAt = dayjs().add(refreshExpire, "day").toDate();
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 existingToken = await this.refreshTokensRepository.findOne({
where: { user: { id: userId } },
});
const refreshExpire = this.configService.getOrThrow<number>("REFRESH_TOKEN_EXPIRE");
const expiresAt = dayjs().add(refreshExpire, "day").toDate();
if (existingToken) {
existingToken.token = refreshToken;
existingToken.expiresAt = expiresAt;
await this.refreshTokensRepository.save(existingToken);
} else {
const token = this.refreshTokensRepository.create({
token: refreshToken,
user: { id: userId },
expiresAt,
const existingToken = await queryRunner.manager.findOne(this.refreshTokensRepository.target, {
where: { user: { id: userId } },
});
await this.refreshTokensRepository.save(token);
if (existingToken) {
existingToken.token = refreshToken;
existingToken.expiresAt = expiresAt;
await queryRunner.manager.save(this.refreshTokensRepository.target, existingToken);
//
} else {
const token = queryRunner.manager.create(this.refreshTokensRepository.target, {
token: refreshToken,
user: { id: userId },
expiresAt,
});
await queryRunner.manager.save(this.refreshTokensRepository.target, 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) {
const token = await this.refreshTokensRepository.findOne({
where: { token: oldRefreshToken },
relations: { user: { roles: { permissions: true } } },
});
const queryRunner = this.dataSource.createQueryRunner();
if (!token) throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN);
try {
await queryRunner.connect();
await queryRunner.startTransaction();
if (dayjs(token.expiresAt).isBefore(dayjs())) throw new UnauthorizedException(AuthMessage.REFRESH_TOKEN_EXPIRED);
const token = await queryRunner.manager.findOne(this.refreshTokensRepository.target, {
where: { token: oldRefreshToken },
relations: { user: { roles: { permissions: true } } },
});
await this.refreshTokensRepository.delete({ id: token.id });
if (!token) throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN);
return await this.generateTokens(token.user);
if (dayjs(token.expiresAt).isBefore(dayjs())) {
await queryRunner.manager.delete(this.refreshTokensRepository.target, { id: token.id });
throw new UnauthorizedException(AuthMessage.REFRESH_TOKEN_EXPIRED);
}
await queryRunner.manager.delete(this.refreshTokensRepository.target, { id: token.id });
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();
}
}
/************************************************************ */