Skip to content

Commit

Permalink
refactor: update api key guard logic (#233)
Browse files Browse the repository at this point in the history
  • Loading branch information
kshitij-k-osmosys authored May 7, 2024
1 parent a646684 commit 984c33e
Show file tree
Hide file tree
Showing 15 changed files with 203 additions and 177 deletions.
59 changes: 2 additions & 57 deletions apps/api/docs/api-documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ This sections lists notification related requests such as creating new notificat

### Create Notification

Allows the user to create a new notification for processing and sending it. Requires passing bearer token for authorization.
Allows the user to create a new notification for processing and sending it. Requires passing `x-api-key` token as header for validation.

**Note:**
- The **Provider** should have a valid `channelType`.
Expand Down Expand Up @@ -110,7 +110,7 @@ Refer the [Available Channel Types](./usage-guide.md#5-available-channel-types)
```sh
curl --location 'http://localhost:3000/notifications' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer mysecuretoken' \
--header 'x-api-key: mysecuretoken' \
--data-raw '{
"providerId": 1,
"data": {
Expand Down Expand Up @@ -306,61 +306,6 @@ curl --location 'http://localhost:3000/graphql' \
}
```

### Fetch Notification by Id

Allows the user to fetch notification based on the passed notificationId.

**Endpoint:** `http://localhost:3000//notifications/{notificationId}`

**Method:** `GET`

**Sample Request:** http://localhost:3000/notifications/2

**Sample Response:**

```json
{
"status": "success",
"data": {
"notification": {
"id": 2,
"providerId": 2,
"channelType": 2,
"data": {
"from": "[email protected]",
"to": "[email protected]",
"subject": "Test subject",
"text": "This is a test notification",
"html": "<b>This is a test notification</b>"
},
"deliveryStatus": 4,
"result": {
"result": {
"accepted": ["[email protected]"],
"rejected": [],
"ehlo": ["PIPELINING", "8BITMIME", "SMTPUTF8", "AUTH LOGIN PLAIN"],
"envelopeTime": 514,
"messageTime": 396,
"messageSize": 598,
"response": "250 Accepted [STATUS=new MSGID=ZO8BDrs4Cney.EXBZcnPdyNjH.7avN-FAAAAmpIr4f.gj7e4YPfAABUQxYg]",
"envelope": {
"from": "[email protected]",
"to": ["[email protected]"]
},
"messageId": "<[email protected]>"
}
},
"createdOn": "2024-04-29T08:14:28.000Z",
"updatedOn": "2024-04-29T08:14:28.000Z",
"createdBy": "sampleFoundationXApp",
"updatedBy": "sampleFoundationXApp",
"status": 1,
"applicationId": 2
}
}
}
```

## Applications

This sections lists application related requests such as creating new application and fetching all applications.
Expand Down
8 changes: 4 additions & 4 deletions apps/api/docs/channels/mailgun.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ Create a new entry in table `notify_providers` and set the fields - `name`, `app

Then set the following configurations in the `configuration` field

| Key | Description |
|-----------------|-----------------|
| MAILGUN_API_KEY | Your Mailgun API key |
| Key | Description |
|-----------------|-----------------------------------------------------------------|
| MAILGUN_API_KEY | Your Mailgun API key |
| MAILGUN_HOST | Mailgun host; api.mailgun.net for US, api.eu.mailgun.net for EU |
| MAILGUN_DOMAIN | Your Mailgun domain name |
| MAILGUN_DOMAIN | Your Mailgun domain name |

```jsonc
// Sample json to set in configuration field
Expand Down
8 changes: 4 additions & 4 deletions apps/api/docs/channels/sms-Plivo.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ Create a new entry in table `notify_providers` and set the fields - `name`, `app

Then set the following configurations in the `configuration` field

| Key | Description |
|-----------------|-----------------|
| PLIVO_SMS_AUTH_ID | Plivo Auth Id |
| PLIVO_SMS_AUTH_TOKEN | Plivo Auth Token |
| Key | Description |
|----------------------|-------------------------|
| PLIVO_SMS_AUTH_ID | Plivo Auth Id |
| PLIVO_SMS_AUTH_TOKEN | Plivo Auth Token |
| PLIVO_SMS_NUMBER | Plivo registered Number |

```jsonc
Expand Down
8 changes: 4 additions & 4 deletions apps/api/docs/channels/sms-Twilio.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ Create a new entry in table `notify_providers` and set the fields - `name`, `app

Then set the following configurations in the `configuration` field

| Key | Description |
|-----------------|-----------------|
| TWILIO_SMS_ACCOUNT_SID | Twilio SMS account SID |
| TWILIO_SMS_AUTH_TOKEN | Twilio SMS auth token |
| Key | Description |
|--------------------------|--------------------------------|
| TWILIO_SMS_ACCOUNT_SID | Twilio SMS account SID |
| TWILIO_SMS_AUTH_TOKEN | Twilio SMS auth token |
| TWILIO_SMS_NUMBER | Twilio registered phone number |

```jsonc
Expand Down
10 changes: 5 additions & 5 deletions apps/api/docs/channels/smtp.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ Create a new entry in table `notify_providers` and set the fields - `name`, `app

Then set the following configurations in the `configuration` field

| Key | Description |
|-----------------|-----------------|
| SMTP_HOST | SMTP server hostname |
| Key | Description |
|-----------------|-------------------------------------------------|
| SMTP_HOST | SMTP server hostname |
| SMTP_PORT | Port number for SMTP (587 for TLS, 465 for SSL) |
| SMTP_USERNAME | Your SMTP username for authentication |
| SMTP_PASSWORD | Your SMTP password for authentication |
| SMTP_USERNAME | Your SMTP username for authentication |
| SMTP_PASSWORD | Your SMTP password for authentication |

```jsonc
// Sample json to set in configuration field
Expand Down
6 changes: 3 additions & 3 deletions apps/api/docs/channels/wa-360Dialog.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ Create a new entry in table `notify_providers` and set the fields - `name`, `app

Then set the following configurations in the `configuration` field

| Key | Description |
|-----------------|-----------------|
| WA_360_DIALOG_API_KEY | WA 360 Dialog api key |
| Key | Description |
|--------------------------|--------------------------------------------------------|
| WA_360_DIALOG_API_KEY | WA 360 Dialog api key |
| WA_360_DIALOG_URL | api url which is https://waba.360dialog.io/v1/messages |

```jsonc
Expand Down
8 changes: 4 additions & 4 deletions apps/api/docs/channels/wa-Twilio.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ Create a new entry in table `notify_providers` and set the fields - `name`, `app

Then set the following configurations in the `configuration` field

| Key | Description |
|-----------------|-----------------|
| TWILIO_WA_ACCOUNT_SID | Twilio SMS account SID |
| TWILIO_WA_AUTH_TOKEN | Twilio SMS auth token |
| Key | Description |
|-------------------------|--------------------------------|
| TWILIO_WA_ACCOUNT_SID | Twilio SMS account SID |
| TWILIO_WA_AUTH_TOKEN | Twilio SMS auth token |
| TWILIO_WA_NUMBER | Twilio registered phone number |

```jsonc
Expand Down
88 changes: 75 additions & 13 deletions apps/api/src/common/guards/api-key/api-key.guard.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import {
BadRequestException,
CanActivate,
ExecutionContext,
Injectable,
Logger,
UnauthorizedException,
} from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { Observable } from 'rxjs';
import { IsEnabledStatus } from 'src/common/constants/database';
import { ProvidersService } from 'src/modules/providers/providers.service';
import { ServerApiKeysService } from 'src/modules/server-api-keys/server-api-keys.service';

@Injectable()
export class ApiKeyGuard implements CanActivate {
constructor(private readonly serverApiKeysService: ServerApiKeysService) {}
constructor(
private readonly serverApiKeysService: ServerApiKeysService,
private readonly providersService: ProvidersService,
private logger: Logger,
) {}

canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
return this.validateRequest(context);
Expand All @@ -14,21 +27,26 @@ export class ApiKeyGuard implements CanActivate {
async validateRequest(execContext: ExecutionContext): Promise<boolean> {
const request = execContext.switchToHttp().getRequest();

// Get auth header incase of http request
// Get api key header incase of http request
if (request && request.headers) {
const authHeader = request.headers['authorization'];
const validationResult = await this.validateAuthHeader(authHeader);
const serverApiKeyHeader = request.headers['x-api-key'];
const requestProviderId = request.body.providerId;
const validationResult = await this.validateApiKeyHeader(
serverApiKeyHeader,
requestProviderId,
);

if (validationResult) {
return true;
}
}

// Get auth header incase of graphql request
// Get api key header incase of graphql request
const ctx = GqlExecutionContext.create(execContext);
const req = ctx.getContext().req;
const authHeader = req.headers.authorization;
const validationResult = await this.validateAuthHeader(authHeader);
const serverApiKeyHeader = req.headers['x-api-key'];
const requestProviderId = request.body.providerId;
const validationResult = await this.validateApiKeyHeader(serverApiKeyHeader, requestProviderId);

if (validationResult) {
return true;
Expand All @@ -37,19 +55,46 @@ export class ApiKeyGuard implements CanActivate {
throw new UnauthorizedException('Invalid API key');
}

async validateAuthHeader(authHeader: string): Promise<boolean> {
async validateApiKeyHeader(
serverApiKeyHeader: string,
requestProviderId: number,
): Promise<boolean> {
let apiKeyToken = null;

if (authHeader.startsWith('Bearer ')) {
apiKeyToken = authHeader.substring(7);
if (serverApiKeyHeader) {
apiKeyToken = serverApiKeyHeader;
} else {
throw new Error('Invalid bearer token format');
this.logger.error('Header x-api-key was not provided');
throw new UnauthorizedException('Header x-api-key was not provided');
}

const apiKeyEntry = await this.serverApiKeysService.findByServerApiKey(apiKeyToken);

if (!apiKeyEntry) {
throw new Error('Invalid token');
//this.logger.error('Invalid x-api-key');
throw new UnauthorizedException('Invalid x-api-key');
}

// Get channel type from providerId & Set the channelType based on providerEntry
const providerEntry = await this.providersService.getById(requestProviderId);

if (!providerEntry) {
this.logger.error('Provider does not exist');
throw new BadRequestException('Provider does not exist');
}

// Check if provider is enabled or not
if (providerEntry.isEnabled != IsEnabledStatus.TRUE) {
this.logger.error(`Provider ${providerEntry.name} is not enabled`);
throw new BadRequestException(`Provider ${providerEntry.name} is not enabled`);
}

// Set correct ApplicationId after verifying
const inputApplicationId = await this.getApplicationIdFromApiKey(apiKeyToken);

if (inputApplicationId != providerEntry.applicationId) {
this.logger.error('The applicationId for Server Key and Provider do not match.');
throw new BadRequestException('The applicationId for Server Key and Provider do not match.');
}

if (apiKeyToken && apiKeyToken === apiKeyEntry.apiKey) {
Expand All @@ -58,4 +103,21 @@ export class ApiKeyGuard implements CanActivate {

return false;
}

// Get correct applicationId using apiKeyToken
async getApplicationIdFromApiKey(apiKeyToken: string): Promise<number> {
try {
const apiKeyEntry = await this.serverApiKeysService.findByServerApiKey(apiKeyToken);

if (!apiKeyEntry || !apiKeyEntry.applicationId) {
this.logger.error('Related Api Key does not exist');
throw new Error('Related Api Key does not exist');
}

return apiKeyEntry.applicationId;
} catch (error) {
this.logger.error(error.message);
throw error;
}
}
}
78 changes: 78 additions & 0 deletions apps/api/src/common/guards/api-key/auth.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {
CanActivate,
ExecutionContext,
Injectable,
Logger,
UnauthorizedException,
} from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { Observable } from 'rxjs';
import { ServerApiKeysService } from 'src/modules/server-api-keys/server-api-keys.service';

@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private readonly serverApiKeysService: ServerApiKeysService,
private logger: Logger,
) {}

canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
return this.validateRequest(context);
}

async validateRequest(execContext: ExecutionContext): Promise<boolean> {
const request = execContext.switchToHttp().getRequest();

// Get auth header incase of http request
if (request && request.headers) {
const authHeader = request.headers['authorization'];
const validationResult = await this.validateAuthHeader(authHeader);

if (validationResult) {
return true;
}
}

// Get auth header incase of graphql request
const ctx = GqlExecutionContext.create(execContext);
const req = ctx.getContext().req;
const authHeader = req.headers.authorization;
const validationResult = await this.validateAuthHeader(authHeader);

if (validationResult) {
return true;
}

throw new UnauthorizedException('Invalid API key');
}

// TODO: validate using jwt token instead of db
async validateAuthHeader(authHeader: string): Promise<boolean> {
if (!authHeader) {
this.logger.error('No bearer token provided');
throw new UnauthorizedException('No bearer token provided');
}

let apiKeyToken = null;

if (authHeader.startsWith('Bearer ')) {
apiKeyToken = authHeader.substring(7);
} else {
this.logger.error('Invalid bearer token format');
throw new UnauthorizedException('Invalid bearer token format');
}

const apiKeyEntry = await this.serverApiKeysService.findByServerApiKey(apiKeyToken);

if (!apiKeyEntry) {
this.logger.error('Invalid token');
throw new UnauthorizedException('Invalid token');
}

if (apiKeyToken && apiKeyToken === apiKeyEntry.apiKey) {
return true;
}

return false;
}
}
Loading

0 comments on commit 984c33e

Please sign in to comment.