Refactored Auth with Sessions #12
|
@ -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 {
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
);
|
|
@ -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];
|
||||
// }
|
||||
// );
|
|
@ -1,2 +0,0 @@
|
|||
export * from './get-user-id.decorator';
|
||||
// export * from './get-user.decorator';
|
|
@ -1,2 +0,0 @@
|
|||
export * from './access-token.guard';
|
||||
export * from './refresh-token.guard';
|
|
@ -1,7 +0,0 @@
|
|||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
export class RefreshTokenGuard extends AuthGuard('jwt-refresh-token') {
|
||||
public constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
export * from './access-token.strategie';
|
||||
export * from './refresh-token.strategie';
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue