Skip to content

Commit

Permalink
JWT Gen + Validation & Service integration
Browse files Browse the repository at this point in the history
  • Loading branch information
tmthecoder committed Sep 17, 2024
1 parent 25cdb55 commit 5695a16
Show file tree
Hide file tree
Showing 25 changed files with 314 additions and 121 deletions.
2 changes: 1 addition & 1 deletion packages/api-gateway/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ import { SentryModule } from '@sentry/nestjs/setup';
EmailModule,
],
})
export class AppModule {}
export class AppModule { }
15 changes: 12 additions & 3 deletions packages/api-gateway/src/middleware/email.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const { JWT_SERVICE_NAME } = JwtProto;
export class EmailLinkingMiddleware implements NestMiddleware, OnModuleInit {
private jwtService: JwtProto.JwtServiceClient;

constructor(@Inject(JWT_SERVICE_NAME) private jwtClient: ClientGrpc) {}
constructor(@Inject(JWT_SERVICE_NAME) private jwtClient: ClientGrpc) { }

onModuleInit() {
this.jwtService = this.jwtClient.getService<JwtProto.JwtServiceClient>(
Expand All @@ -27,15 +27,24 @@ export class EmailLinkingMiddleware implements NestMiddleware, OnModuleInit {

async use(req: Request, res: Response, next: NextFunction) {
try {
if (!req.headers.authorization) {
if (
req.headers.authorization === undefined ||
req.headers.authorization.length === 0
) {
throw new Error('No authorization headers');
}
const token = this.extractTokenFromHeader(req);
if (!token) {
throw new Error('Jwt not found');
}

const jwtValidation = this.jwtService.validateJwt({ jwt: token });
await lastValueFrom(jwtValidation);
const res = await lastValueFrom(jwtValidation);

if (!res.valid) {
throw new Error('Invalid JWT');
}

next();
} catch {
throw new HttpException(
Expand Down
2 changes: 1 addition & 1 deletion packages/api-gateway/src/middleware/project.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const { JWT_SERVICE_NAME } = JwtProto;
export class ProjectLinkingMiddleware implements NestMiddleware, OnModuleInit {
private jwtService: JwtProto.JwtServiceClient;

constructor(@Inject(JWT_SERVICE_NAME) private jwtClient: ClientGrpc) {}
constructor(@Inject(JWT_SERVICE_NAME) private jwtClient: ClientGrpc) { }

onModuleInit() {
this.jwtService = this.jwtClient.getService<JwtProto.JwtServiceClient>(
Expand Down
14 changes: 11 additions & 3 deletions packages/api-gateway/src/models/auth.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty } from 'class-validator';
import { ApiKeyProto, IdentifierProto } from 'juno-proto';
import { ApiKeyProto, IdentifierProto, JwtProto } from 'juno-proto';

export class IssueApiKeyRequest {
@ApiProperty({ description: 'Issuing user email' })
Expand All @@ -20,10 +20,18 @@ export class IssueApiKeyRequest {
}

export class IssueApiKeyResponse {
@ApiProperty({ type: 'boolean' })
apiKey: ApiKeyProto.ApiKey;
@ApiProperty({ type: 'string' })
apiKey: string;

constructor(res: ApiKeyProto.IssueApiKeyResponse) {
this.apiKey = res.apiKey;
}
}

export class IssueJWTResponse {
token: string;

constructor(res: JwtProto.CreateJwtResponse) {
this.token = res.jwt;
}
}
32 changes: 24 additions & 8 deletions packages/api-gateway/src/modules/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {
Body,
Controller,
Get,
Headers,
Inject,
OnModuleInit,
Post,
UnauthorizedException,
} from '@nestjs/common';
import { ClientGrpc } from '@nestjs/microservices';
import {
Expand All @@ -16,7 +17,11 @@ import { ApiKeyProto, JwtProto } from 'juno-proto';
import { ApiKeyServiceClient } from 'juno-proto/dist/gen/api_key';
import { JwtServiceClient } from 'juno-proto/dist/gen/jwt';
import { lastValueFrom } from 'rxjs';
import { IssueApiKeyRequest, IssueApiKeyResponse } from 'src/models/auth';
import {
IssueApiKeyRequest,
IssueApiKeyResponse,
IssueJWTResponse,
} from 'src/models/auth';

const { JWT_SERVICE_NAME } = JwtProto;
const { API_KEY_SERVICE_NAME } = ApiKeyProto;
Expand All @@ -29,7 +34,7 @@ export class AuthController implements OnModuleInit {
constructor(
@Inject(JWT_SERVICE_NAME) private jwtClient: ClientGrpc,
@Inject(API_KEY_SERVICE_NAME) private apiClient: ClientGrpc,
) {}
) { }

onModuleInit() {
this.jwtService =
Expand All @@ -38,12 +43,23 @@ export class AuthController implements OnModuleInit {
this.apiClient.getService<ApiKeyServiceClient>(API_KEY_SERVICE_NAME);
}

@Get()
@ApiOperation({
description:
'Thie endpoint issues a new API key for the project tied to the specified environment.',
})
@ApiCreatedResponse({
description: 'The API Key has been successfully created',
type: IssueApiKeyResponse,
})
@Post('/jwt')
@ApiBearerAuth()
getJWT() {
console.log('CALLED');
// this.apiKeyService.issueApiKey({}).subscribe();
return 'test';
async getJWT(@Headers('Authorization') apiKey?: string) {
const key = apiKey?.replace('Bearer ', '');
if (key === undefined) {
throw new UnauthorizedException('API Key is required');
}
const jwt = await lastValueFrom(this.jwtService.createJwt({ apiKey: key }));
return new IssueJWTResponse(jwt);
}

@ApiOperation({
Expand Down
4 changes: 2 additions & 2 deletions packages/api-gateway/src/modules/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Module } from '@nestjs/common';
import { MiddlewareConsumer, Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { AuthController } from './auth.controller';
import { join } from 'path';
Expand Down Expand Up @@ -41,4 +41,4 @@ const { API_KEY_SERVICE_NAME, JUNO_API_KEY_PACKAGE_NAME } = ApiKeyProto;
],
controllers: [AuthController],
})
export class AuthModule {}
export class AuthModule { }
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export class ProjectController implements OnModuleInit {

constructor(
@Inject(PROJECT_SERVICE_NAME) private projectClient: ClientGrpc,
) {}
) { }

onModuleInit() {
this.projectService =
Expand Down Expand Up @@ -83,7 +83,6 @@ export class ProjectController implements OnModuleInit {
const project = this.projectService.getProject({
name,
});

return new ProjectResponse(await lastValueFrom(project));
}

Expand Down
4 changes: 1 addition & 3 deletions packages/api-gateway/src/modules/user/user.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,12 @@ const { USER_SERVICE_NAME, USER_AUTH_SERVICE_NAME, JUNO_USER_PACKAGE_NAME } =
})
export class UserModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(ProjectLinkingMiddleware)
.forRoutes({ path: 'user/id/:id/project', method: RequestMethod.PUT });
consumer
.apply(CredentialsMiddleware)
.forRoutes(
{ path: 'user', method: RequestMethod.POST },
{ path: 'user/type', method: RequestMethod.POST },
{ path: 'user/id/:id/project', method: RequestMethod.PUT },
);
}
}
48 changes: 27 additions & 21 deletions packages/api-gateway/test/email.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ import * as request from 'supertest';
import { ResetProtoFile } from 'juno-proto';
import * as GRPC from '@grpc/grpc-js';
import * as ProtoLoader from '@grpc/proto-loader';
import * as jwt from 'jsonwebtoken';

let app: INestApplication;
const ADMIN_EMAIL = '[email protected]';
const ADMIN_PASSWORD = 'test-password';

let token: string;

beforeAll(async () => {
const proto = ProtoLoader.loadSync([ResetProtoFile]) as any;
Expand Down Expand Up @@ -47,18 +51,38 @@ beforeEach(async () => {
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));

await app.init();

if (!token) {
const key = await request(app.getHttpServer())
.post('/auth/key')
.send({
email: ADMIN_EMAIL,
password: ADMIN_PASSWORD,
environment: 'prod',
project: {
name: 'test-seed-project',
},
});

const apiKey = key.body['apiKey'];

const jwt = await request(app.getHttpServer())
.post('/auth/jwt')
.set('Authorization', `Bearer ${apiKey}`)
.send();

token = jwt.body['token'];
}
});

describe('Email Registration Routes', () => {
it('Registers an email without a body', () => {
const token = jwt.sign({}, 'secret');
return request(app.getHttpServer())
.post('/email/register-sender')
.set('Authorization', 'Bearer ' + token)
.expect(400);
});
it('Has been called with a malformed emaiil', () => {
const token = jwt.sign({}, 'secret');
return request(app.getHttpServer())
.post('/email/register-sender')
.set('Authorization', 'Bearer ' + token)
Expand Down Expand Up @@ -86,7 +110,6 @@ describe('Email Registration Routes', () => {
});
it('Registration endpoint called with a correct payload (header + body)', () => {
// Assuming 'valid.jwt.token' is a placeholder for a valid JWT obtained in a way relevant to your test setup
const token = jwt.sign({}, 'secret');
return request(app.getHttpServer())
.post('/email/register-sender')
.set('Authorization', 'Bearer ' + token)
Expand Down Expand Up @@ -122,7 +145,6 @@ describe('Email Sending Route', () => {
});

it('Send email with valid parameters and JWT is valid', async () => {
const token = jwt.sign({}, 'secret');
return request(app.getHttpServer())
.post('/email/send')
.set('Authorization', 'Bearer ' + token)
Expand All @@ -135,7 +157,6 @@ describe('Email Sending Route', () => {
});

it('Send email with valid parameters (with names) and JWT is valid', async () => {
const token = jwt.sign({}, 'secret');
return request(app.getHttpServer())
.post('/email/send')
.set('Authorization', 'Bearer ' + token)
Expand All @@ -150,7 +171,6 @@ describe('Email Sending Route', () => {
});

it('Send email with valid parameters (with names, multiple recipients) and JWT is valid', async () => {
const token = jwt.sign({}, 'secret');
return request(app.getHttpServer())
.post('/email/send')
.set('Authorization', 'Bearer ' + token)
Expand All @@ -166,7 +186,6 @@ describe('Email Sending Route', () => {
});

it('Send email with valid parameters (with names, multiple recipients and contents) and JWT is valid', async () => {
const token = jwt.sign({}, 'secret');
return request(app.getHttpServer())
.post('/email/send')
.set('Authorization', 'Bearer ' + token)
Expand All @@ -185,7 +204,6 @@ describe('Email Sending Route', () => {
});

it('Send email with empty request and JWT is valid', async () => {
const token = jwt.sign({}, 'secret');
return request(app.getHttpServer())
.post('/email/send')
.set('Authorization', 'Bearer ' + token)
Expand All @@ -194,7 +212,6 @@ describe('Email Sending Route', () => {
});

it('Send email with empty sender email and JWT is valid', async () => {
const token = jwt.sign({}, 'secret');
return request(app.getHttpServer())
.post('/email/send')
.set('Authorization', 'Bearer ' + token)
Expand All @@ -213,7 +230,6 @@ describe('Email Sending Route', () => {
});

it('Send email with invalid sender email and JWT is valid', async () => {
const token = jwt.sign({}, 'secret');
return request(app.getHttpServer())
.post('/email/send')
.set('Authorization', 'Bearer ' + token)
Expand All @@ -232,7 +248,6 @@ describe('Email Sending Route', () => {
});

it('Send email with null sender email and JWT is valid', async () => {
const token = jwt.sign({}, 'secret');
return request(app.getHttpServer())
.post('/email/send')
.set('Authorization', 'Bearer ' + token)
Expand All @@ -251,7 +266,6 @@ describe('Email Sending Route', () => {
});

it('Send email with empty recipients email and JWT is valid', async () => {
const token = jwt.sign({}, 'secret');
return request(app.getHttpServer())
.post('/email/send')
.set('Authorization', 'Bearer ' + token)
Expand All @@ -270,7 +284,6 @@ describe('Email Sending Route', () => {
});

it('Send email with invalid recipients email and JWT is valid', async () => {
const token = jwt.sign({}, 'secret');
return request(app.getHttpServer())
.post('/email/send')
.set('Authorization', 'Bearer ' + token)
Expand All @@ -289,7 +302,6 @@ describe('Email Sending Route', () => {
});

it('Send email with null recipients email and JWT is valid', async () => {
const token = jwt.sign({}, 'secret');
return request(app.getHttpServer())
.post('/email/send')
.set('Authorization', 'Bearer ' + token)
Expand All @@ -308,7 +320,6 @@ describe('Email Sending Route', () => {
});

it('Send email with empty content type email and JWT is valid', async () => {
const token = jwt.sign({}, 'secret');
return request(app.getHttpServer())
.post('/email/send')
.set('Authorization', 'Bearer ' + token)
Expand All @@ -327,7 +338,6 @@ describe('Email Sending Route', () => {
});

it('Send email with null content type email and JWT is valid', async () => {
const token = jwt.sign({}, 'secret');
return request(app.getHttpServer())
.post('/email/send')
.set('Authorization', 'Bearer ' + token)
Expand All @@ -346,7 +356,6 @@ describe('Email Sending Route', () => {
});

it('Send email with empty content value email and JWT is valid', async () => {
const token = jwt.sign({}, 'secret');
return request(app.getHttpServer())
.post('/email/send')
.set('Authorization', 'Bearer ' + token)
Expand All @@ -365,7 +374,6 @@ describe('Email Sending Route', () => {
});

it('Send email with null content value email and JWT is valid', async () => {
const token = jwt.sign({}, 'secret');
return request(app.getHttpServer())
.post('/email/send')
.set('Authorization', 'Bearer ' + token)
Expand All @@ -386,15 +394,13 @@ describe('Email Sending Route', () => {

describe('Domain Registration Routes', () => {
it('Registers a domain without a domain parameter', () => {
const token = jwt.sign({}, 'secret');
return request(app.getHttpServer())
.post('/email/register-domain')
.set('Authorization', 'Bearer ' + token)
.expect(400);
});

it('Registers a domain with valid parameters', () => {
const token = jwt.sign({}, 'secret');
return request(app.getHttpServer())
.post('/email/register-domain')
.set('Authorization', 'Bearer ' + token)
Expand Down
Loading

0 comments on commit 5695a16

Please sign in to comment.