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 { HttpsRedirectMiddleware } from './middleware/https-middlware/https-redirect.middleware';
|
||||||
import { SecurityHeadersMiddleware } from './middleware/security-middleware/security.middleware';
|
import { SecurityHeadersMiddleware } from './middleware/security-middleware/security.middleware';
|
||||||
import { AuthModule } from './modules/auth-module/auth.module';
|
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 { DatabaseModule } from './modules/database-module/database.module';
|
||||||
import { SendgridModule } from './modules/sendgrid-module/sendgrid.module';
|
import { SendgridModule } from './modules/sendgrid-module/sendgrid.module';
|
||||||
import { UserModule } from './modules/user-module/user.module';
|
import { UserModule } from './modules/user-module/user.module';
|
||||||
|
@ -29,11 +28,7 @@ import { VerifyModule } from './modules/verify-module/verify.module';
|
||||||
VerifyModule,
|
VerifyModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [
|
providers: [AppService, ClearExpiredSessionsCron],
|
||||||
AppService,
|
|
||||||
{ provide: 'APP_GUARD', useClass: AccessTokenGuard },
|
|
||||||
ClearExpiredSessionsCron,
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export class AppModule {
|
export class AppModule {
|
||||||
public configure(consumer: MiddlewareConsumer): void {
|
public configure(consumer: MiddlewareConsumer): void {
|
||||||
|
|
|
@ -13,7 +13,6 @@ import { UserCredentialsRepository } from './repositories/user-credentials.repos
|
||||||
import { AuthService } from './services/auth.service';
|
import { AuthService } from './services/auth.service';
|
||||||
import { SessionService } from './services/session.service';
|
import { SessionService } from './services/session.service';
|
||||||
import { TokenManagementService } from './services/token-management.service';
|
import { TokenManagementService } from './services/token-management.service';
|
||||||
import { AccessTokenStrategy, RefreshTokenStrategy } from './strategies';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -29,8 +28,6 @@ import { AccessTokenStrategy, RefreshTokenStrategy } from './strategies';
|
||||||
TokenManagementService,
|
TokenManagementService,
|
||||||
UserCredentialsRepository,
|
UserCredentialsRepository,
|
||||||
SessionRepository,
|
SessionRepository,
|
||||||
AccessTokenStrategy,
|
|
||||||
RefreshTokenStrategy,
|
|
||||||
],
|
],
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
exports: [SessionService],
|
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 {
|
import { Controller, Post, Body, HttpCode, HttpStatus } from '@nestjs/common';
|
||||||
Controller,
|
|
||||||
Post,
|
|
||||||
Body,
|
|
||||||
HttpCode,
|
|
||||||
HttpStatus,
|
|
||||||
Res,
|
|
||||||
Req,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { ApiCreatedResponse, ApiTags } from '@nestjs/swagger';
|
import { ApiCreatedResponse, ApiTags } from '@nestjs/swagger';
|
||||||
import { Response, Request } from 'express';
|
|
||||||
import { Public } from 'src/shared/decorator';
|
import { Public } from 'src/shared/decorator';
|
||||||
|
|
||||||
import { GetCurrentUserId } from '../common/decorators';
|
import { LoginResponseDto, UserCredentialsDto } from '../models/dto';
|
||||||
import {
|
|
||||||
AccessTokenDto,
|
|
||||||
LoginResponseDto,
|
|
||||||
UserCredentialsDto,
|
|
||||||
} from '../models/dto';
|
|
||||||
import { AuthService } from '../services/auth.service';
|
import { AuthService } from '../services/auth.service';
|
||||||
|
|
||||||
@ApiTags('Authentication')
|
@ApiTags('Authentication')
|
||||||
|
@ -37,39 +23,39 @@ export class AuthController {
|
||||||
return this.authService.signup(userCredentials);
|
return this.authService.signup(userCredentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiCreatedResponse({
|
// @ApiCreatedResponse({
|
||||||
description: 'User signin successfully',
|
// description: 'User signin successfully',
|
||||||
type: LoginResponseDto,
|
// type: LoginResponseDto,
|
||||||
})
|
// })
|
||||||
@HttpCode(HttpStatus.OK)
|
// @HttpCode(HttpStatus.OK)
|
||||||
@Public()
|
// @Public()
|
||||||
@Post('signin')
|
// @Post('signin')
|
||||||
public async signin(
|
// public async signin(
|
||||||
@Res({ passthrough: true }) response: Response,
|
// @Res({ passthrough: true }) response: Response,
|
||||||
@Req() request: Request,
|
// @Req() request: Request,
|
||||||
@Body() userCredentials: UserCredentialsDto
|
// @Body() userCredentials: UserCredentialsDto
|
||||||
): Promise<LoginResponseDto> {
|
// ): Promise<LoginResponseDto> {
|
||||||
return await this.authService.signin(userCredentials, response, request);
|
// return await this.authService.signin(userCredentials, response, request);
|
||||||
}
|
// }
|
||||||
|
|
||||||
@ApiCreatedResponse({
|
// @ApiCreatedResponse({
|
||||||
description: 'User tokens refreshed successfully',
|
// description: 'User tokens refreshed successfully',
|
||||||
type: AccessTokenDto,
|
// type: AccessTokenDto,
|
||||||
})
|
// })
|
||||||
@HttpCode(HttpStatus.OK)
|
// @HttpCode(HttpStatus.OK)
|
||||||
@Public()
|
// @Public()
|
||||||
@Post('refresh')
|
// @Post('refresh')
|
||||||
public async refreshToken(@Req() request: Request): Promise<AccessTokenDto> {
|
// public async refreshToken(@Req() request: Request): Promise<AccessTokenDto> {
|
||||||
return await this.authService.refresh(request);
|
// return await this.authService.refresh(request);
|
||||||
}
|
// }
|
||||||
|
|
||||||
@ApiCreatedResponse({
|
// @ApiCreatedResponse({
|
||||||
description: 'User signed out successfully',
|
// description: 'User signed out successfully',
|
||||||
type: Boolean,
|
// type: Boolean,
|
||||||
})
|
// })
|
||||||
@HttpCode(HttpStatus.OK)
|
// @HttpCode(HttpStatus.OK)
|
||||||
@Post('logout')
|
// @Post('logout')
|
||||||
public async logout(@GetCurrentUserId() userId: string): Promise<boolean> {
|
// public async logout(@GetCurrentUserId() userId: string): Promise<boolean> {
|
||||||
return this.authService.logout(userId);
|
// 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 {
|
public attachSessionToResponse(response: Response, sessionId: string): void {
|
||||||
response.cookie('session_id', sessionId, {
|
response.cookie('session_id', sessionId, {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
|
|
|
@ -1,17 +1,10 @@
|
||||||
import { ForbiddenException, Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Response, Request } from 'express';
|
|
||||||
import { Session } from 'src/entities';
|
|
||||||
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';
|
||||||
import { UserDataRepository } from '../../user-module/repositories/user-data.repository';
|
import { UserDataRepository } from '../../user-module/repositories/user-data.repository';
|
||||||
import { EmailVerificationService } from '../../verify-module/services/email-verification.service';
|
import { EmailVerificationService } from '../../verify-module/services/email-verification.service';
|
||||||
import {
|
import { LoginResponseDto, UserCredentialsDto } from '../models/dto';
|
||||||
AccessTokenDto,
|
|
||||||
LoginResponseDto,
|
|
||||||
UserCredentialsDto,
|
|
||||||
} from '../models/dto';
|
|
||||||
import { TokenPayload } from '../models/types';
|
|
||||||
import { UserCredentialsRepository } from '../repositories/user-credentials.repository';
|
import { UserCredentialsRepository } from '../repositories/user-credentials.repository';
|
||||||
|
|
||||||
import { SessionService } from './session.service';
|
import { SessionService } from './session.service';
|
||||||
|
@ -52,129 +45,129 @@ export class AuthService {
|
||||||
token
|
token
|
||||||
);
|
);
|
||||||
|
|
||||||
return this.generateAndPersistTokens(user.id, user.email);
|
//return this.generateAndPersistTokens(user.id, user.email);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async signin(
|
// public async signin(
|
||||||
userCredentials: UserCredentialsDto,
|
// userCredentials: UserCredentialsDto,
|
||||||
response: Response,
|
// response: Response,
|
||||||
request: Request
|
// request: Request
|
||||||
): Promise<LoginResponseDto> {
|
// ): Promise<LoginResponseDto> {
|
||||||
const user = await this.userCredentialsRepository.findUserByEmail(
|
// const user = await this.userCredentialsRepository.findUserByEmail(
|
||||||
userCredentials.email
|
// userCredentials.email
|
||||||
);
|
// );
|
||||||
|
|
||||||
if (!user) {
|
// if (!user) {
|
||||||
throw new ForbiddenException('Access Denied');
|
// throw new ForbiddenException('Access Denied');
|
||||||
}
|
// }
|
||||||
|
|
||||||
const passwordMatch = await EncryptionService.compareHash(
|
// const passwordMatch = await EncryptionService.compareHash(
|
||||||
userCredentials.password,
|
// userCredentials.password,
|
||||||
user.hash
|
// user.hash
|
||||||
);
|
// );
|
||||||
|
|
||||||
if (!passwordMatch) {
|
// if (!passwordMatch) {
|
||||||
throw new ForbiddenException('Access Denied');
|
// throw new ForbiddenException('Access Denied');
|
||||||
}
|
// }
|
||||||
|
|
||||||
await this.sessionService.checkSessionLimit(user.id);
|
// await this.sessionService.checkSessionLimit(user.id);
|
||||||
|
|
||||||
const sesseionId = await this.sessionService.createSession(
|
// const sesseionId = await this.sessionService.createSession(
|
||||||
user.id,
|
// user.id,
|
||||||
request.headers['user-agent']
|
// 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> {
|
// public async logout(userId: string): Promise<boolean> {
|
||||||
const affected =
|
// const affected =
|
||||||
await this.userCredentialsRepository.updateUserRefreshToken(userId, null);
|
// 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> {
|
// public async refresh(request: Request): Promise<AccessTokenDto> {
|
||||||
const sessionId = request.cookies['session_id'];
|
// const sessionId = request.cookies['session_id'];
|
||||||
|
|
||||||
if (!sessionId) {
|
// if (!sessionId) {
|
||||||
throw new ForbiddenException('Session ID missing');
|
// throw new ForbiddenException('Session ID missing');
|
||||||
}
|
// }
|
||||||
|
|
||||||
const session: Session =
|
// const session: Session =
|
||||||
await this.sessionService.findSessionBySessionId(sessionId);
|
// await this.sessionService.findSessionBySessionId(sessionId);
|
||||||
|
|
||||||
if (!session) {
|
// if (!session) {
|
||||||
throw new ForbiddenException('Invalid session');
|
// throw new ForbiddenException('Invalid session');
|
||||||
}
|
// }
|
||||||
|
|
||||||
const isUserAgentValid = await this.sessionService.validateSessionUserAgent(
|
// const isUserAgentValid = await this.sessionService.validateSessionUserAgent(
|
||||||
sessionId,
|
// sessionId,
|
||||||
request.headers['user-agent']
|
// request.headers['user-agent']
|
||||||
);
|
// );
|
||||||
|
|
||||||
if (!isUserAgentValid) {
|
// if (!isUserAgentValid) {
|
||||||
throw new ForbiddenException('Invalid session - User agent mismatch');
|
// throw new ForbiddenException('Invalid session - User agent mismatch');
|
||||||
}
|
// }
|
||||||
|
|
||||||
await this.sessionService.extendSessionExpiration(sessionId);
|
// await this.sessionService.extendSessionExpiration(sessionId);
|
||||||
|
|
||||||
const decodedToken: TokenPayload = await this.validateRefreshToken(
|
// const decodedToken: TokenPayload = await this.validateRefreshToken(
|
||||||
session.userCredentials['id']
|
// session.userCredentials['id']
|
||||||
);
|
// );
|
||||||
|
|
||||||
const newTokens = await this.generateAndPersistTokens(
|
// const newTokens = await this.generateAndPersistTokens(
|
||||||
decodedToken.sub,
|
// decodedToken.sub,
|
||||||
decodedToken.email,
|
// decodedToken.email,
|
||||||
false
|
// false
|
||||||
);
|
// );
|
||||||
|
|
||||||
return { access_token: newTokens.access_token };
|
// return { access_token: newTokens.access_token };
|
||||||
}
|
// }
|
||||||
|
|
||||||
private async generateAndPersistTokens(
|
// private async generateAndPersistTokens(
|
||||||
userId: string,
|
// userId: string,
|
||||||
email: string,
|
// email: string,
|
||||||
updateRefreshToken: boolean = false
|
// updateRefreshToken: boolean = false
|
||||||
): Promise<LoginResponseDto> {
|
// ): Promise<LoginResponseDto> {
|
||||||
const tokens = await this.tokenManagementService.generateTokens(
|
// const tokens = await this.tokenManagementService.generateTokens(
|
||||||
userId,
|
// userId,
|
||||||
email
|
// email
|
||||||
);
|
// );
|
||||||
|
|
||||||
if (updateRefreshToken) {
|
// if (updateRefreshToken) {
|
||||||
await this.userCredentialsRepository.updateUserRefreshToken(
|
// await this.userCredentialsRepository.updateUserRefreshToken(
|
||||||
userId,
|
// userId,
|
||||||
tokens.refresh_token
|
// 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> {
|
// private async validateRefreshToken(userId: string): Promise<TokenPayload> {
|
||||||
const user = await this.userCredentialsRepository.findUserById(userId);
|
// const user = await this.userCredentialsRepository.findUserById(userId);
|
||||||
|
|
||||||
if (!user || !user.refreshToken) {
|
// if (!user || !user.refreshToken) {
|
||||||
throw new Error('No refresh token found');
|
// throw new Error('No refresh token found');
|
||||||
}
|
// }
|
||||||
|
|
||||||
const decodedToken = await this.tokenManagementService.verifyRefreshToken(
|
// const decodedToken = await this.tokenManagementService.verifyRefreshToken(
|
||||||
user.refreshToken
|
// user.refreshToken
|
||||||
);
|
// );
|
||||||
|
|
||||||
if (decodedToken.exp < Date.now() / 1000) {
|
// if (decodedToken.exp < Date.now() / 1000) {
|
||||||
throw new Error('Token expired');
|
// throw new Error('Token expired');
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (decodedToken.sub !== user.id) {
|
// if (decodedToken.sub !== user.id) {
|
||||||
throw new Error('Token subject mismatch');
|
// throw new Error('Token subject mismatch');
|
||||||
}
|
// }
|
||||||
|
|
||||||
return decodedToken;
|
// return decodedToken;
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,10 @@ export class SessionService {
|
||||||
return this.sessionRepository.findSessionBySessionId(sessionId);
|
return this.sessionRepository.findSessionBySessionId(sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public findSessionByUserId(userId: string): Promise<Session[]> {
|
||||||
|
return this.sessionRepository.findSessionByUserId(userId);
|
||||||
|
}
|
||||||
|
|
||||||
public attachSessionToResponse(response: Response, sessionId: string): void {
|
public attachSessionToResponse(response: Response, sessionId: string): void {
|
||||||
this.sessionRepository.attachSessionToResponse(response, sessionId);
|
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