Skip to content

Commit

Permalink
Enhance security and refacto (#17)
Browse files Browse the repository at this point in the history
* delete old user provider

* enhance security, move login and register to auth

* close app after test

Co-authored-by: abdelillah <aghomari.professionnel@gmail.com>
  • Loading branch information
abdelillah-tech and abdelillah authored Feb 28, 2022
1 parent ecfb747 commit 003df0e
Show file tree
Hide file tree
Showing 27 changed files with 603 additions and 418 deletions.
17 changes: 17 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,15 @@
"@nestjs/passport": "^8.2.1",
"@nestjs/platform-express": "^8.0.0",
"@nestjs/swagger": "^5.2.0",
"@types/cookie-parser": "^1.4.2",
"@types/hapi__joi": "^17.1.8",
"@types/mongodb": "^4.0.7",
"@types/validator": "^13.7.1",
"apollo-server-express": "^3.6.3",
"bcrypt": "^5.0.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.13.2",
"cookie-parser": "^1.4.6",
"graphql": "^16.3.0",
"mongoose": "^6.2.2",
"passport": "^0.5.2",
Expand Down
3 changes: 3 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import { UsersModule } from './users/users.module';
MONGO_PASSWORD: Joi.string().required(),
MONGO_DATABASE: Joi.string().required(),
MONGO_PORT: Joi.number().required(),
JWT_SECRET: Joi.string().required(),
JWT_EXPIRATION_TIME: Joi.string().required(),
JWT_ACCESS_TOKEN_EXPIRATION_TIME: Joi.string().required(),
PORT: Joi.number(),
})
}),
Expand Down
102 changes: 102 additions & 0 deletions src/auth/auth.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { Test, TestingModule } from '@nestjs/testing';
import { mockCreateResponse, mockCreateUser, mockUsers } from '../test-utils/data/data-test';
import { AuthController } from './auth.controller';
import { UsersService } from '../users/users.service';
import { AuthService } from './auth.service';
import { JwtService } from '@nestjs/jwt';
import mockedJwtService from '../test-utils/mocks/jwt-mock.service';
import * as request from 'supertest';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as passport from 'passport';
import { LocalStrategy } from './strategies/local.strategy';

describe('AuthController', () => {
let app: INestApplication;
const loginUser = {
email: mockUsers[0].email,
password: 'Super123+',
};

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AuthController],
providers: [
AuthService,
LocalStrategy,
{
provide: JwtService,
useValue: mockedJwtService,
},
{
provide: ConfigService,
useValue: {
get: jest.fn().mockReturnValue('60s'),
}
},
{
provide: UsersService,
useValue: {
findUserByEmail: (email: string) => mockUsers.find(user => user.email === email),
create: () => mockCreateResponse
},
},
],
})
.compile();

app = module.createNestApplication();
app.useGlobalPipes(new ValidationPipe());
app.use(passport.initialize())
await app.init();
});

describe('login', () => {

it('should return 401 if username does not exist', () => {
return request(app.getHttpServer())
.post('/auth/login')
.send({email: 'unknown', password: 'anything'})
.expect(401)
})

it('should return 401 if password is incorrect', () => {
return request(app.getHttpServer())
.post('/auth/login')
.send({email: mockUsers[0].email, password: 'anything'})
.expect(401)
})

it('should return a user with jwt', () => {

const result = {
_id: mockUsers[0]._id,
email: mockUsers[0].email,
username: mockUsers[0].username
};

return request(app.getHttpServer())
.post('/auth/login')
.send(loginUser)
.expect(201)
.expect(result)
});
});

describe('register', () => {

it('should return a user without password', () => {

return request(app.getHttpServer())
.post('/auth/register')
.send(mockCreateUser)
.expect(201)
.expect(mockCreateResponse);
});
});

afterAll(done => {
app.close();
done();
})
});
38 changes: 38 additions & 0 deletions src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Body, Controller, Post, Request, Res, UseGuards } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalAuthGuard } from './guards/local-auth.guard';
import { Response } from 'express';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { UsersService } from '../users/users.service';
import { CreateUserDto } from '../users/dto/create-user.dto';
import { IRequestWithUser } from '../users/interfaces/request-with-user.interface';

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

@Post('register')
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}

@UseGuards(LocalAuthGuard)
@Post('login')
login(@Request() request: IRequestWithUser, @Res() response: Response) {
const { user } = request;
const cookie = this.authService.getCookieWithJwtToken(user._id);
response.setHeader('Set-Cookie', cookie);
user.password = undefined;
return response.send(user);
}

