Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
39 changes: 36 additions & 3 deletions backend/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,47 @@
import { Controller, Post, Body } from '@nestjs/common';
import {
Controller,
Post,
Body,
HttpCode,
HttpStatus,
Res,
UseGuards,
} from '@nestjs/common';
import { AuthService } from './providers/auth.service';
import { CreateUserDto } from '../users/dto/createUser.dto';
import { User } from '../users/entities/user.entity';
import { Public } from './decorators/public.decorator';
import { Response } from 'express';
import { AuthResponse } from './interfaces/authResponse.interface';
import { LoginUserDto } from 'src/users/dto/loginUser.dto';
import { GetCurrentUser } from './decorators/getCurrentUser.decorator';
import { LocalAuthGuard } from './guards/local.guard';

@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}

// CREATE A NEW USER
@Public()
@Post('register')
async register(@Body() createUserDto: CreateUserDto): Promise<User> {
return await this.authService.createUser(createUserDto);
@HttpCode(HttpStatus.CREATED)
async register(
@Body() createUserDto: CreateUserDto,
@Res({ passthrough: true }) response: Response,
): Promise<AuthResponse> {
return await this.authService.createUser(createUserDto, response);
}

// LOGIN USER
@Public()
@Post('login')
@UseGuards(LocalAuthGuard)
@HttpCode(HttpStatus.OK)
public async loginUser(
@Body() loginUserDto: LoginUserDto,
@GetCurrentUser() user: User,
@Res({ passthrough: true }) response: Response,
): Promise<AuthResponse> {
return await this.authService.loginUser(user, response);
}
}
36 changes: 33 additions & 3 deletions backend/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,29 @@ import { BcryptProvider } from './providers/bcrypt.provider';
import { LocalStrategy } from './strategies/local.strategy';
import { JwtStrategy } from './strategies/jwt.strategy';
import { JwtRefreshStrategy } from './strategies/jwtRefresh.strategy';
import { LoginUserProvider } from './providers/loginUser.provider';
import { GenerateTokensProvider } from './providers/generateTokens.provider';
import { RefreshTokensProvider } from './providers/refreshTokens.provider';
import { RefreshTokenRepositoryOperations } from './providers/RefreshTokenCrud.repository';
import { FindOneRefreshTokenProvider } from './providers/findOneRefreshToken.provider';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';

@Module({
imports: [
TypeOrmModule.forFeature([RefreshToken]),
forwardRef(() => UsersModule)
forwardRef(() => UsersModule),
ConfigModule,
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
secret: configService.get('JWT_SECRET'),
signOptions: {
expiresIn: configService.get('JWT_ACCESS_EXPIRATION'),
},
}),
}),
],
controllers: [AuthController],
providers: [
Expand All @@ -22,10 +40,22 @@ import { JwtRefreshStrategy } from './strategies/jwtRefresh.strategy';
provide: HashingProvider,
useClass: BcryptProvider,
},
LocalStrategy,
LocalStrategy,
JwtStrategy,
JwtRefreshStrategy,
LoginUserProvider,
GenerateTokensProvider,
RefreshTokensProvider,
RefreshTokenRepositoryOperations,
FindOneRefreshTokenProvider,
],
exports: [
AuthService,
HashingProvider,
GenerateTokensProvider,
RefreshTokensProvider,
RefreshTokenRepositoryOperations,
FindOneRefreshTokenProvider,
],
exports: [AuthService, HashingProvider]
})
export class AuthModule {}
20 changes: 20 additions & 0 deletions backend/src/auth/http/post.auth.endpoints.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

// CREATE NEW USER
POST http://localhost:6000/auth/register
Content-Type: application/json

{
"firstname": "john",
"lastname": "doe",
"email": "johndoe@gmail.com",
"password": "Adetomi.54"
}

// LOGIN USER
POST http://localhost:6000/auth/login
Content-Type: application/json

