Refactored Auth with Sessions #12
|
@ -1,28 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ApiCreatedResponse, ApiTags } from '@nestjs/swagger';
|
import { ApiCreatedResponse, ApiTags } from '@nestjs/swagger';
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
|
import { SessionGuard } from 'src/modules/session/guard';
|
||||||
import { Public } from 'src/shared/decorator';
|
import { Public } from 'src/shared/decorator';
|
||||||
|
|
||||||
import { LocalAuthGuard } from '../guard';
|
import { LocalAuthGuard } from '../guard';
|
||||||
|
@ -46,4 +47,10 @@ export class AuthController {
|
||||||
request.user as LoginResponseDto & { userAgent: string }
|
request.user as LoginResponseDto & { userAgent: string }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(SessionGuard)
|
||||||
|
@Post('logout')
|
||||||
|
public async logout(@Req() request: Request): Promise<void> {
|
||||||
|
this.authService.logout(request.sessionID);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
Injectable,
|
Injectable,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { UserCredentials } from 'src/entities';
|
import { UserCredentials } from 'src/entities';
|
||||||
|
import { SessionService } from 'src/modules/session/services/session.service';
|
||||||
import { EncryptionService } from 'src/shared';
|
import { EncryptionService } from 'src/shared';
|
||||||
|
|
||||||
import { PasswordConfirmationMailService } from '../../sendgrid-module/services/password-confirmation.mail.service';
|
import { PasswordConfirmationMailService } from '../../sendgrid-module/services/password-confirmation.mail.service';
|
||||||
|
@ -20,7 +21,8 @@ export class AuthService {
|
||||||
private readonly userCredentialsRepository: UserCredentialsRepository,
|
private readonly userCredentialsRepository: UserCredentialsRepository,
|
||||||
private readonly userDataRepository: UserDataRepository,
|
private readonly userDataRepository: UserDataRepository,
|
||||||
private readonly passwordConfirmationMailService: PasswordConfirmationMailService,
|
private readonly passwordConfirmationMailService: PasswordConfirmationMailService,
|
||||||
private readonly emailVerificationService: EmailVerificationService
|
private readonly emailVerificationService: EmailVerificationService,
|
||||||
|
private readonly sessionService: SessionService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async signup(
|
public async signup(
|
||||||
|
@ -73,29 +75,6 @@ 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(
|
public async validateUser(
|
||||||
email: string,
|
email: string,
|
||||||
password: string
|
password: string
|
||||||
|
@ -140,84 +119,14 @@ export class AuthService {
|
||||||
return responseData;
|
return responseData;
|
||||||
}
|
}
|
||||||
|
|
||||||
// public async logout(userId: string): Promise<boolean> {
|
public async logout(sessionId: string): Promise<void> {
|
||||||
// const affected =
|
try {
|
||||||
// await this.userCredentialsRepository.updateUserRefreshToken(userId, null);
|
this.sessionService.deleteSessionBySessionId(sessionId);
|
||||||
|
} catch (error) {
|
||||||
// await this.sessionService.invalidateAllSessionsForUser(userId);
|
throw new HttpException(
|
||||||
|
'Fehler beim Logout',
|
||||||
// return affected > 0;
|
HttpStatus.INTERNAL_SERVER_ERROR
|
||||||
// }
|
);
|
||||||
|
}
|
||||||
// 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;
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './session.guard';
|
|
@ -0,0 +1,40 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Session } from 'src/entities';
|
import { Session } from 'src/entities';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
@ -7,7 +8,8 @@ import { Repository } from 'typeorm';
|
||||||
export class SessionRepository {
|
export class SessionRepository {
|
||||||
public constructor(
|
public constructor(
|
||||||
@InjectRepository(Session)
|
@InjectRepository(Session)
|
||||||
private readonly repository: Repository<Session>
|
private readonly repository: Repository<Session>,
|
||||||
|
private readonly configService: ConfigService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async findSessionsByUserId(userId: string): Promise<Session[]> {
|
public async findSessionsByUserId(userId: string): Promise<Session[]> {
|
||||||
|
@ -46,7 +48,16 @@ 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> {
|
public async enforceSessionLimit(userId: string): Promise<void> {
|
||||||
|
const sessionLimit = this.configService.get<number>('SESSION_LIMIT');
|
||||||
const sessions = await this.repository
|
const sessions = await this.repository
|
||||||
.createQueryBuilder('session')
|
.createQueryBuilder('session')
|
||||||
.withDeleted()
|
.withDeleted()
|
||||||
|
@ -56,8 +67,11 @@ export class SessionRepository {
|
||||||
.orderBy('session.expiredAt', 'ASC')
|
.orderBy('session.expiredAt', 'ASC')
|
||||||
.getMany();
|
.getMany();
|
||||||
|
|
||||||
if (sessions.length > 5) {
|
if (sessions.length > sessionLimit) {
|
||||||
const sessionsToDelete = sessions.slice(0, sessions.length - 5);
|
const sessionsToDelete = sessions.slice(
|
||||||
|
0,
|
||||||
|
sessions.length - sessionLimit
|
||||||
|
);
|
||||||
|
|
||||||
await this.repository.remove(sessionsToDelete);
|
await this.repository.remove(sessionsToDelete);
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,14 @@ export class SessionService {
|
||||||
return this.sessionRepository.findSessionsByUserId(userId);
|
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(
|
public async findSessionBySessionId(
|
||||||
sessionId: string
|
sessionId: string
|
||||||
): Promise<Session | null> {
|
): Promise<Session | null> {
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { Session } from 'src/entities';
|
import { Session } from 'src/entities';
|
||||||
|
|
||||||
|
import { SessionGuard } from './guard';
|
||||||
import { SessionRepository } from './repository/session.repository';
|
import { SessionRepository } from './repository/session.repository';
|
||||||
import { SessionInitService, SessionSerializerService } from './services';
|
import { SessionInitService, SessionSerializerService } from './services';
|
||||||
import { SessionService } from './services/session.service';
|
import { SessionService } from './services/session.service';
|
||||||
|
@ -12,9 +13,10 @@ import { SessionService } from './services/session.service';
|
||||||
SessionInitService,
|
SessionInitService,
|
||||||
SessionSerializerService,
|
SessionSerializerService,
|
||||||
SessionRepository,
|
SessionRepository,
|
||||||
|
SessionGuard,
|
||||||
SessionService,
|
SessionService,
|
||||||
],
|
],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
exports: [SessionService],
|
exports: [SessionService, SessionGuard],
|
||||||
})
|
})
|
||||||
export class SessionModule {}
|
export class SessionModule {}
|
||||||
|
|
Loading…
Reference in New Issue