@UseGuards(JwtAuthGuard)
@Post('logout')
async logOut(@Request() request: IRequestWithUser, @Res() response: Response) {
response.setHeader('Set-Cookie', this.authService.getCookieForLogOut());
return response.sendStatus(200);
}
}
26 changes: 14 additions & 12 deletions src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { jwtConstants } from './constants';
import { JwtStrategy } from './strategies/jwt.strategy';
import { LocalStrategy } from './strategies/local.strategy';
import { AuthController } from './auth.controller';
import { UsersModule } from '../users/users.module';

@Module({
imports: [
PassportModule,
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: '60s' },
}),
],
providers: [AuthService, JwtStrategy],
exports: [AuthService],
controllers: [],
imports: [
ConfigModule,
PassportModule,
UsersModule,
JwtModule.register({}),
],
providers: [AuthService, JwtStrategy, LocalStrategy],
exports: [AuthService],
controllers: [AuthController],
})
export class AuthModule {}
export class AuthModule { }
72 changes: 53 additions & 19 deletions src/auth/auth.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,64 @@
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { Test, TestingModule } from '@nestjs/testing';
import { mockUsers } from '../test-utils/data/data-test';
import mockedJwtService from '../test-utils/mocks/jwt-mock.service';
import { UsersService } from '../users/users.service';
import { AuthService } from './auth.service';

describe('AuthService', () => {
let service: AuthService;
let service: AuthService;
let get: jest.Mock;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
AuthService,
{
provide: JwtService,
useValue: mockedJwtService,
},
{
provide: ConfigService,
useValue: {
get: jest.fn().mockReturnValue('60s'),
}
},
{
provide: UsersService,
useValue: {
findUserByEmail: jest.fn().mockReturnValue(mockUsers[0])
},
},
],
}).compile();

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
AuthService,
{
provide: JwtService,
useValue: mockedJwtService,
},
],
}).compile();
service = module.get<AuthService>(AuthService);
});

describe('when ask for a cookie', () => {
it('should return a string', () => {
get
let userId = '0';
let expectedResult = 'Authentication=mercure23beta; HttpOnly; Path=/; Max-Age=60s';
let jwt = service.getCookieWithJwtToken(userId);
expect(jwt).toBe(expectedResult);
});
});

service = module.get<AuthService>(AuthService);
});
describe('when logout', () => {
it('should return an empty', () => {
let expectedResult = "Authentication=; HttpOnly; Path=/; Max-Age=0";
let jwt = service.getCookieForLogOut();
expect(jwt).toBe(expectedResult);
});
});

describe('when login', () => {
it('should return a string', async () => {
const userId = '1';
let jwt = await service.login(userId);
expect(jwt).toBe('mercure23beta');
describe('when ask for authenticated user', () => {
it('should return a user', async () => {
let userEmail = '96abdou96@gmail.com';
let userPassword = 'Super123+';
let user = await service.getAuthenticatedUser(userEmail, userPassword);
expect(user).toBe(mockUsers[0]);
});
});
});
});
46 changes: 40 additions & 6 deletions src/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,46 @@
import { Injectable } from '@nestjs/common';
import { Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { IUser } from '../users/interfaces/user.interface';
import { UsersService } from '../users/users.service';

@Injectable()
export class AuthService {
constructor(private jwtService: JwtService) {}
constructor(
private jwtService: JwtService,
private configService: ConfigService,
private usersService: UsersService
) { }

async login(userId: string): Promise<string> {
const payload = { userId };
return this.jwtService.sign(payload);
}
public getCookieWithJwtToken(userId: string): string {
const payload = { userId };
const token = this.jwtService.sign(payload, {
secret: this.configService.get('JWT_SECRET'),
expiresIn: `${this.configService.get('JWT_EXPIRATION_TIME')}`
});
return `Authentication=${token}; HttpOnly; Path=/; Max-Age=${this.configService.get('JWT_EXPIRATION_TIME')}`;
}

public async getAuthenticatedUser(email: string, password: string): Promise<IUser> {
const user = await this.usersService.findUserByEmail(email);
if (!user) {
throw new UnauthorizedException("User doesn't exist");
}
await this.checkPassword(password, user);
return user;
}

public getCookieForLogOut() {
return `Authentication=; HttpOnly; Path=/; Max-Age=0`;
}


async checkPassword(password: string, user: IUser): Promise<boolean> {
const match = await bcrypt.compare(password, user.password);
if (!match) {
throw new UnauthorizedException('Wrong email or password.');
}
return match;
}
}
12 changes: 0 additions & 12 deletions src/auth/guards/current-user.guard.ts

This file was deleted.

5 changes: 5 additions & 0 deletions src/auth/guards/local-auth.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}
Loading

0 comments on commit 003df0e

Please sign in to comment.