Skip to content

Commit 741c1ab

Browse files
authored
Method to validate webhooks (#2)
1 parent 4ecaa0a commit 741c1ab

File tree

8 files changed

+101
-31
lines changed

8 files changed

+101
-31
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@
2424
"ts-jest": "^27.0.7",
2525
"typescript": "^4.4.4"
2626
}
27-
}
27+
}

src/base.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,32 @@
1+
import { Config } from "./types"
2+
3+
// Default environment variables
4+
const ENV_API_KEY = 'NIGHTFALL_API_KEY'
5+
const ENV_WEBHOOK_SIGNING_SECRET = 'NIGHTFALL_WEBHOOK_SIGNING_SECRET'
6+
17
export class Base {
28
protected readonly API_HOST = 'https://api.nightfall.ai'
39
protected readonly API_KEY: string = ''
10+
protected readonly WEBHOOK_SIGNING_SECRET: string = ''
411
protected readonly AXIOS_HEADERS: { [key: string]: string | number } = {}
512

6-
constructor(apiKey?: string) {
7-
if (!apiKey && !process.env.hasOwnProperty('NIGHTFALL_API_KEY')) {
13+
constructor(config?: Config) {
14+
// Set API Key
15+
if (!config?.apiKey && !process.env.hasOwnProperty(ENV_API_KEY)) {
816
throw new Error('Please provide an API Key or configure your key as an environment variable.')
917
}
1018

11-
this.API_KEY = apiKey || process.env.NIGHTFALL_API_KEY as string
19+
this.API_KEY = config?.apiKey || process.env[ENV_API_KEY] as string
20+
21+
// Set webhook signing secret if supplied
22+
if (config?.webhookSigningSecret) {
23+
this.WEBHOOK_SIGNING_SECRET = config.webhookSigningSecret
24+
}
25+
26+
// Attempt to set webhook signing secret from env if not supplied in config
27+
if (!config?.webhookSigningSecret && process.env.hasOwnProperty(ENV_WEBHOOK_SIGNING_SECRET)) {
28+
this.WEBHOOK_SIGNING_SECRET = process.env[ENV_WEBHOOK_SIGNING_SECRET] as string
29+
}
1230

1331
// Set Axios request headers since we will reuse this quite a lot
1432
this.AXIOS_HEADERS = {

src/filesScanner.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import axios, { AxiosResponse } from "axios"
22
import fs from 'fs'
33
import { Base } from './base'
4-
import { ScanFile } from './types'
4+
import { Config, ScanFile } from './types'
55

66
export class FileScanner extends Base {
77
filePath: string
@@ -11,11 +11,10 @@ export class FileScanner extends Base {
1111
/**
1212
* Create an instance of the FileScanner helper.
1313
*
14-
* @param apiKey Your Nightfall API key
1514
* @param filePath The path of the file that needs to be scanned
1615
*/
17-
constructor(apiKey: string, filePath: string) {
18-
super(apiKey)
16+
constructor(config: Config, filePath: string) {
17+
super(config)
1918
this.filePath = filePath
2019
}
2120

src/nightfall.ts

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,25 @@
11
import axios, { AxiosError } from 'axios'
2+
import crypto from 'crypto'
23
import { Base } from './base'
34
import { FileScanner } from './filesScanner'
4-
import { NightfallResponse, NightfallError, ScanText, ScanFile } from './types'
5+
import {
6+
Config, NightfallResponse, NightfallError, ScanText, ScanFile
7+
} from './types'
58

69
export class Nightfall extends Base {
710
/**
8-
* Create an instance of the Nightfall client. Although you can supply
9-
* your API key manually when you initiate the client, we recommend that
10-
* you configure your API key as an environment variable named
11-
* NIGHTFALL_API_KEY. The client automatically reads `process.env.NIGHTFALL_API_KEY`
12-
* when you initiate the client like so: `const client = new Nightfall()`.
11+
* Create an instance of the Nightfall client. Although you can supply your API key and webhook signing secret
12+
* manually when you initiate the client, we recommend that you configure them as an environment variables named
13+
* `NIGHTFALL_API_KEY` and `NIGHTFALL_WEBHOOK_SIGNING_SECRET`. The client automatically reads
14+
* `process.env.NIGHTFALL_API_KEY` and `process.env.NIGHTFALL_WEBHOOK_SIGNING_SECRET` when you initiate the
15+
* client like so: `const client = new Nightfall()`.
1316
*
14-
* @param apiKey Your Nightfall API key
17+
* @param {Object} config An optional object to initialise the client with your key manually
18+
* @param {string} config.apiKey Your Nightfall API Key
19+
* @param {string} config.webhookSigningSecret Your webhook signing secret
1520
*/
16-
constructor(apiKey?: string) {
17-
super(apiKey)
21+
constructor(config?: Config) {
22+
super(config)
1823
}
1924

2025
/**
@@ -65,7 +70,13 @@ export class Nightfall extends Base {
6570
*/
6671
async scanFile(filePath: string, policy: ScanFile.ScanPolicy, requestMetadata?: string): Promise<NightfallResponse<ScanFile.ScanResponse>> {
6772
try {
68-
const fileScanner = new FileScanner(this.API_KEY, filePath)
73+
const fileScanner = new FileScanner(
74+
{
75+
apiKey: this.API_KEY,
76+
webhookSigningSecret: this.WEBHOOK_SIGNING_SECRET,
77+
},
78+
filePath,
79+
)
6980
await fileScanner.initialize()
7081
await fileScanner.uploadChunks()
7182
await fileScanner.finish()
@@ -83,4 +94,38 @@ export class Nightfall extends Base {
8394
return Promise.reject(error)
8495
}
8596
}
97+
98+
/**
99+
* A helper method to validate incoming webhook requests from Nightfall. In order to use this method, you
100+
* must initialize the Nightfall client with your `webhookSigningSecret` or declare your signing secret as
101+
* an environment variable called `NIGHTFALL_WEBHOOK_SIGNING_SECRET`. For more information,
102+
* visit https://docs.nightfall.ai/docs/creating-a-webhook-server#webhook-signature-verification.
103+
*
104+
* @param requestBody The webhook request body
105+
* @param requestSignature The value of the X-Nightfall-Signature header
106+
* @param requestTimestamp The value of the X-Nightfall-Timestamp header
107+
* @param threshold Optional - The threshold in seconds. Defaults to 300 seconds (5 minutes).
108+
* @returns A boolean that indicates whether the webhook is valid
109+
*/
110+
validateWebhook(requestBody: ScanFile.WebhookBody, requestSignature: string, requestTimestamp: number, threshold?: number): boolean {
111+
// Do not continue if the client isn't initialized with a webhook signing secret
112+
if (!this.WEBHOOK_SIGNING_SECRET && !process.env.hasOwnProperty('NIGHTFALL_WEBHOOK_SIGNING_SECRET')) {
113+
throw new Error('Please initialize the Nightfall client with a webhook signing secret or configure your signing secret as an environment variable.')
114+
}
115+
116+
// First verify that the request occurred recently (before the configured threshold time)
117+
// to protect against replay attacks
118+
const defaultThreshold = threshold || 300
119+
const now = Math.round(new Date().getTime() / 1000)
120+
const thresholdTimestamp = now - defaultThreshold
121+
if (requestTimestamp < thresholdTimestamp || requestTimestamp > now) {
122+
return false
123+
}
124+
125+
// Validate request signature using the signing secret
126+
const hashPayload = `${requestTimestamp}:${JSON.stringify(requestBody)}`
127+
const computedSignature = crypto.createHmac('sha256', this.WEBHOOK_SIGNING_SECRET).update(hashPayload).digest('hex')
128+
129+
return computedSignature === requestSignature
130+
}
86131
}

src/tests/scanText.test.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,16 @@ import { Nightfall } from '../nightfall'
22
import { creditCardConfig, creditCardPayload, errorResponse } from './mocks'
33

44
describe('should test the text scanning method', () => {
5-
// Set API key and other dependencies
6-
if (!process.env.NIGHTFALL_API_KEY) {
7-
throw new Error("NIGHTFALL_API_KEY environment variable is required")
8-
}
9-
10-
const apiKey = process.env.NIGHTFALL_API_KEY
11-
125
// Run tests
136
it('should create a new nightfall client and check if the scanText method exists', () => {
14-
const client = new Nightfall(apiKey)
7+
const client = new Nightfall()
158

169
expect(client).toBeDefined()
1710
expect(typeof client.scanText).toBe('function')
1811
})
1912

2013
it('should return an error if the request was configured incorrectly', async () => {
21-
const client = new Nightfall(apiKey)
14+
const client = new Nightfall()
2215
const scanTextSpy = jest.spyOn(client, 'scanText')
2316

2417
const response = await client.scanText(creditCardPayload, {})
@@ -30,7 +23,7 @@ describe('should test the text scanning method', () => {
3023
})
3124

3225
it('should return findings', async () => {
33-
const client = new Nightfall(apiKey)
26+
const client = new Nightfall()
3427
const scanTextSpy = jest.spyOn(client, 'scanText')
3528

3629
const response = await client.scanText(creditCardPayload, creditCardConfig)

src/types/global.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
// Params to initiate the client
2+
export type Config = {
3+
apiKey?: string
4+
webhookSigningSecret?: string
5+
}
6+
17
export interface NightfallError {
28
code: number;
39
message: string;
@@ -27,4 +33,4 @@ export class NightfallResponse<T> {
2733
getError() {
2834
return this.error
2935
}
30-
}
36+
}

src/types/scanFile.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Detector } from ".";
1+
import { Detector, NightfallError } from ".";
22

33
export namespace ScanFile {
44
export interface ScanRequest {
@@ -30,4 +30,13 @@ export namespace ScanFile {
3030
id: string
3131
message: string
3232
}
33+
34+
export interface WebhookBody {
35+
findingsURL: string;
36+
validUntil: string;
37+
uploadID: string;
38+
findingsPresent: boolean;
39+
requestMetadata: string;
40+
errors: NightfallError[];
41+
}
3342
}

src/types/scanText.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,4 @@ export namespace ScanText {
3838
start: number
3939
end: number
4040
}
41-
}
41+
}

0 commit comments

Comments
 (0)