Compare commits

..

No commits in common. "fcd147bca4b70b52e895ec5130f86b59330433c0" and "bbe444ea5f36f4202785fb091cade50603fc1353" have entirely different histories.

9 changed files with 136 additions and 94 deletions

View File

@ -0,0 +1,28 @@
import { Injectable, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { Observable } from 'rxjs';
@Injectable()
export class AccessTokenGuard extends AuthGuard('jwt-access-token') {
public constructor(private readonly reflector: Reflector) {
super();
}
public canActivate(
context: ExecutionContext
): boolean | Promise<boolean> | Observable<boolean> {
// Check if the current route is marked as public
const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
context.getHandler(),
context.getClass(),
]);
// Allow access if the route is public, otherwise defer to the standard JWT authentication mechanism
if (isPublic) {
return true;
}
return super.canActivate(context);
}
}

View File

@ -9,7 +9,6 @@ import {
} from '@nestjs/common';
import { ApiCreatedResponse, ApiTags } from '@nestjs/swagger';
import { Request } from 'express';
import { SessionGuard } from 'src/modules/session/guard';
import { Public } from 'src/shared/decorator';
import { LocalAuthGuard } from '../guard';
@ -47,14 +46,4 @@ export class AuthController {
request.user as LoginResponseDto & { userAgent: string }
);
}
@ApiCreatedResponse({
description: 'User signed out',
})
@HttpCode(HttpStatus.OK)
@UseGuards(SessionGuard)
@Post('logout')
public async logout(@Req() request: Request): Promise<{ success: boolean }> {
return this.authService.logout(request.sessionID);
}
}

View File

@ -6,7 +6,6 @@ import {
Injectable,
} from '@nestjs/common';
import { UserCredentials } from 'src/entities';
import { SessionService } from 'src/modules/session/services/session.service';
import { EncryptionService } from 'src/shared';
import { PasswordConfirmationMailService } from '../../sendgrid-module/services/password-confirmation.mail.service';
@ -21,8 +20,7 @@ export class AuthService {
private readonly userCredentialsRepository: UserCredentialsRepository,
private readonly userDataRepository: UserDataRepository,
private readonly passwordConfirmationMailService: PasswordConfirmationMailService,
private readonly emailVerificationService: EmailVerificationService,
private readonly sessionService: SessionService
private readonly emailVerificationService: EmailVerificationService
) {}
public async signup(
@ -75,6 +73,29 @@ export class AuthService {
}
}
public async signin(userCredentials: UserCredentialsDto): Promise<void> {
// const user = await this.userCredentialsRepository.findUserByEmail(
// userCredentials.email
// );
// if (!user) {
// throw new ForbiddenException('Access Denied');
// }
// const passwordMatch = await EncryptionService.compareHash(
// userCredentials.password,
// user.hash
// );
// if (!passwordMatch) {
// throw new ForbiddenException('Access Denied');
// }
// await this.sessionService.checkSessionLimit(user.id);
// const sesseionId = await this.sessionService.createSession(
// user.id,
// request.headers['user-agent']
// );
// this.sessionService.attachSessionToResponse(response, sesseionId.sessionId);
// return this.generateAndPersistTokens(user.id, user.email, true);
}
public async validateUser(
email: string,
password: string
@ -119,15 +140,84 @@ export class AuthService {
return responseData;
}
public async logout(sessionId: string): Promise<{ success: boolean }> {
try {
this.sessionService.deleteSessionBySessionId(sessionId);
return { success: true };
} catch (error) {
throw new HttpException(
'Fehler beim Logout',
HttpStatus.INTERNAL_SERVER_ERROR
);
}
}
// public async logout(userId: string): Promise<boolean> {
// const affected =
// await this.userCredentialsRepository.updateUserRefreshToken(userId, null);
// await this.sessionService.invalidateAllSessionsForUser(userId);
// return affected > 0;
// }
// public async refresh(request: Request): Promise<AccessTokenDto> {
// const sessionId = request.cookies['session_id'];
// if (!sessionId) {
// throw new ForbiddenException('Session ID missing');
// }
// const session: Session =
// await this.sessionService.findSessionBySessionId(sessionId);
// if (!session) {
// throw new ForbiddenException('Invalid session');
// }
// const isUserAgentValid = await this.sessionService.validateSessionUserAgent(
// sessionId,
// request.headers['user-agent']
// );
// if (!isUserAgentValid) {
// throw new ForbiddenException('Invalid session - User agent mismatch');
// }
// await this.sessionService.extendSessionExpiration(sessionId);
// const decodedToken: TokenPayload = await this.validateRefreshToken(
// session.userCredentials['id']
// );
// const newTokens = await this.generateAndPersistTokens(
// decodedToken.sub,
// decodedToken.email,
// false
// );
// return { access_token: newTokens.access_token };
// }
// private async generateAndPersistTokens(
// userId: string,
// email: string
// ): Promise<LoginResponseDto> {
// const tokens = await this.tokenManagementService.generateTokens(
// userId,
// email
// );
// return { access_token: tokens.access_token, email: email, userId: 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');
// }
// const decodedToken = await this.tokenManagementService.verifyRefreshToken(
// user.refreshToken
// );
// if (decodedToken.exp < Date.now() / 1000) {
// throw new Error('Token expired');
// }
// if (decodedToken.sub !== user.id) {
// throw new Error('Token subject mismatch');
// }
// return decodedToken;
// }
}

