This commit is contained in:
Igor Hrenowitsch Propisnov 2024-06-03 09:06:55 +02:00
parent 7ef46af4f3
commit 9993c63a56
14 changed files with 145 additions and 264 deletions

View File

@ -10,7 +10,6 @@ import { CspMiddleware } from './middleware/csp-middleware/csp.middleware';
import { HttpsRedirectMiddleware } from './middleware/https-middlware/https-redirect.middleware';
import { SecurityHeadersMiddleware } from './middleware/security-middleware/security.middleware';
import { AuthModule } from './modules/auth-module/auth.module';
import { AccessTokenGuard } from './modules/auth-module/common/guards';
import { DatabaseModule } from './modules/database-module/database.module';
import { SendgridModule } from './modules/sendgrid-module/sendgrid.module';
import { UserModule } from './modules/user-module/user.module';
@ -29,11 +28,7 @@ import { VerifyModule } from './modules/verify-module/verify.module';
VerifyModule,
],
controllers: [AppController],
providers: [
AppService,
{ provide: 'APP_GUARD', useClass: AccessTokenGuard },
ClearExpiredSessionsCron,
],
providers: [AppService, ClearExpiredSessionsCron],
})
export class AppModule {
public configure(consumer: MiddlewareConsumer): void {

View File

@ -13,7 +13,6 @@ import { UserCredentialsRepository } from './repositories/user-credentials.repos
import { AuthService } from './services/auth.service';
import { SessionService } from './services/session.service';
import { TokenManagementService } from './services/token-management.service';
import { AccessTokenStrategy, RefreshTokenStrategy } from './strategies';
@Module({
imports: [
@ -29,8 +28,6 @@ import { AccessTokenStrategy, RefreshTokenStrategy } from './strategies';
TokenManagementService,
UserCredentialsRepository,
SessionRepository,
AccessTokenStrategy,
RefreshTokenStrategy,
],
controllers: [AuthController],
exports: [SessionService],

View File

@ -1,11 +0,0 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { JwtPayload } from 'src/modules/auth-module/models/types';
export const GetCurrentUserId = createParamDecorator(
(_: undefined, context: ExecutionContext): number => {
const request = context.switchToHttp().getRequest();
const user = request.user as JwtPayload;
return user.sub;
}
);

View File

@ -1,13 +0,0 @@
//import { JwtPayloadWithRefreshToken } from 'src/modules/auth-module/models/types';
// export const GetCurrentUser = createParamDecorator(
// (
// data: keyof JwtPayloadWithRefreshToken | undefined,
// context: ExecutionContext
// ) => {
// const request = context.switchToHttp().getRequest();
// if (!data) return request.user;
// return request.user[data];
// }
// );

View File

@ -1,2 +0,0 @@
export * from './get-user-id.decorator';
// export * from './get-user.decorator';

View File

@ -1,2 +0,0 @@
export * from './access-token.guard';
export * from './refresh-token.guard';

View File

@ -1,7 +0,0 @@
import { AuthGuard } from '@nestjs/passport';
export class RefreshTokenGuard extends AuthGuard('jwt-refresh-token') {
public constructor() {
super();
}
}

View File

@ -1,22 +1,8 @@
import {
Controller,
Post,
Body,
HttpCode,
HttpStatus,
Res,
Req,
} from '@nestjs/common';
import { Controller, Post, Body, HttpCode, HttpStatus } from '@nestjs/common';
import { ApiCreatedResponse, ApiTags } from '@nestjs/swagger';
import { Response, Request } from 'express';
import { Public } from 'src/shared/decorator';
import { GetCurrentUserId } from '../common/decorators';
import {
AccessTokenDto,
LoginResponseDto,
UserCredentialsDto,
} from '../models/dto';
import { LoginResponseDto, UserCredentialsDto } from '../models/dto';
import { AuthService } from '../services/auth.service';
@ApiTags('Authentication')
@ -37,39 +23,39 @@ export class AuthController {
return this.authService.signup(userCredentials);
}
@ApiCreatedResponse({
description: 'User signin successfully',
type: LoginResponseDto,
})
@HttpCode(HttpStatus.OK)
@Public()
@Post('signin')
public async signin(
@Res({ passthrough: true }) response: Response,
@Req() request: Request,
@Body() userCredentials: UserCredentialsDto
): Promise<LoginResponseDto> {
return await this.authService.signin(userCredentials, response, request);
}
// @ApiCreatedResponse({
// description: 'User signin successfully',
// type: LoginResponseDto,
// })
// @HttpCode(HttpStatus.OK)
// @Public()
// @Post('signin')
// public async signin(
// @Res({ passthrough: true }) response: Response,
// @Req() request: Request,
// @Body() userCredentials: UserCredentialsDto
// ): Promise<LoginResponseDto> {
// return await this.authService.signin(userCredentials, response, request);
// }
@ApiCreatedResponse({
description: 'User tokens refreshed successfully',
type: AccessTokenDto,
})
@HttpCode(HttpStatus.OK)
@Public()
@Post('refresh')
public async refreshToken(@Req() request: Request): Promise<AccessTokenDto> {
return await this.authService.refresh(request);
}
// @ApiCreatedResponse({
// description: 'User tokens refreshed successfully',
// type: AccessTokenDto,
// })
// @HttpCode(HttpStatus.OK)
// @Public()
// @Post('refresh')
// public async refreshToken(@Req() request: Request): Promise<AccessTokenDto> {
// return await this.authService.refresh(request);
// }
@ApiCreatedResponse({
description: 'User signed out successfully',
type: Boolean,
})
@HttpCode(HttpStatus.OK)
@Post('logout')
public async logout(@GetCurrentUserId() userId: string): Promise<boolean> {
return this.authService.logout(userId);
}
// @ApiCreatedResponse({
// description: 'User signed out successfully',
// type: Boolean,
// })
// @HttpCode(HttpStatus.OK)
// @Post('logout')
// public async logout(@GetCurrentUserId() userId: string): Promise<boolean> {
// return this.authService.logout(userId);
// }
}

View File

@ -35,6 +35,15 @@ export class SessionRepository {
});
}
public findSessionByUserId(userId: string): Promise<Session[]> {
return this.sessionRepository
.createQueryBuilder('session')
.where('session.userCredentialsId = :userId', {
userId,
})
.getMany();
}
public attachSessionToResponse(response: Response, sessionId: string): void {
response.cookie('session_id', sessionId, {
httpOnly: true,

View File

@ -1,17 +1,10 @@
import { ForbiddenException, Injectable } from '@nestjs/common';
import { Response, Request } from 'express';
import { Session } from 'src/entities';
import { Injectable } from '@nestjs/common';
import { EncryptionService } from 'src/shared';
import { PasswordConfirmationMailService } from '../../sendgrid-module/services/password-confirmation.mail.service';
import { UserDataRepository } from '../../user-module/repositories/user-data.repository';
import { EmailVerificationService } from '../../verify-module/services/email-verification.service';
import {
AccessTokenDto,
LoginResponseDto,
UserCredentialsDto,
} from '../models/dto';
import { TokenPayload } from '../models/types';
import { LoginResponseDto, UserCredentialsDto } from '../models/dto';
import { UserCredentialsRepository } from '../repositories/user-credentials.repository';
import { SessionService } from './session.service';
@ -52,129 +45,129 @@ export class AuthService {
token
);
return this.generateAndPersistTokens(user.id, user.email);
//return this.generateAndPersistTokens(user.id, user.email);
}
public async signin(
userCredentials: UserCredentialsDto,
response: Response,
request: Request
): Promise<LoginResponseDto> {
const user = await this.userCredentialsRepository.findUserByEmail(
userCredentials.email
);
// public async signin(
// userCredentials: UserCredentialsDto,
// response: Response,
// request: Request
// ): Promise<LoginResponseDto> {
// const user = await this.userCredentialsRepository.findUserByEmail(
// userCredentials.email
// );
if (!user) {
throw new ForbiddenException('Access Denied');
}
// if (!user) {
// throw new ForbiddenException('Access Denied');
// }
const passwordMatch = await EncryptionService.compareHash(
userCredentials.password,
user.hash
);
// const passwordMatch = await EncryptionService.compareHash(
// userCredentials.password,
// user.hash
// );
if (!passwordMatch) {
throw new ForbiddenException('Access Denied');
}
// if (!passwordMatch) {
// throw new ForbiddenException('Access Denied');
// }
await this.sessionService.checkSessionLimit(user.id);
// await this.sessionService.checkSessionLimit(user.id);
const sesseionId = await this.sessionService.createSession(
user.id,
request.headers['user-agent']
);
// const sesseionId = await this.sessionService.createSession(
// user.id,
// request.headers['user-agent']
// );
this.sessionService.attachSessionToResponse(response, sesseionId.sessionId);
// this.sessionService.attachSessionToResponse(response, sesseionId.sessionId);
return this.generateAndPersistTokens(user.id, user.email, true);
}
// return this.generateAndPersistTokens(user.id, user.email, true);
// }
public async logout(userId: string): Promise<boolean> {
const affected =
await this.userCredentialsRepository.updateUserRefreshToken(userId, null);
// public async logout(userId: string): Promise<boolean> {
// const affected =
// await this.userCredentialsRepository.updateUserRefreshToken(userId, null);
await this.sessionService.invalidateAllSessionsForUser(userId);
// await this.sessionService.invalidateAllSessionsForUser(userId);
return affected > 0;
}
// return affected > 0;
// }
public async refresh(request: Request): Promise<AccessTokenDto> {
const sessionId = request.cookies['session_id'];
// public async refresh(request: Request): Promise<AccessTokenDto> {
// const sessionId = request.cookies['session_id'];
if (!sessionId) {
throw new ForbiddenException('Session ID missing');
}
// if (!sessionId) {
// throw new ForbiddenException('Session ID missing');
// }
const session: Session =
await this.sessionService.findSessionBySessionId(sessionId);
// const session: Session =
// await this.sessionService.findSessionBySessionId(sessionId);
if (!session) {
throw new ForbiddenException('Invalid session');
}
// if (!session) {
// throw new ForbiddenException('Invalid session');
// }
const isUserAgentValid = await this.sessionService.validateSessionUserAgent(
sessionId,
request.headers['user-agent']
);
// const isUserAgentValid = await this.sessionService.validateSessionUserAgent(
// sessionId,
// request.headers['user-agent']
// );
if (!isUserAgentValid) {
throw new ForbiddenException('Invalid session - User agent mismatch');
}
// if (!isUserAgentValid) {
// throw new ForbiddenException('Invalid session - User agent mismatch');
// }
await this.sessionService.extendSessionExpiration(sessionId);
// await this.sessionService.extendSessionExpiration(sessionId);
const decodedToken: TokenPayload = await this.validateRefreshToken(
session.userCredentials['id']
);
// const decodedToken: TokenPayload = await this.validateRefreshToken(
// session.userCredentials['id']
// );
const newTokens = await this.generateAndPersistTokens(
decodedToken.sub,
decodedToken.email,
false
);
// const newTokens = await this.generateAndPersistTokens(
// decodedToken.sub,
// decodedToken.email,
// false
// );
return { access_token: newTokens.access_token };
}
// return { access_token: newTokens.access_token };
// }
private async generateAndPersistTokens(
userId: string,
email: string,
updateRefreshToken: boolean = false
): Promise<LoginResponseDto> {
const tokens = await this.tokenManagementService.generateTokens(
userId,
email
);
// private async generateAndPersistTokens(
// userId: string,
// email: string,
// updateRefreshToken: boolean = false
// ): Promise<LoginResponseDto> {
// const tokens = await this.tokenManagementService.generateTokens(
// userId,
// email
// );
if (updateRefreshToken) {
await this.userCredentialsRepository.updateUserRefreshToken(
userId,
tokens.refresh_token
);
}
// if (updateRefreshToken) {
// await this.userCredentialsRepository.updateUserRefreshToken(
// userId,
// tokens.refresh_token
// );
// }
return { access_token: tokens.access_token, email: email, userId: userId };
}
// return { access_token: tokens.access_token, email: email, userId: userId };
// }
private async validateRefreshToken(userId: string): Promise<TokenPayload> {
const user = await this.userCredentialsRepository.findUserById(userId);
// private async validateRefreshToken(userId: string): Promise<TokenPayload> {
// const user = await this.userCredentialsRepository.findUserById(userId);
if (!user || !user.refreshToken) {
throw new Error('No refresh token found');
}
// if (!user || !user.refreshToken) {
// throw new Error('No refresh token found');
// }
const decodedToken = await this.tokenManagementService.verifyRefreshToken(
user.refreshToken
);
// const decodedToken = await this.tokenManagementService.verifyRefreshToken(
// user.refreshToken
// );
if (decodedToken.exp < Date.now() / 1000) {
throw new Error('Token expired');
}
// if (decodedToken.exp < Date.now() / 1000) {
// throw new Error('Token expired');
// }
if (decodedToken.sub !== user.id) {
throw new Error('Token subject mismatch');
}
// if (decodedToken.sub !== user.id) {
// throw new Error('Token subject mismatch');
// }
return decodedToken;
}
// return decodedToken;
// }
}

View File

@ -43,6 +43,10 @@ export class SessionService {
return this.sessionRepository.findSessionBySessionId(sessionId);
}
public findSessionByUserId(userId: string): Promise<Session[]> {
return this.sessionRepository.findSessionByUserId(userId);
}
public attachSessionToResponse(response: Response, sessionId: string): void {
this.sessionRepository.attachSessionToResponse(response, sessionId);
}

View File

@ -1,27 +0,0 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, ExtractJwt } from 'passport-jwt';
import { JwtPayload } from '../models/types';
@Injectable()
export class AccessTokenStrategy extends PassportStrategy(
Strategy,
'jwt-access-token'
) {
public constructor(private readonly configService: ConfigService) {
super(AccessTokenStrategy.getJwtConfig(configService));
}
private static getJwtConfig(configService: ConfigService): any {
return {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: configService.get<string>('JWT_SECRET_AT'),
};
}
public async validate(payload: JwtPayload): Promise<JwtPayload> {
return payload;
}
}

View File

@ -1,2 +0,0 @@
export * from './access-token.strategie';
export * from './refresh-token.strategie';

View File

@ -1,39 +0,0 @@
import { Injectable, ForbiddenException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { Request } from 'express';
import { Strategy, ExtractJwt } from 'passport-jwt';
@Injectable()
export class RefreshTokenStrategy extends PassportStrategy(
Strategy,
'jwt-refresh-token'
) {
public constructor(private readonly configService: ConfigService) {
super(RefreshTokenStrategy.createJwtStrategyOptions(configService));
}
private static createJwtStrategyOptions(configService: ConfigService): any {
return {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: configService.get<string>('JWT_SECRET_RT'),
passReqToCallback: true,
};
}
public async validate(req: Request, payload: any) {
const refresh_token: string = req
?.get('authorization')
?.replace('Bearer', '')
.trim();
if (!refresh_token) {
throw new ForbiddenException('Refresh token malformed');
}
return {
...payload,
refresh_token,
};
}
}