{
"email": "johndoe",
"password": "Adetomi.54"
}
139 changes: 139 additions & 0 deletions backend/src/auth/providers/RefreshTokenCrud.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import {
BadRequestException,
Inject,
Injectable,
RequestTimeoutException,
UnauthorizedException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { RefreshToken } from '../entities/refreshToken.entity';
import { Repository } from 'typeorm';
import { User } from 'src/users/entities/user.entity';
import { Request } from 'express';
import { ConfigService } from '@nestjs/config';
import { HashingProvider } from './hashing.provider';
import { FindOneRefreshTokenProvider } from './findOneRefreshToken.provider';

@Injectable()
export class RefreshTokenRepositoryOperations {
constructor(
@InjectRepository(RefreshToken)
private readonly refreshTokenEntity: Repository<RefreshToken>,

private readonly configService: ConfigService,

private readonly hashingProvider: HashingProvider,

private readonly findOneRefreshTokenProvider: FindOneRefreshTokenProvider,
) {}

// [1] save the refresh token to the databse
public async saveRefreshToken(
user: User,
token: string,
req?: Request,
): Promise<RefreshToken> {
let savedRefreshTokenEntity: RefreshToken;

let raw = this.configService.get<string>('JWT_REFRESH_EXPIRATION'); // e.g. "604800" or "604800000"
let expirationSeconds: number;

if (parseInt(raw) > 1000000) {
// looks like milliseconds
expirationSeconds = Math.floor(parseInt(raw) / 1000);
} else {
// already seconds
expirationSeconds = parseInt(raw);
}

const expiresAt = new Date(Date.now() + expirationSeconds * 1000);

const refreshTokenEntityData: Partial<RefreshToken> = {
user,
token: await this.hashingProvider.hash(token),
expiresAt,
};

// conditionally add the metadata fileds
if (req) {
refreshTokenEntityData.userAgent = req.headers['user-agent'] || 'unknown';
refreshTokenEntityData.ipAddress = req.ip || 'unknown';
}

// create a new refresh token entity
savedRefreshTokenEntity = this.refreshTokenEntity.create(
refreshTokenEntityData,
);

try {
savedRefreshTokenEntity = await this.refreshTokenEntity.save(
savedRefreshTokenEntity,
);
} catch (error) {
throw new RequestTimeoutException('Error connecting to the database');
}

if (!savedRefreshTokenEntity) {
throw new BadRequestException('Error saving refresh token to database');
}

return savedRefreshTokenEntity;
}

// [2] find one refresh token from the database and return it
public async findOneRefreshToken(userId: string, userToken: string) {
const refreshToken =
await this.findOneRefreshTokenProvider.findRefreshToken(
userId,
userToken,
);

return refreshToken;
}

// [3] invalidate/revoke a refresh token entity
public async revokeSingleRefreshToken(userId: string, userToken: string) {
let refreshToken: RefreshToken;

refreshToken = await this.findOneRefreshTokenProvider.findRefreshToken(
userId,
userToken,
);

const now = new Date();

refreshToken.revoked = true;
refreshToken.revokedAt = now;
await this.refreshTokenEntity.save(refreshToken);

return {
loggedOut: true,
refreshToken,
};
}

// [4] invalidate/revoke all refresh token entity of the user
public async revokeAllRefreshTokens(userId: string) {
const allRefreshTokenEntities = await this.refreshTokenEntity.find({
where: {
user: {
id: userId,
},
revoked: false,
},
});

const now = new Date();

const revokedTokens = allRefreshTokenEntities.map((token) => {
((token.revoked = true), (token.revokedAt = now));
return token;
});

await this.refreshTokenEntity.save(revokedTokens);

return {
revokedAllSessions: true,
};
}
}
33 changes: 30 additions & 3 deletions backend/src/auth/providers/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,39 @@ import { Injectable } from '@nestjs/common';
import { UsersService } from '../../users/providers/users.service';
import { CreateUserDto } from '../../users/dto/createUser.dto';
import { User } from '../../users/entities/user.entity';
import { LoginUserProvider } from './loginUser.provider';
import { AuthResponse } from '../interfaces/authResponse.interface';
import { Response } from 'express';

@Injectable()
export class AuthService {
constructor(private readonly usersService: UsersService) {}
constructor(
private readonly usersService: UsersService,

async createUser(createUserDto: CreateUserDto): Promise<User> {
return await this.usersService.createUser(createUserDto);
private readonly loginUserProvider: LoginUserProvider,
) {}

// CREATE USER
async createUser(
createUserDto: CreateUserDto,
response: Response,
): Promise<AuthResponse> {
return await this.usersService.createUser(createUserDto, response);
}

// VALIDATE USER
public async validateUser(
email: string,
password: string,
): Promise<Partial<User>> {
return await this.usersService.validateUser(email, password);
}

// LOGIN USER
public async loginUser(
user: User,
response: Response,
): Promise<AuthResponse> {
return await this.loginUserProvider.loginUser(user, response);
}
}
58 changes: 58 additions & 0 deletions backend/src/auth/providers/findOneRefreshToken.provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {
Injectable,
RequestTimeoutException,
UnauthorizedException,
} from '@nestjs/common';
import { RefreshToken } from '../entities/refreshToken.entity';
import { HashingProvider } from './hashing.provider';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

@Injectable()
export class FindOneRefreshTokenProvider {
constructor(
@InjectRepository(RefreshToken)
private readonly refreshTokenEntity: Repository<RefreshToken>,
private readonly hashingProvider: HashingProvider,
) {}

public async findRefreshToken(userId: string, userRefreshToken: string) {
let userTokens: RefreshToken[];

try {
userTokens = await this.refreshTokenEntity.find({
where: {
user: {
id: userId,
},
},
relations: ['user'],
});
} catch (error) {
throw new RequestTimeoutException('Error connecting to the database');
}

if (!userTokens || userTokens.length === 0) {
throw new UnauthorizedException('Invalid refresh token');
}

// compare provided token with each stored one
for (const tokenEntity of userTokens) {
const isMatch = await this.hashingProvider.compare(
userRefreshToken,
tokenEntity.token,
);

if (isMatch) {
//checkif the token is already revoked
if (tokenEntity.revoked) {
throw new UnauthorizedException('Refresh token is already revoked');
}

return tokenEntity;
}
}

throw new UnauthorizedException('Invalid refresh token');
}
}
Loading
Loading