Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions src/modules/auth/controllers/auth.public.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,8 @@ export class AuthPublicController {

const token = this.authService.createToken(
userWithRole,
session._id
session._id,
session.jti
);

await this.databaseService.commitTransaction(databaseSession);
Expand Down Expand Up @@ -257,7 +258,8 @@ export class AuthPublicController {

const token = this.authService.createToken(
userWithRole,
session._id
session._id,
session.jti
);

return {
Expand Down Expand Up @@ -338,7 +340,8 @@ export class AuthPublicController {

const token = this.authService.createToken(
userWithRole,
session._id
session._id,
session.jti
);

return {
Expand Down
53 changes: 45 additions & 8 deletions src/modules/auth/controllers/auth.shared.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ import {
IAuthJwtAccessTokenPayload,
IAuthJwtRefreshTokenPayload,
} from '@modules/auth/interfaces/auth.interface';
import { v4 as uuidV4 } from 'uuid';
import { SessionJtiProtected } from '@modules/session/decorators/session.jti.decorator';

@ApiTags('modules.shared.auth')
@Controller({
Expand All @@ -69,6 +71,7 @@ export class AuthSharedController {

@AuthSharedRefreshDoc()
@Response('auth.refresh')
@SessionJtiProtected()
@UserProtected()
@AuthJwtRefreshProtected()
@ApiKeyProtected()
Expand All @@ -77,23 +80,57 @@ export class AuthSharedController {
async refresh(
@AuthJwtToken() refreshToken: string,
@AuthJwtPayload<IAuthJwtRefreshTokenPayload>()
{ user: userFromPayload, session }: IAuthJwtRefreshTokenPayload
{
user: userFromPayload,
session: sessionId,
}: IAuthJwtRefreshTokenPayload
): Promise<IResponse<AuthRefreshResponseDto>> {
const checkActive = await this.sessionService.findLoginSession(session);
const checkActive =
await this.sessionService.findLoginSession(sessionId);
if (!checkActive) {
throw new UnauthorizedException({
statusCode: ENUM_SESSION_STATUS_CODE_ERROR.NOT_FOUND,
message: 'session.error.notFound',
});
}

const user: IUserDoc =
await this.userService.findOneActiveById(userFromPayload);
const token = this.authService.refreshToken(user, refreshToken);
const dbSession: ClientSession =
await this.databaseService.createTransaction();
try {
const activeSession = await this.sessionService.findOneActiveById(
sessionId,
{ session: dbSession }
);
const session = await this.sessionService.updateJti(
activeSession,
uuidV4(),
{ session: dbSession }
);

const user: IUserDoc = await this.userService.findOneActiveById(
userFromPayload,
{ session: dbSession }
);
const token = this.authService.refreshToken(
user,
refreshToken,
session.jti
);

await this.databaseService.commitTransaction(dbSession);

return {
data: token,
};
return {
data: token,
};
} catch (err: unknown) {
await this.databaseService.abortTransaction(dbSession);

throw new InternalServerErrorException({
statusCode: ENUM_APP_STATUS_CODE_ERROR.UNKNOWN,
message: 'http.serverError.internalServerError',
_error: err,
});
}
}

@AuthSharedChangePasswordDoc()
Expand Down
1 change: 1 addition & 0 deletions src/modules/auth/interfaces/auth.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export interface IAuthJwtAccessTokenPayload {
termPolicy: IAuthJwtTermPolicyPayload;
verification: IAuthJwtVerificationPayload;
type: ENUM_POLICY_ROLE_TYPE;
jti?: string;
iat?: number;
nbf?: number;
exp?: number;
Expand Down
17 changes: 12 additions & 5 deletions src/modules/auth/interfaces/auth.service.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,23 @@ import { AuthLoginResponseDto } from '@modules/auth/dtos/response/auth.login.res
export interface IAuthService {
createAccessToken(
subject: string,
payload: IAuthJwtAccessTokenPayload
payload: IAuthJwtAccessTokenPayload,
jwtid: string
): string;
validateAccessToken(subject: string, token: string): boolean;
payload<T = any>(token: string): T;
createRefreshToken(
subject: string,
payload: IAuthJwtRefreshTokenPayload
payload: IAuthJwtRefreshTokenPayload,
jti: string
): string;
validateRefreshToken(subject: string, token: string): boolean;
validateUser(passwordString: string, passwordHash: string): boolean;
createPayloadAccessToken(
data: IUserDoc,
session: string,
loginDate: Date,
loginFrom: ENUM_AUTH_LOGIN_FROM
loginFrom: ENUM_AUTH_LOGIN_FROM,
): IAuthJwtAccessTokenPayload;
createPayloadRefreshToken({
user,
Expand All @@ -42,10 +44,15 @@ export interface IAuthService {
): IAuthPassword;
createPasswordRandom(): string;
checkPasswordExpired(passwordExpired: Date): boolean;
createToken(user: IUserDoc, session: string): AuthLoginResponseDto;
createToken(
user: IUserDoc,
session: string,
jti: string
): AuthLoginResponseDto;
refreshToken(
user: IUserDoc,
refreshTokenFromRequest: string
refreshTokenFromRequest: string,
jti: string
): AuthLoginResponseDto;
getPasswordAttempt(): boolean;
getPasswordMaxAttempt(): number;
Expand Down
31 changes: 22 additions & 9 deletions src/modules/auth/services/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import { readFileSync } from 'fs';
import { JwtService, JwtSignOptions } from '@nestjs/jwt';
import { join } from 'path';
import { v4 as uuidV4 } from 'uuid';

Check warning on line 24 in src/modules/auth/services/auth.service.ts

View workflow job for this annotation

GitHub Actions / linter (lts/*)

'uuidV4' is defined but never used. Allowed unused vars must match /^_/u

@Injectable()
export class AuthService implements IAuthService {
Expand Down Expand Up @@ -153,7 +154,8 @@

createAccessToken(
subject: string,
payload: IAuthJwtAccessTokenPayload
payload: IAuthJwtAccessTokenPayload,
jwtid: string
): string {
return this.jwtService.sign(payload, {
privateKey: this.jwtAccessTokenPrivateKey,
Expand All @@ -163,6 +165,7 @@
subject,
algorithm: this.jwtAlgorithm,
keyid: this.jwtAccessTokenKid,
jwtid: jwtid,
} as JwtSignOptions);
}

Expand All @@ -188,7 +191,8 @@

createRefreshToken(
subject: string,
payload: IAuthJwtRefreshTokenPayload
payload: IAuthJwtRefreshTokenPayload,
jti: string
): string {
return this.jwtService.sign(payload, {
privateKey: this.jwtRefreshTokenPrivateKey,
Expand All @@ -198,6 +202,7 @@
subject,
algorithm: this.jwtAlgorithm,
keyid: this.jwtRefreshTokenKid,
jwtid: jti
} as JwtSignOptions);
}

Expand Down Expand Up @@ -228,7 +233,7 @@
data: IUserDoc,
session: string,
loginDate: Date,
loginFrom: ENUM_AUTH_LOGIN_FROM
loginFrom: ENUM_AUTH_LOGIN_FROM,
): IAuthJwtAccessTokenPayload {
return {
user: data._id,
Expand Down Expand Up @@ -306,7 +311,11 @@
return today > passwordExpiredConvert;
}

createToken(user: IUserDoc, session: string): AuthLoginResponseDto {
createToken(
user: IUserDoc,
session: string,
jti: string
): AuthLoginResponseDto {
const loginDate = this.helperDateService.create();
const roleType = user.role.type;

Expand All @@ -315,18 +324,20 @@
user,
session,
loginDate,
ENUM_AUTH_LOGIN_FROM.CREDENTIAL
ENUM_AUTH_LOGIN_FROM.CREDENTIAL,
);
const accessToken: string = this.createAccessToken(
user._id,
payloadAccessToken
payloadAccessToken,
jti
);

const payloadRefreshToken: IAuthJwtRefreshTokenPayload =
this.createPayloadRefreshToken(payloadAccessToken);
const refreshToken: string = this.createRefreshToken(
user._id,
payloadRefreshToken
payloadRefreshToken,
jti
);

return {
Expand All @@ -340,7 +351,8 @@

refreshToken(
user: IUserDoc,
refreshTokenFromRequest: string
refreshTokenFromRequest: string,
jti: string
): AuthLoginResponseDto {
const roleType = user.role.type;

Expand All @@ -356,7 +368,8 @@
);
const accessToken: string = this.createAccessToken(
user._id,
payloadAccessToken
payloadAccessToken,
jti
);

return {
Expand Down
6 changes: 6 additions & 0 deletions src/modules/session/decorators/session.jti.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { applyDecorators, UseGuards } from '@nestjs/common';
import { SessionJtiGuard } from '@modules/session/guards/session.jti.guard';

export function SessionJtiProtected(): MethodDecorator {
return applyDecorators(UseGuards(SessionJtiGuard));
}
1 change: 1 addition & 0 deletions src/modules/session/enums/session.status-code.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export enum ENUM_SESSION_STATUS_CODE_ERROR {
NOT_FOUND = 5070,
EXPIRED = 5071,
FORBIDDEN_REVOKE = 5072,
INVALID_JTI = 5080
}
52 changes: 52 additions & 0 deletions src/modules/session/guards/session.jti.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { SessionService } from '@modules/session/services/session.service';
import { Reflector } from '@nestjs/core';
import { ENUM_SESSION_STATUS_CODE_ERROR } from '@modules/session/enums/session.status-code.enum';
import { IRequestApp } from '@common/request/interfaces/request.interface';
import { IAuthJwtRefreshTokenPayload } from '@modules/auth/interfaces/auth.interface';

@Injectable()
export class SessionJtiGuard implements CanActivate {
constructor(
private readonly sessionService: SessionService,
private readonly reflector: Reflector
) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context
.switchToHttp()
.getRequest<IRequestApp<IAuthJwtRefreshTokenPayload>>();
const user = request.user;

if (!user || !user.session || !user.jti) {
throw new UnauthorizedException({
statusCode: ENUM_SESSION_STATUS_CODE_ERROR.NOT_FOUND,
message: 'session.error.notFound',
});
}

const session = await this.sessionService.findOneActiveById(
user.session
);
if (!session) {
throw new UnauthorizedException({
statusCode: ENUM_SESSION_STATUS_CODE_ERROR.NOT_FOUND,
message: 'session.error.notFound',
});
}

if (session.jti !== user.jti) {
throw new UnauthorizedException({
statusCode: ENUM_SESSION_STATUS_CODE_ERROR.INVALID_JTI,
message: 'session.error.invalidJti',
});
}

return true;
}
}
7 changes: 6 additions & 1 deletion src/modules/session/interfaces/session.service.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
IDatabaseDeleteManyOptions,
IDatabaseFindAllOptions,
IDatabaseGetTotalOptions,
IDatabaseOptions,
IDatabaseOptions, IDatabaseSaveOptions,
IDatabaseUpdateManyOptions,
} from '@common/database/interfaces/database.interface';
import { SessionCreateRequestDto } from '@modules/session/dtos/request/session.create.request.dto';
Expand Down Expand Up @@ -59,6 +59,11 @@ export interface ISessionService {
setLoginSession(user: IUserDoc, session: SessionDoc): Promise<void>;
deleteLoginSession(_id: string): Promise<void>;
resetLoginSession(): Promise<void>;
updateJti(
repository: SessionDoc,
jti: string,
options?: IDatabaseSaveOptions
): Promise<SessionDoc>
updateRevoke(
repository: SessionDoc,
options?: IDatabaseOptions
Expand Down
8 changes: 8 additions & 0 deletions src/modules/session/repository/entities/session.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ export class SessionEntity extends DatabaseUUIDEntityBase {
})
user: string;

@DatabaseProp({
required: true,
index: true,
trim: true,
type: String,
})
jti: string;

@DatabaseProp({
required: true,
trim: true,
Expand Down
Loading