Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dev #83

Merged
merged 3 commits into from
Mar 20, 2024
Merged

Dev #83

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
2 changes: 1 addition & 1 deletion .graphqlrc.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"schema": "https://localhost:3000/graphql",
"schema": "https://localhost:4200/graphql",
"documents": "apps/api/src/schema.gql"
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ You can also view node execution logs by running `docker logs set-nx -f -n 100`

You can now navigate to:

- 👹 Apollo Studio on https://localhost:3000/graphql
- 👹 Apollo Studio on https://localhost:4200/graphql
- 🧜🏻‍♀️ Frontend on https://localhost:4200

## Migrations
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/module/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { User } from '../user/user.entity';
import { AuthController } from './auth.controller';
import { JwtGuard } from './guard/jwt.guard';
import { JwtStrategyService } from './strategy/jwt.strategy.service';
import { CheckJwtRefreshTokenUseCase } from './use-case/check-jwt-refresh-token.use-case';
import { CreateJwtAccessTokenUseCase } from './use-case/create-jwt-access-token.use-case';
import { CreateJwtRefreshTokenUseCase } from './use-case/create-jwt-refresh-token.use-case';
import { LoginUseCase } from './use-case/login.use-case';
Expand Down Expand Up @@ -42,6 +43,7 @@ import { SetJwtTokenUseCase } from './use-case/set-jwt-token.use-case';
JwtStrategyService,
HashPasswordUseCase,
CheckPasswordUseCase,
CheckJwtRefreshTokenUseCase,
CreateJwtAccessTokenUseCase,
CreateJwtRefreshTokenUseCase,
SetJwtTokenUseCase,
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/module/auth/guard/jwt.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export class JwtGuard extends AuthGuard('jwt') {

handleRequest<T>(error: Error, payload: T | false): T | undefined {
if (error) {
throw new UnauthorizedException('invalid bearer', 'authorization');
throw new UnauthorizedException('invalid access token', 'jwt');
}

return payload || undefined;
Expand Down
8 changes: 6 additions & 2 deletions apps/api/src/module/auth/guard/roles.guard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ describe('RolesGuard', () => {
const result = guard.canActivate(executionContext as ExecutionContext);

expect(result).rejects.toThrow(UnauthorizedException);
expect(result).rejects.toMatchObject({ response: { authorization: 'invalid bearer' } });
expect(result).rejects.toMatchObject({
response: { authorization: 'x-jwt-access-token invalid' },
});
});

it('throw UnauthorizedException when context has UserRole but args does not have User', async () => {
Expand All @@ -50,7 +52,9 @@ describe('RolesGuard', () => {
const result = guard.canActivate(executionContext as ExecutionContext);

expect(result).rejects.toThrow(UnauthorizedException);
expect(result).rejects.toMatchObject({ response: { authorization: 'invalid bearer' } });
expect(result).rejects.toMatchObject({
response: { authorization: 'x-jwt-access-token invalid' },
});
});

it('throw ForbiddenException when context has UserRole but args User has no privileges', async () => {
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/module/auth/guard/roles.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class RolesGuard implements CanActivate {
}>().req.user;

if (!user) {
throw new UnauthorizedException('invalid bearer', 'authorization');
throw new UnauthorizedException('x-jwt-access-token invalid', 'authorization');
}

if (roles.length === 0) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { JsonWebTokenError, JwtModule, JwtService } from '@nestjs/jwt';
import { Test, TestingModule } from '@nestjs/testing';
import { mock } from 'jest-mock-extended';

import { ConfigurationService } from '../../../shared/module/config/configuration.service';
import { JwtFactory } from '../../../shared/module/config/factories/jwt.factory';
import { EditableDate } from '../../../shared/util/editable-date';

import { CheckJwtRefreshTokenUseCase } from './check-jwt-refresh-token.use-case';

describe('CheckJwtRefreshTokenUseCase', () => {
let module: TestingModule;
let useCase: CheckJwtRefreshTokenUseCase;

const jwtServiceVerify = jest.spyOn(JwtService.prototype, 'verify');
const validJwtDate = new Date('2024-03-20T22:10:42.000Z');
const fakeJwtRefreshToken =
'eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MTA5NzE1NjgsIm5iZiI6MTcxMDk3MjQ2OCwiZXhwIjoxNzEyMjY3NTY4LCJhdWQiOiJ0ZXN0LXJ1bm5lci1yZWZyZXNoLXRva2VuIiwiaXNzIjoidGVzdCIsInN1YiI6InVzZXItdXVpZCIsImp0aSI6InV1aWQifQ.2hP7NbBjKGpUJ3rPLFo3qIhpTeSAVwl7uTW1gd4n1lutBpEwxIcSmi_WALMENNq9yl4Lkf2vjbhGxsLhx7WJJQ';

const prepareTestingModule = async (
configurationService: ConfigurationService,
): Promise<void> => {
module = await Test.createTestingModule({
imports: [
JwtModule.registerAsync({
useFactory: () => new JwtFactory(configurationService).createJwtOptions(),
}),
],
providers: [
CheckJwtRefreshTokenUseCase,
{ provide: ConfigurationService, useValue: configurationService },
],
}).compile();

useCase = await module.resolve(CheckJwtRefreshTokenUseCase);
};

beforeEach(async () => {
const configurationService = mock<ConfigurationService>({
jwt: {
secret: 'secret',
id: 'uuid',
audience: 'test-runner',
issuer: 'test',
},
});

await prepareTestingModule(configurationService);
});

it('should be defined', () => {
expect(useCase).toBeDefined();
});

describe('execute', () => {
it('throw JsonWebTokenError with "invalid signature" when secret is not valid', async () => {
const configurationService = mock<ConfigurationService>({
jwt: {
secret: 'invalid secret',
id: 'uuid',
audience: 'test-runner',
issuer: 'test',
},
});

await prepareTestingModule(configurationService);

expect(() => useCase.execute(fakeJwtRefreshToken)).toThrow(
new JsonWebTokenError('invalid signature'),
);

expect(jwtServiceVerify).toHaveBeenCalledTimes(1);
expect(jwtServiceVerify).toHaveBeenCalledWith(fakeJwtRefreshToken, {
jwtid: 'uuid',
audience: 'test-runner-refresh-token',
issuer: 'test',
});
});

it('throw JsonWebTokenError with "jwt jwtid invalid" when id is not valid', async () => {
jest.useFakeTimers().setSystemTime(validJwtDate);

const configurationService = mock<ConfigurationService>({
jwt: {
secret: 'secret',
id: 'iduu',
audience: 'test-runner',
issuer: 'test',
},
});

await prepareTestingModule(configurationService);

expect(() => useCase.execute(fakeJwtRefreshToken)).toThrow(
new JsonWebTokenError('jwt jwtid invalid. expected: iduu'),
);

expect(jwtServiceVerify).toHaveBeenCalledTimes(1);
expect(jwtServiceVerify).toHaveBeenCalledWith(fakeJwtRefreshToken, {
jwtid: 'iduu',
audience: 'test-runner-refresh-token',
issuer: 'test',
});
});

it('throw JsonWebTokenError with "jwt audience invalid" when audience is not valid', async () => {
jest.useFakeTimers().setSystemTime(validJwtDate);

const configurationService = mock<ConfigurationService>({
jwt: {
secret: 'secret',
id: 'uuid',
audience: 'test-mock',
issuer: 'test',
},
});

await prepareTestingModule(configurationService);

expect(() => useCase.execute(fakeJwtRefreshToken)).toThrow(
new JsonWebTokenError('jwt audience invalid. expected: test-mock-refresh-token'),
);

expect(jwtServiceVerify).toHaveBeenCalledTimes(1);
expect(jwtServiceVerify).toHaveBeenCalledWith(fakeJwtRefreshToken, {
jwtid: 'uuid',
audience: 'test-mock-refresh-token',
issuer: 'test',
});
});

it('throw JsonWebTokenError with "jwt issuer invalid" when audience is not valid', async () => {
jest.useFakeTimers().setSystemTime(validJwtDate);

const configurationService = mock<ConfigurationService>({
jwt: {
secret: 'secret',
id: 'uuid',
audience: 'test-runner',
issuer: 'spec',
},
});

await prepareTestingModule(configurationService);

expect(() => useCase.execute(fakeJwtRefreshToken)).toThrow(
new JsonWebTokenError('jwt issuer invalid. expected: spec'),
);

expect(jwtServiceVerify).toHaveBeenCalledTimes(1);
expect(jwtServiceVerify).toHaveBeenCalledWith(fakeJwtRefreshToken, {
jwtid: 'uuid',
audience: 'test-runner-refresh-token',
issuer: 'spec',
});
});

it('throw JsonWebTokenError with "jwt not active" when system date is below jwt notBefore time', async () => {
jest.useFakeTimers().setSystemTime(new EditableDate(validJwtDate).edit('day', -1));

expect(() => useCase.execute(fakeJwtRefreshToken)).toThrow(
new JsonWebTokenError('jwt not active'),
);

expect(jwtServiceVerify).toHaveBeenCalledTimes(1);
expect(jwtServiceVerify).toHaveBeenCalledWith(fakeJwtRefreshToken, {
jwtid: 'uuid',
audience: 'test-runner-refresh-token',
issuer: 'test',
});
});

it('throw JsonWebTokenError with "jwt not active" when system date is above jwt expiration time', async () => {
jest.useFakeTimers().setSystemTime(new EditableDate(validJwtDate).edit('month', 1));

expect(() => useCase.execute(fakeJwtRefreshToken)).toThrow(
new JsonWebTokenError('jwt expired'),
);

expect(jwtServiceVerify).toHaveBeenCalledTimes(1);
expect(jwtServiceVerify).toHaveBeenCalledWith(fakeJwtRefreshToken, {
jwtid: 'uuid',
audience: 'test-runner-refresh-token',
issuer: 'test',
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';

import { ConfigurationService } from '../../../shared/module/config/configuration.service';
import { JsonWebToken } from '../definition/json-web-token.interface';

@Injectable()
export class CheckJwtRefreshTokenUseCase {
constructor(
private readonly jwtService: JwtService,
private readonly configurationService: ConfigurationService,
) {}

execute(token: string): JsonWebToken {
return this.jwtService.verify(token, {
jwtid: this.configurationService.jwt.id,
audience: `${this.configurationService.jwt.audience}-refresh-token`,
issuer: this.configurationService.jwt.issuer,
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ describe('CreateJwtRefreshTokenUseCase', () => {
it('should return valid Jwt', async () => {
const jwtToken = useCase.execute('user-uuid');

console.log({ jwtToken });

const parts = jwtToken.split('.');

expect(jwtServiceSign).toHaveBeenCalledTimes(1);
Expand Down
19 changes: 11 additions & 8 deletions apps/api/src/module/auth/use-case/refresh-session.use-case.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { REQUEST } from '@nestjs/core';
import { JwtModule, JwtService } from '@nestjs/jwt';
import { JwtModule } from '@nestjs/jwt';
import { Test, TestingModule } from '@nestjs/testing';
import { Request } from 'express';
import { mock } from 'jest-mock-extended';
Expand All @@ -8,6 +8,7 @@ import { UnauthorizedException } from '../../../shared/exception/unauthorized-ex
import { ConfigurationService } from '../../../shared/module/config/configuration.service';
import { JwtFactory } from '../../../shared/module/config/factories/jwt.factory';

import { CheckJwtRefreshTokenUseCase } from './check-jwt-refresh-token.use-case';
import { CreateJwtAccessTokenUseCase } from './create-jwt-access-token.use-case';
import { CreateJwtRefreshTokenUseCase } from './create-jwt-refresh-token.use-case';
import { RefreshSessionUseCase } from './refresh-session.use-case';
Expand All @@ -21,7 +22,7 @@ describe('RefreshSessionUseCase', () => {
let module: TestingModule;
let useCase: RefreshSessionUseCase;

const jwtServiceVerify = jest.spyOn(JwtService.prototype, 'verify');
const checkJwtRefreshTokenExecute = jest.spyOn(CheckJwtRefreshTokenUseCase.prototype, 'execute');

const createAccessToken = mock<CreateJwtAccessTokenUseCase>();
const createRefreshToken = mock<CreateJwtRefreshTokenUseCase>();
Expand All @@ -40,7 +41,9 @@ describe('RefreshSessionUseCase', () => {
],
providers: [
RefreshSessionUseCase,
CheckJwtRefreshTokenUseCase,
{ provide: REQUEST, useValue: request },
{ provide: ConfigurationService, useValue: configurationService },
{ provide: CreateJwtAccessTokenUseCase, useValue: createAccessToken },
{ provide: CreateJwtRefreshTokenUseCase, useValue: createRefreshToken },
{ provide: SetJwtTokenUseCase, useValue: setJwtToken },
Expand Down Expand Up @@ -74,7 +77,7 @@ describe('RefreshSessionUseCase', () => {
it('throw UnauthorizedException when the request missing cookies', async () => {
const result = useCase.execute();

expect(result).rejects.toThrowError(UnauthorizedException);
expect(result).rejects.toThrow(UnauthorizedException);
expect(result).rejects.toMatchObject({ response: { refreshToken: 'jwt must be provided' } });
});

Expand All @@ -83,11 +86,11 @@ describe('RefreshSessionUseCase', () => {

const result = useCase.execute();

expect(result).rejects.toThrowError(UnauthorizedException);
expect(result).rejects.toThrow(UnauthorizedException);
expect(result).rejects.toMatchObject({ response: { refreshToken: 'jwt malformed' } });

expect(jwtServiceVerify).toHaveBeenCalledTimes(1);
expect(jwtServiceVerify).toHaveBeenCalledWith('wrong-refresh-token');
expect(checkJwtRefreshTokenExecute).toHaveBeenCalledTimes(1);
expect(checkJwtRefreshTokenExecute).toHaveBeenCalledWith('wrong-refresh-token');
});

it('should call two times to setJwtToken useCase', async () => {
Expand All @@ -114,8 +117,8 @@ describe('RefreshSessionUseCase', () => {

expect(result).toStrictEqual(undefined);

expect(jwtServiceVerify).toHaveBeenCalledTimes(1);
expect(jwtServiceVerify).toHaveBeenCalledWith(fakeRefreshToken);
expect(checkJwtRefreshTokenExecute).toHaveBeenCalledTimes(1);
expect(checkJwtRefreshTokenExecute).toHaveBeenCalledWith(fakeRefreshToken);

expect(createAccessToken.execute).toHaveBeenCalledTimes(1);
expect(createAccessToken.execute).toHaveBeenCalledWith('user-uuid');
Expand Down
7 changes: 3 additions & 4 deletions apps/api/src/module/auth/use-case/refresh-session.use-case.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { Inject, Injectable, Scope } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';

import { UnauthorizedException } from '../../../shared/exception/unauthorized-exception.exception';
import { JsonWebToken } from '../definition/json-web-token.interface';
import { JwtCookie } from '../definition/jwt-cookie.enum';

import { CheckJwtRefreshTokenUseCase } from './check-jwt-refresh-token.use-case';
import { CreateJwtAccessTokenUseCase } from './create-jwt-access-token.use-case';
import { CreateJwtRefreshTokenUseCase } from './create-jwt-refresh-token.use-case';
import { SetJwtTokenUseCase } from './set-jwt-token.use-case';
Expand All @@ -15,7 +14,7 @@ import { SetJwtTokenUseCase } from './set-jwt-token.use-case';
export class RefreshSessionUseCase {
constructor(
@Inject(REQUEST) private readonly request: Request,
private readonly jwtService: JwtService,
private readonly checkrefreshToken: CheckJwtRefreshTokenUseCase,
private readonly createAccessToken: CreateJwtAccessTokenUseCase,
private readonly createRefreshToken: CreateJwtRefreshTokenUseCase,
private readonly setJwtToken: SetJwtTokenUseCase,
Expand All @@ -25,7 +24,7 @@ export class RefreshSessionUseCase {
try {
const currentRefreshToken = this.request.signedCookies[JwtCookie.refresh];

const refreshTokenPayload = this.jwtService.verify(currentRefreshToken) as JsonWebToken;
const refreshTokenPayload = this.checkrefreshToken.execute(currentRefreshToken);

const accessToken = this.createAccessToken.execute(refreshTokenPayload.sub);
const refreshToken = this.createRefreshToken.execute(refreshTokenPayload.sub);
Expand Down
Loading