View File

@ -1 +0,0 @@
export * from './session.guard';

View File

@ -1,40 +0,0 @@
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { SessionService } from '../services/session.service';
@Injectable()
export class SessionGuard implements CanActivate {
public constructor(private readonly sessionService: SessionService) {}
public async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const sessionId = request.session.id;
const currentAgent = request.headers['user-agent'];
const session = await this.sessionService.findSessionBySessionId(sessionId);
if (!session) {
throw new UnauthorizedException('Session not found.');
}
const isExpired = await this.sessionService.isSessioExpired(session);
if (isExpired) {
throw new UnauthorizedException('Session expired.');
}
const userAgentInSession = JSON.parse(session.json).passport.user
.userAgent as string;
if (userAgentInSession !== currentAgent) {
throw new UnauthorizedException('User agent mismatch.');
}
return true;
}
}

View File

@ -1,5 +1,4 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
import { Session } from 'src/entities';
import { Repository } from 'typeorm';
@ -8,8 +7,7 @@ import { Repository } from 'typeorm';
export class SessionRepository {
public constructor(
@InjectRepository(Session)
private readonly repository: Repository<Session>,
private readonly configService: ConfigService
private readonly repository: Repository<Session>
) {}
public async findSessionsByUserId(userId: string): Promise<Session[]> {
@ -48,16 +46,7 @@ export class SessionRepository {
});
}
public async isSessionExpired(session: Session): Promise<boolean> {
return session.expiredAt < Date.now();
}
public async deleteSessionBySessionId(sessionId: string): Promise<void> {
await this.repository.delete(sessionId);
}
public async enforceSessionLimit(userId: string): Promise<void> {
const sessionLimit = this.configService.get<number>('SESSION_LIMIT');
const sessions = await this.repository
.createQueryBuilder('session')
.withDeleted()
@ -67,11 +56,8 @@ export class SessionRepository {
.orderBy('session.expiredAt', 'ASC')
.getMany();
if (sessions.length > sessionLimit) {
const sessionsToDelete = sessions.slice(
0,
sessions.length - sessionLimit
);
if (sessions.length > 5) {
const sessionsToDelete = sessions.slice(0, sessions.length - 5);
await this.repository.remove(sessionsToDelete);
}

View File

@ -20,14 +20,6 @@ export class SessionService {
return this.sessionRepository.findSessionsByUserId(userId);
}
public async isSessioExpired(session: Session): Promise<boolean> {
return this.sessionRepository.isSessionExpired(session);
}
public async deleteSessionBySessionId(sessionId: string): Promise<void> {
return this.sessionRepository.deleteSessionBySessionId(sessionId);
}
public async findSessionBySessionId(
sessionId: string
): Promise<Session | null> {

View File

@ -2,7 +2,6 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Session } from 'src/entities';
import { SessionGuard } from './guard';
import { SessionRepository } from './repository/session.repository';
import { SessionInitService, SessionSerializerService } from './services';
import { SessionService } from './services/session.service';
@ -13,10 +12,9 @@ import { SessionService } from './services/session.service';
SessionInitService,
SessionSerializerService,
SessionRepository,
SessionGuard,
SessionService,
],
controllers: [],
exports: [SessionService, SessionGuard],
exports: [SessionService],
})
export class SessionModule {}