rebuild frontend to http only cookies for session
This commit is contained in:
parent
9aec010316
commit
5769cf4f5a
|
@ -35,19 +35,22 @@
|
|||
"argon2": "^0.40.1",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"pg": "^8.11.5",
|
||||
"reflect-metadata": "^0.2.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"swagger-ui-express": "^5.0.0",
|
||||
"typeorm": "^0.3.20"
|
||||
"typeorm": "^0.3.20",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.0.0",
|
||||
"@nestjs/schematics": "^10.0.0",
|
||||
"@nestjs/testing": "^10.0.0",
|
||||
"@types/argon2": "^0.15.0",
|
||||
"@types/cookie-parser": "^1.4.7",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/jest": "^29.5.2",
|
||||
"@types/node": "^20.3.1",
|
||||
|
|
|
@ -41,6 +41,9 @@ dependencies:
|
|||
class-validator:
|
||||
specifier: ^0.14.1
|
||||
version: 0.14.1
|
||||
cookie-parser:
|
||||
specifier: ^1.4.6
|
||||
version: 1.4.6
|
||||
passport:
|
||||
specifier: ^0.7.0
|
||||
version: 0.7.0
|
||||
|
@ -62,6 +65,9 @@ dependencies:
|
|||
typeorm:
|
||||
specifier: ^0.3.20
|
||||
version: 0.3.20(pg@8.11.5)(ts-node@10.9.2)
|
||||
uuid:
|
||||
specifier: ^9.0.1
|
||||
version: 9.0.1
|
||||
|
||||
devDependencies:
|
||||
'@nestjs/cli':
|
||||
|
@ -76,6 +82,9 @@ devDependencies:
|
|||
'@types/argon2':
|
||||
specifier: ^0.15.0
|
||||
version: 0.15.0
|
||||
'@types/cookie-parser':
|
||||
specifier: ^1.4.7
|
||||
version: 1.4.7
|
||||
'@types/express':
|
||||
specifier: ^4.17.17
|
||||
version: 4.17.21
|
||||
|
@ -1288,6 +1297,12 @@ packages:
|
|||
'@types/node': 20.12.4
|
||||
dev: true
|
||||
|
||||
/@types/cookie-parser@1.4.7:
|
||||
resolution: {integrity: sha512-Fvuyi354Z+uayxzIGCwYTayFKocfV7TuDYZClCdIP9ckhvAu/ixDtCB6qx2TT0FKjPLf1f3P/J1rgf6lPs64mw==}
|
||||
dependencies:
|
||||
'@types/express': 4.17.21
|
||||
dev: true
|
||||
|
||||
/@types/cookiejar@2.1.5:
|
||||
resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==}
|
||||
dev: true
|
||||
|
@ -2237,9 +2252,22 @@ packages:
|
|||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||
dev: true
|
||||
|
||||
/cookie-parser@1.4.6:
|
||||
resolution: {integrity: sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
dependencies:
|
||||
cookie: 0.4.1
|
||||
cookie-signature: 1.0.6
|
||||
dev: false
|
||||
|
||||
/cookie-signature@1.0.6:
|
||||
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
|
||||
|
||||
/cookie@0.4.1:
|
||||
resolution: {integrity: sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/cookie@0.6.0:
|
||||
resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
|
|
@ -3,7 +3,6 @@ import { ConfigModule } from '@nestjs/config';
|
|||
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { CorsMiddleware } from './middleware/cors-middleware/cors.middlware';
|
||||
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';
|
||||
|
@ -35,8 +34,8 @@ export class AppModule {
|
|||
.apply(
|
||||
CspMiddleware,
|
||||
SecurityHeadersMiddleware,
|
||||
HttpsRedirectMiddleware,
|
||||
CorsMiddleware
|
||||
HttpsRedirectMiddleware
|
||||
//CorsMiddleware
|
||||
)
|
||||
.forRoutes({ path: '*', method: RequestMethod.ALL });
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export * from './user-credentials.entity';
|
||||
export * from './user-data.entity';
|
||||
export * from './email-verification.entity';
|
||||
export * from './session.entity';
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
import { UserCredentials } from './user-credentials.entity';
|
||||
|
||||
@Entity()
|
||||
export class Session {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
public id: string;
|
||||
|
||||
@Column()
|
||||
public sessionId: string;
|
||||
|
||||
@Column({ type: 'timestamp' })
|
||||
public expiresAt: Date;
|
||||
|
||||
@Column({})
|
||||
public userAgent: string;
|
||||
|
||||
@ManyToOne(() => UserCredentials, (userCredentials) => userCredentials.id, {
|
||||
nullable: false,
|
||||
})
|
||||
@JoinColumn({ name: 'userCredentialsId' })
|
||||
public userCredentials: UserCredentials['id'];
|
||||
|
||||
@CreateDateColumn()
|
||||
public createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
public updatedAt: Date;
|
||||
}
|
|
@ -4,6 +4,7 @@ import { join } from 'path';
|
|||
import { INestApplication, ValidationPipe } from '@nestjs/common';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||
import * as cookieParser from 'cookie-parser';
|
||||
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
|
@ -30,6 +31,10 @@ async function setupSwagger(app: INestApplication): Promise<void> {
|
|||
);
|
||||
}
|
||||
|
||||
async function setupCookieParser(app: INestApplication): Promise<void> {
|
||||
app.use(cookieParser());
|
||||
}
|
||||
|
||||
async function setupPrefix(app: INestApplication): Promise<void> {
|
||||
app.setGlobalPrefix('api');
|
||||
}
|
||||
|
@ -41,6 +46,7 @@ async function setupClassValidator(app: INestApplication): Promise<void> {
|
|||
async function bootstrap(): Promise<void> {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
await setupCookieParser(app);
|
||||
await setupSwagger(app);
|
||||
await setupPrefix(app);
|
||||
await setupClassValidator(app);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { UserCredentials } from 'src/entities';
|
||||
import { Session, UserCredentials } from 'src/entities';
|
||||
|
||||
import { SendgridModule } from '../sendgrid-module/sendgrid.module';
|
||||
import { UserModule } from '../user-module/user.module';
|
||||
|
@ -10,6 +10,7 @@ import { VerifyModule } from '../verify-module/verify.module';
|
|||
import { AuthController } from './controller/auth.controller';
|
||||
import { UserCredentialsRepository } from './repositories/user-credentials.repository';
|
||||
import { AuthService } from './services/auth.service';
|
||||
import { SessionService } from './services/session.service';
|
||||
import { TokenManagementService } from './services/token-management.service';
|
||||
import { AccessTokenStrategy, RefreshTokenStrategy } from './strategies';
|
||||
|
||||
|
@ -19,10 +20,11 @@ import { AccessTokenStrategy, RefreshTokenStrategy } from './strategies';
|
|||
SendgridModule,
|
||||
VerifyModule,
|
||||
JwtModule.register({}),
|
||||
TypeOrmModule.forFeature([UserCredentials]),
|
||||
TypeOrmModule.forFeature([UserCredentials, Session]),
|
||||
],
|
||||
providers: [
|
||||
AuthService,
|
||||
SessionService,
|
||||
TokenManagementService,
|
||||
UserCredentialsRepository,
|
||||
AccessTokenStrategy,
|
||||
|
|
|
@ -4,14 +4,19 @@ import {
|
|||
Body,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
UseGuards,
|
||||
Res,
|
||||
Req,
|
||||
} from '@nestjs/common';
|
||||
import { ApiCreatedResponse, ApiHeader, ApiTags } from '@nestjs/swagger';
|
||||
import { ApiCreatedResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { Response, Request } from 'express';
|
||||
import { Public } from 'src/shared/decorator';
|
||||
|
||||
import { GetCurrentUser, GetCurrentUserId } from '../common/decorators';
|
||||
import { RefreshTokenGuard } from '../common/guards';
|
||||
import { TokensDto, UserCredentialsDto } from '../models/dto';
|
||||
import { GetCurrentUserId } from '../common/decorators';
|
||||
import {
|
||||
AccessTokenDto,
|
||||
LoginResponseDto,
|
||||
UserCredentialsDto,
|
||||
} from '../models/dto';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
|
||||
@ApiTags('Authentication')
|
||||
|
@ -21,28 +26,36 @@ export class AuthController {
|
|||
|
||||
@ApiCreatedResponse({
|
||||
description: 'User signed up successfully',
|
||||
type: TokensDto,
|
||||
type: LoginResponseDto,
|
||||
})
|
||||
@Public()
|
||||
@Post('signup')
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
public async signup(
|
||||
@Body() userCredentials: UserCredentialsDto
|
||||
): Promise<TokensDto> {
|
||||
): Promise<LoginResponseDto> {
|
||||
return this.authService.signup(userCredentials);
|
||||
}
|
||||
|
||||
@ApiCreatedResponse({
|
||||
description: 'User signin successfully',
|
||||
type: TokensDto,
|
||||
type: LoginResponseDto,
|
||||
})
|
||||
@Public()
|
||||
@Post('signin')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
public async signin(
|
||||
@Res({ passthrough: true }) response: Response,
|
||||
@Req() request: Request,
|
||||
@Body() userCredentials: UserCredentialsDto
|
||||
): Promise<TokensDto> {
|
||||
return this.authService.signin(userCredentials);
|
||||
): Promise<LoginResponseDto> {
|
||||
return await this.authService.signin(userCredentials, response, request);
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Post('refresh')
|
||||
public async refreshToken(@Req() request: Request): Promise<AccessTokenDto> {
|
||||
return await this.authService.refresh(request);
|
||||
}
|
||||
|
||||
@ApiCreatedResponse({
|
||||
|
@ -55,25 +68,42 @@ export class AuthController {
|
|||
return this.authService.logout(userId);
|
||||
}
|
||||
|
||||
@ApiHeader({
|
||||
name: 'Authorization',
|
||||
required: true,
|
||||
schema: {
|
||||
example: 'Bearer <refresh_token>',
|
||||
},
|
||||
})
|
||||
@ApiCreatedResponse({
|
||||
description: 'User tokens refreshed successfully',
|
||||
type: TokensDto,
|
||||
})
|
||||
@Public()
|
||||
@UseGuards(RefreshTokenGuard)
|
||||
@Post('refresh')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
public async refresh(
|
||||
@GetCurrentUserId() userId: string,
|
||||
@GetCurrentUser('refresh_token') refresh_token: string
|
||||
): Promise<TokensDto> {
|
||||
return this.authService.refresh(userId, refresh_token);
|
||||
}
|
||||
// @ApiHeader({
|
||||
// name: 'Authorization',
|
||||
// required: true,
|
||||
// schema: {
|
||||
// example: 'Bearer <refresh_token>',
|
||||
// },
|
||||
// })
|
||||
// @ApiCreatedResponse({
|
||||
// description: 'User tokens refreshed successfully',
|
||||
// type: TokensDto,
|
||||
// })
|
||||
// @Public()
|
||||
// @UseGuards(RefreshTokenGuard)
|
||||
// @Post('refresh')
|
||||
// @HttpCode(HttpStatus.OK)
|
||||
// public async refresh(
|
||||
// @GetCurrentUserId() userId: string,
|
||||
// @GetCurrentUser('refresh_token') refresh_token: string
|
||||
// ): Promise<TokensDto> {
|
||||
// return this.authService.refresh(userId, refresh_token);
|
||||
// }
|
||||
|
||||
// @ApiHeader({
|
||||
// name: 'Authorization',
|
||||
// required: true,
|
||||
// schema: {
|
||||
// example: 'Bearer <access_token>',
|
||||
// },
|
||||
// })
|
||||
// @ApiCreatedResponse({
|
||||
// description: 'Token validity checked successfully',
|
||||
// type: Boolean,
|
||||
// })
|
||||
// @Post('check-token')
|
||||
// @HttpCode(HttpStatus.OK)
|
||||
// public checkTokenValidity(): Promise<boolean> {
|
||||
// return this.authService.checkTokenValidity();
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class TokensDto {
|
||||
export class AccessTokenDto {
|
||||
@ApiProperty({
|
||||
title: 'Access token',
|
||||
description: 'Access token',
|
||||
|
@ -10,13 +10,4 @@ export class TokensDto {
|
|||
@IsNotEmpty()
|
||||
@IsString()
|
||||
public access_token: string;
|
||||
|
||||
@ApiProperty({
|
||||
title: 'Refresh token',
|
||||
description: 'Refresh token',
|
||||
example: 'eyJhbGci',
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
public refresh_token: string;
|
||||
}
|
|
@ -1,2 +1,3 @@
|
|||
export * from './user-credentials.dto';
|
||||
export * from './tokens.dto';
|
||||
export * from './login-response.dto';
|
||||
export * from './access-token.dto';
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class LoginResponseDto {
|
||||
@ApiProperty({
|
||||
title: 'Access token',
|
||||
description: 'Access token',
|
||||
example: 'eyJhbGci',
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
public access_token: string;
|
||||
|
||||
@ApiProperty({
|
||||
title: 'Email',
|
||||
description: 'User Email',
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@IsEmail()
|
||||
public email: string;
|
||||
|
||||
@ApiProperty({
|
||||
title: 'User ID',
|
||||
description: 'User ID',
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@IsEmail()
|
||||
public userId: string;
|
||||
}
|
|
@ -1,13 +1,23 @@
|
|||
import { ForbiddenException, Injectable } from '@nestjs/common';
|
||||
import { Response, Request } from 'express';
|
||||
import { Session } from 'src/entities';
|
||||
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 { TokensDto, UserCredentialsDto } from '../models/dto';
|
||||
import {
|
||||
AccessTokenDto,
|
||||
LoginResponseDto,
|
||||
UserCredentialsDto,
|
||||
} from '../models/dto';
|
||||
import { UserCredentialsRepository } from '../repositories/user-credentials.repository';
|
||||
|
||||
import { TokenManagementService } from './token-management.service';
|
||||
import { SessionService } from './session.service';
|
||||
import {
|
||||
TokenManagementService,
|
||||
TokenPayload,
|
||||
} from './token-management.service';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
|
@ -16,10 +26,13 @@ export class AuthService {
|
|||
private readonly userDataRepository: UserDataRepository,
|
||||
private readonly tokenManagementService: TokenManagementService,
|
||||
private readonly passwordConfirmationMailService: PasswordConfirmationMailService,
|
||||
private readonly emailVerificationService: EmailVerificationService
|
||||
private readonly emailVerificationService: EmailVerificationService,
|
||||
private readonly sessionService: SessionService
|
||||
) {}
|
||||
|
||||
public async signup(userCredentials: UserCredentialsDto): Promise<TokensDto> {
|
||||
public async signup(
|
||||
userCredentials: UserCredentialsDto
|
||||
): Promise<LoginResponseDto> {
|
||||
const passwordHashed = await EncryptionService.hashData(
|
||||
userCredentials.password
|
||||
);
|
||||
|
@ -44,7 +57,11 @@ export class AuthService {
|
|||
return this.generateAndPersistTokens(user.id, user.email);
|
||||
}
|
||||
|
||||
public async signin(userCredentials: UserCredentialsDto): Promise<TokensDto> {
|
||||
public async signin(
|
||||
userCredentials: UserCredentialsDto,
|
||||
response: Response,
|
||||
request: Request
|
||||
): Promise<LoginResponseDto> {
|
||||
const user = await this.userCredentialsRepository.findUserByEmail(
|
||||
userCredentials.email
|
||||
);
|
||||
|
@ -62,32 +79,18 @@ export class AuthService {
|
|||
throw new ForbiddenException('Access Denied');
|
||||
}
|
||||
|
||||
return this.generateAndPersistTokens(user.id, user.email);
|
||||
}
|
||||
|
||||
public async refresh(
|
||||
userId: string,
|
||||
refreshToken: string
|
||||
): Promise<TokensDto> {
|
||||
const user = await this.userCredentialsRepository.findUserById(userId);
|
||||
|
||||
if (!user || !user.hashedRt) {
|
||||
throw new ForbiddenException('Access Denied');
|
||||
}
|
||||
|
||||
const refreshTokenMatch = await EncryptionService.compareHash(
|
||||
refreshToken,
|
||||
user.hashedRt
|
||||
const sesseionId = await this.sessionService.createSession(
|
||||
user.id,
|
||||
request.headers['user-agent']
|
||||
);
|
||||
|
||||
if (!refreshTokenMatch) {
|
||||
throw new ForbiddenException('Access Denied');
|
||||
}
|
||||
this.sessionService.attachSessionToResponse(response, sesseionId.sessionId);
|
||||
|
||||
return this.generateAndPersistTokens(user.id, user.email);
|
||||
}
|
||||
|
||||
public async logout(userId: string): Promise<boolean> {
|
||||
// TODO Check if the user is logged out already
|
||||
const affected = await this.userCredentialsRepository.updateUserTokenHash(
|
||||
userId,
|
||||
null
|
||||
|
@ -96,22 +99,77 @@ export class AuthService {
|
|||
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']
|
||||
);
|
||||
|
||||
// TODO expand session expiration
|
||||
|
||||
if (!isUserAgentValid) {
|
||||
throw new ForbiddenException('Invalid session - User agent mismatch');
|
||||
}
|
||||
|
||||
const decodedToken: TokenPayload = await this.validateRefreshToken(
|
||||
session.userCredentials['id']
|
||||
);
|
||||
|
||||
const newTokens = await this.generateAndPersistTokens(
|
||||
decodedToken.sub,
|
||||
decodedToken.email
|
||||
);
|
||||
|
||||
return { access_token: newTokens.access_token };
|
||||
}
|
||||
|
||||
private async generateAndPersistTokens(
|
||||
userId: string,
|
||||
email: string
|
||||
): Promise<TokensDto> {
|
||||
): Promise<LoginResponseDto> {
|
||||
const tokens = await this.tokenManagementService.generateTokens(
|
||||
userId,
|
||||
email
|
||||
);
|
||||
const hashedRefreshToken = await EncryptionService.hashData(
|
||||
tokens.refresh_token
|
||||
);
|
||||
|
||||
await this.userCredentialsRepository.updateUserTokenHash(
|
||||
userId,
|
||||
hashedRefreshToken
|
||||
tokens.refresh_token
|
||||
);
|
||||
return tokens;
|
||||
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.hashedRt) {
|
||||
throw new Error('No refresh token found');
|
||||
}
|
||||
|
||||
const decodedToken = await this.tokenManagementService.verifyRefreshToken(
|
||||
user.hashedRt
|
||||
);
|
||||
|
||||
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,82 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Response } from 'express';
|
||||
import { Session } from 'src/entities';
|
||||
import { LessThan, Repository } from 'typeorm';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@Injectable()
|
||||
export class SessionService {
|
||||
public constructor(
|
||||
@InjectRepository(Session)
|
||||
private sessionRepository: Repository<Session>
|
||||
) {}
|
||||
|
||||
public async createSession(
|
||||
userId: string,
|
||||
userAgent: string
|
||||
): Promise<Session> {
|
||||
const sessionId = uuidv4();
|
||||
const expirationDate = new Date();
|
||||
|
||||
expirationDate.setHours(expirationDate.getHours() + 1);
|
||||
|
||||
const session = this.sessionRepository.create({
|
||||
userCredentials: userId,
|
||||
sessionId: sessionId,
|
||||
expiresAt: expirationDate,
|
||||
userAgent: userAgent,
|
||||
});
|
||||
|
||||
await this.sessionRepository.save(session);
|
||||
return session;
|
||||
}
|
||||
|
||||
public async validateSessionUserAgent(
|
||||
sessionId: string,
|
||||
currentUserAgent: string
|
||||
): Promise<boolean> {
|
||||
const session = await this.sessionRepository.findOne({
|
||||
where: { sessionId: sessionId },
|
||||
select: ['userAgent'],
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return session.userAgent === currentUserAgent;
|
||||
}
|
||||
|
||||
// TODO use method to invalidate session
|
||||
public async invalidateSession(sessionId: string): Promise<void> {
|
||||
await this.sessionRepository.delete({ sessionId: sessionId });
|
||||
}
|
||||
|
||||
// TODO use method to invalidate all sessions for user
|
||||
public async invalidateAllSessionsForUser(userId: string): Promise<void> {
|
||||
await this.sessionRepository.delete({ userCredentials: userId });
|
||||
}
|
||||
|
||||
// TODO use method to clear expired sessions
|
||||
public async clearExpiredSessions(): Promise<void> {
|
||||
const now = new Date();
|
||||
|
||||
await this.sessionRepository.delete({ expiresAt: LessThan(now) });
|
||||
}
|
||||
|
||||
public async findSessionBySessionId(sessionId: string): Promise<Session> {
|
||||
return this.sessionRepository.findOne({
|
||||
where: { sessionId: sessionId },
|
||||
relations: ['userCredentials'],
|
||||
});
|
||||
}
|
||||
|
||||
public attachSessionToResponse(response: Response, sessionId: string): void {
|
||||
response.cookie('session_id', sessionId, {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: 'strict',
|
||||
});
|
||||
}
|
||||
}
|
|
@ -2,7 +2,17 @@ import { Injectable } from '@nestjs/common';
|
|||
import { ConfigService } from '@nestjs/config';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
|
||||
import { TokensDto } from '../models/dto';
|
||||
type Tokens = {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
};
|
||||
|
||||
export type TokenPayload = {
|
||||
sub: string;
|
||||
email: string;
|
||||
iat: number;
|
||||
exp: number;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class TokenManagementService {
|
||||
|
@ -25,16 +35,19 @@ export class TokenManagementService {
|
|||
this.JWT_SECRET_RT = this.configService.get<string>('JWT_SECRET_RT');
|
||||
}
|
||||
|
||||
public async generateTokens(
|
||||
userId: string,
|
||||
email: string
|
||||
): Promise<TokensDto> {
|
||||
public async generateTokens(userId: string, email: string): Promise<Tokens> {
|
||||
const access_token: string = await this.createAccessToken(userId, email);
|
||||
const refresh_token: string = await this.createRefreshToken(userId, email);
|
||||
|
||||
return { access_token, refresh_token };
|
||||
}
|
||||
|
||||
public async verifyRefreshToken(token: string): Promise<TokenPayload> {
|
||||
return this.jwt.verifyAsync(token, {
|
||||
secret: this.JWT_SECRET_RT,
|
||||
});
|
||||
}
|
||||
|
||||
private async createAccessToken(
|
||||
userId: string,
|
||||
email: string
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import { ConfigService } from '@nestjs/config';
|
||||
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
|
||||
import { EmailVerification, UserCredentials, UserData } from 'src/entities';
|
||||
import {
|
||||
EmailVerification,
|
||||
UserCredentials,
|
||||
UserData,
|
||||
Session,
|
||||
} from 'src/entities';
|
||||
|
||||
export const databaseConfigFactory = (
|
||||
configService: ConfigService
|
||||
|
@ -13,5 +18,5 @@ export const databaseConfigFactory = (
|
|||
database: configService.get('DB_NAME'),
|
||||
synchronize: true,
|
||||
logging: true,
|
||||
entities: [UserCredentials, UserData, EmailVerification],
|
||||
entities: [UserCredentials, UserData, EmailVerification, Session],
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue