Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
62d9a9a
implemented the Implement Secure Session and Token Management
codesailor4 Aug 21, 2026
2030e21
implemented the Implement Secure Session and Token Management
codesailor4 Aug 21, 2026
3fa1cfe
implemented the Implement Secure Session and Token Management
codesailor4 Aug 21, 2026
a1f8486
implemented the Implement Secure Session and Token Management
codesailor4 Aug 21, 2026
82287db
implemneted the Implement Secure Session and Token Management
codesailor4 Aug 21, 2026
7027c5a
implemneted the Implement Centralized API Error Handling
codesailor4 Aug 21, 2026
fec0b42
implemneted the Implement Centralized API Error Handling
codesailor4 Aug 21, 2026
c1d7073
implemneted the Implement Secure Session and Token Management
codesailor4 Aug 21, 2026
3d3ce6b
implemneted the Implement Secure Session and Token Management
codesailor4 Aug 21, 2026
3af72b5
implemneted the Implement Secure Session and Token Management
codesailor4 Aug 21, 2026
3fa20f3
implemneted the Implement Secure Session and Token Management
codesailor4 Aug 21, 2026
86d2d73
implemneted the Implement Secure Session and Token Management
codesailor4 Aug 21, 2026
a699b1a
implemneted the Implement Secure Session and Token Management
codesailor4 Aug 21, 2026
5b719c2
implemneted the Implement Secure Session and Token Management
codesailor4 Aug 21, 2026
975f5fd
implemneted the Implement Secure Session and Token Management
codesailor4 Aug 21, 2026
d1e2284
implemneted the Implement Secure Session and Token Management
codesailor4 Aug 21, 2026
adf7b00
implemneted the Implement Secure Session and Token Management
codesailor4 Aug 21, 2026
3a023d8
implemneted the Implement Secure Session and Token Management
codesailor4 Aug 21, 2026
fcc3815
run build
codesailor4 Aug 24, 2026
d859b06
Merge branch 'main' into feat/session
codesailor4 Aug 24, 2026
47d2ab8
implemeneted the build
codesailor4 Aug 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
"typescript-eslint": "^8.20.0",
"yn": "^3.1.1"
}
}
5 changes: 4 additions & 1 deletion backend/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,16 @@ import { ForgotPasswordProvider } from './providers/forgot-password.provider';
import { ResetPasswordProvider } from './providers/reset-password.provider';
import { MailService } from './providers/mail.service';
import { NonceService } from './providers/nonce.service';

import { Session } from './entities/session.entity';
import { SessionsProvider } from './providers/sessions.provider';
import { GuestSessionProvider } from './providers/guest-session.provider';

@Module({
imports: [
forwardRef(() => UsersModule),
TypeOrmModule.forFeature([User]),
ConfigModule.forFeature(jwtConfig),
TypeOrmModule.forFeature([Session]),
JwtModule.registerAsync(jwtConfig.asProvider()),
ThrottlerModule.forRoot([
{
Expand All @@ -54,6 +56,7 @@ import { GuestSessionProvider } from './providers/guest-session.provider';
ResetPasswordProvider,
MailService,
NonceService,
SessionsProvider,
{
provide: HashingProvider, // Use the abstract class as a token
useClass: BcryptProvider, // Bind it to the concrete implementation
Expand Down
5 changes: 3 additions & 2 deletions backend/src/auth/authConfig/jwt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export default registerAs('jwt', () => {
googleClient_id: process.env.GOOGLE_CLIENT_ID,
googleClient_secret: process.env.GOOGLE_CLIENT_SECRET,
issuer: process.env.JWT_TOKEN_ISSUER ?? 'localhost',
ttl: parseInt(process.env.JWT_ACCESS_TOKEN_TTL ?? '3600'),
accessTokenTtl: parseInt(process.env.JWT_ACCESS_TOKEN_TTL ?? '3600'), // 1 hour default
refreshTokenTtl: parseInt(process.env.JWT_REFRESH_TOKEN_TTL ?? '604800'), // 7 days default
};
});
});
59 changes: 36 additions & 23 deletions backend/src/auth/controllers/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,22 @@ import {
Get,
Query,
Param,
Req,
UseGuards,
} from '@nestjs/common';
import { Throttle } from '@nestjs/throttler';
import { LoginDto } from '../dtos/login.dto';
import { RegisterDto } from '../dtos/register.dto';
import { AuthService } from '../providers/auth.service';
import { RefreshTokenDto } from '../dtos/refreshTokenDto';
import { ApiOperation, ApiResponse } from '@nestjs/swagger';
import { ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
import { NonceResponseDto } from '../dtos/nonceResponse.dto';
import { StellarWalletLoginDto } from '../dtos/walletLogin.dto';
import { ResetPasswordDto } from '../dtos/reset-password.dto';
import { ForgotPasswordDto } from '../dtos/forgot-password.dto';
import { AuthGuard } from '@nestjs/passport';
import { ActiveUser } from '../decorators/activeUser.decorator';
import { ActiveUserData } from '../interfaces/activeInterface';

import { GuestSessionProvider } from '../providers/guest-session.provider';
import { ConvertGuestDto } from '../dtos/convert-guest.dto';
Expand Down Expand Up @@ -210,32 +215,40 @@ export class AuthController {
}

@Post('/reset-password/:token')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Reset password with token',
description: 'Resets user password using the token from email',
})
@ApiResponse({
status: 200,
description: 'Password reset successfully',
schema: {
type: 'object',
properties: {
message: {
type: 'string',
example: 'Password has been reset successfully',
},
},
},
})
@ApiResponse({
status: 400,
description: 'Invalid or expired token',
})
public async resetPassword(
@Param('token') token: string,
@Body() resetPasswordDto: ResetPasswordDto,
) {
return await this.authservice.resetPassword(token, resetPasswordDto);
}

@Post('/logout')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Logout current user and invalidate session' })
@ApiResponse({ status: 200, description: 'Successfully logged out' })
@ApiResponse({ status: 401, description: 'Invalid or missing refresh token' })
public async logout(@Body() refreshTokenDto: RefreshTokenDto) {
return await this.authservice.logout(refreshTokenDto);
}

@Post('/logout-all')
@UseGuards(AuthGuard('jwt'))
@ApiBearerAuth()
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Logout from all devices' })
@ApiResponse({ status: 200, description: 'All sessions invalidated' })
public async logoutAll(@ActiveUser() user: ActiveUserData) {
return await this.authservice.logoutAll(user.sub);
}

@Get('/me')
@UseGuards(AuthGuard('jwt'))
@ApiBearerAuth()
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Get current authenticated user' })
@ApiResponse({ status: 200, description: 'Current user data retrieved' })
@ApiResponse({ status: 401, description: 'Unauthorized - invalid or missing token' })
public async getCurrentUser(@ActiveUser() user: ActiveUserData) {
return await this.authservice.getCurrentUser(user.sub);
}
}
42 changes: 42 additions & 0 deletions backend/src/auth/entities/session.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {
Column,
Entity,
PrimaryGeneratedColumn,
ManyToOne,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
import { User } from '../../users/user.entity';

@Entity()
export class Session {
@PrimaryGeneratedColumn('uuid')
id: string;

@ManyToOne(() => User, { onDelete: 'CASCADE' })
user: User;

@Column()
userId: string;

@Column({ unique: true })
refreshTokenHash: string;

@Column({ nullable: true })
deviceInfo?: string;

@Column({ nullable: true })
ipAddress?: string;

@Column({ type: 'timestamp' })
expiresAt: Date;

@Column({ default: true })
isActive: boolean;

@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn()
updatedAt: Date;
}
43 changes: 42 additions & 1 deletion backend/src/auth/providers/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { BadRequestException, Injectable, UnauthorizedException } from '@nestjs/common';
import { LoginDto } from '../dtos/login.dto';
import { RegisterDto } from '../dtos/register.dto';
import { SignInProvider } from './sign-in.provider';
Expand All @@ -14,6 +14,8 @@ import { ResetPasswordProvider } from './reset-password.provider';
import { ForgotPasswordDto } from '../dtos/forgot-password.dto';
import { ResetPasswordDto } from '../dtos/reset-password.dto';
import { NonceService } from './nonce.service';
import { SessionsProvider } from './sessions.provider';
import { UsersService } from '../../users/providers/users.service';

@Injectable()
export class AuthService {
Expand Down Expand Up @@ -52,6 +54,16 @@ export class AuthService {
* inject nonceService
*/
private readonly nonceService: NonceService,

/**
* Inject SessionsProvider for secure session management
*/
private readonly sessionsProvider: SessionsProvider,

/**
* Inject UsersService to retrieve user data
*/
private readonly usersService: UsersService,
) {}

public async register(registerDto: RegisterDto) {
Expand Down Expand Up @@ -150,4 +162,33 @@ export class AuthService {
resetPasswordDto,
);
}

/**
* Logout current user by invalidating their refresh token
*/
public async logout(refreshTokenDto: RefreshTokenDto) {
await this.sessionsProvider.invalidateSession(refreshTokenDto.refreshToken);
return { message: 'Successfully logged out' };
}

/**
* Invalidate all sessions for a user (logout from all devices)
*/
public async logoutAll(userId: string) {
await this.sessionsProvider.invalidateAllUserSessions(userId);
return { message: 'All sessions invalidated successfully' };
}

/**
* Get current authenticated user data
*/
public async getCurrentUser(userId: string) {
const user = await this.usersService.findOneById(userId);
if (!user) {
throw new UnauthorizedException('User not found');
}
// Return user without sensitive data
const { password, passwordResetToken, passwordResetExpires, ...userWithoutSensitiveData } = user;
return userWithoutSensitiveData;
}
}
6 changes: 3 additions & 3 deletions backend/src/auth/providers/generate-tokens.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,12 @@ export class GenerateTokensProvider {
}

const [accessToken, refreshToken] = await Promise.all([
this.signToken(user.id, user.username, this.jwtConfiguration.ttl, {
this.signToken(user.id, user.username, this.jwtConfiguration.accessTokenTtl, {
email: user.email,
}),
this.signToken(user.id, user.username, this.jwtConfiguration.ttl),
this.signToken(user.id, user.username, this.jwtConfiguration.refreshTokenTtl),
]);

return { accessToken, refreshToken, user };
}
}
}
66 changes: 14 additions & 52 deletions backend/src/auth/providers/refreshTokensProvider.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,7 @@
import {
forwardRef,
Inject,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { RefreshTokenDto } from '../dtos/refreshTokenDto';
import { JwtService } from '@nestjs/jwt';
import { ConfigType } from '@nestjs/config';
import jwtConfig from '../authConfig/jwt.config';
import { GenerateTokensProvider } from './generate-tokens.provider';
import { ApiTags, ApiOperation, ApiBody } from '@nestjs/swagger';
import { UsersService } from '../../users/providers/users.service';
import { SessionsProvider } from './sessions.provider';

/**
* Refresh token provider class
Expand All @@ -20,26 +11,9 @@ import { UsersService } from '../../users/providers/users.service';
export class RefreshTokensProvider {
constructor(
/**
* Injecting UserService repository
* Injecting SessionsProvider for secure session management
*/
@Inject(forwardRef(() => UsersService))
private readonly userService: UsersService,

/**
* Injecting JwtService
*/
private readonly jwtService: JwtService,

/**
* Injecting JWT Configuration
*/
@Inject(jwtConfig.KEY)
private readonly jwtConfiguration: ConfigType<typeof jwtConfig>,

/**
* Injecting GenerateTokensProvider
*/
private readonly generateTokenProvider: GenerateTokensProvider,
private readonly sessionsProvider: SessionsProvider,
) {}

/**
Expand All @@ -49,28 +23,16 @@ export class RefreshTokensProvider {
*/
@ApiOperation({ summary: 'Refresh authentication tokens' })
@ApiBody({ type: RefreshTokenDto })
public async refreshTokens(refreshTokenDto: RefreshTokenDto) {
// Validate the refresh token using JWT
const payload = await this.jwtService.verifyAsync<{ sub: string }>(
public async refreshTokens(
refreshTokenDto: RefreshTokenDto,
deviceInfo?: string,
ipAddress?: string,
) {
// Use sessions provider to validate and rotate the refresh token
return await this.sessionsProvider.refreshSession(
refreshTokenDto.refreshToken,
{
secret: this.jwtConfiguration.secret,
audience: this.jwtConfiguration.audience,
issuer: this.jwtConfiguration.issuer,
},
deviceInfo,
ipAddress,
);

const sub = payload.sub;

// Retrieve the user from the database
const user = await this.userService.findOneByGoogleId(sub);

// inside refreshTokens
if (!user) {
throw new UnauthorizedException('Invalid refresh token');
}

// Generate new tokens
return await this.generateTokenProvider.generateTokens(user);
}
}
}
Loading
Loading