Skip to content

Commit ca19400

Browse files
authored
feat(gatehub): validate webhook signature (#1702)
* feat(gatehub): validate webhook signature * fix: return 200 on initial request
1 parent 398430a commit ca19400

8 files changed

Lines changed: 57 additions & 2 deletions

File tree

docker/dev/.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ RATE_API_KEY=
88

99
GATEHUB_ACCESS_KEY=
1010
GATEHUB_SECRET_KEY=
11+
GATEHUB_WEBHOOK_SECRET=
1112
GATEHUB_GATEWAY_UUID=
1213
GATEHUB_VAULT_UUID_EUR=
1314
GATEHUB_VAULT_UUID_USD=

docker/dev/docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ services:
5252
KRATOS_ADMIN_URL: 'http://kratos:4434/admin'
5353
GATEHUB_ACCESS_KEY: ${GATEHUB_ACCESS_KEY}
5454
GATEHUB_SECRET_KEY: ${GATEHUB_SECRET_KEY}
55+
GATEHUB_WEBHOOK_SECRET: ${GATEHUB_WEBHOOK_SECRET}
5556
GATEHUB_GATEWAY_UUID: ${GATEHUB_GATEWAY_UUID}
5657
GATEHUB_VAULT_UUID_EUR: ${GATEHUB_VAULT_UUID_EUR}
5758
GATEHUB_VAULT_UUID_USD: ${GATEHUB_VAULT_UUID_USD}

docker/prod/.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ WALLET_BACKEND_AUTH_DOMAIN=
2929
WALLET_BACKEND_GATEHUB_ENV=
3030
WALLET_BACKEND_GATEHUB_ACCESS_KEY=
3131
WALLET_BACKEND_GATEHUB_SECRET_KEY=
32+
WALLET_BACKEND_GATEHUB_WEBHOOK_SECRET=
3233
WALLET_BACKEND_GATEHUB_GATEWAY_UUID=
3334
WALLET_BACKEND_GATEHUB_VAULT_UUID_EUR=
3435
WALLET_BACKEND_GATEHUB_VAULT_UUID_USD=

docker/prod/docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ services:
6464
GATEHUB_ENV: ${WALLET_BACKEND_GATEHUB_ENV}
6565
GATEHUB_ACCESS_KEY: ${WALLET_BACKEND_GATEHUB_ACCESS_KEY}
6666
GATEHUB_SECRET_KEY: ${WALLET_BACKEND_GATEHUB_SECRET_KEY}
67+
GATEHUB_WEBHOOK_SECRET: ${WALLET_BACKEND_GATEHUB_WEBHOOK_SECRET}
6768
GATEHUB_GATEWAY_UUID: ${WALLET_BACKEND_GATEHUB_GATEWAY_UUID}
6869
GATEHUB_VAULT_UUID_EUR: ${WALLET_BACKEND_GATEHUB_VAULT_UUID_EUR}
6970
GATEHUB_VAULT_UUID_USD: ${WALLET_BACKEND_GATEHUB_VAULT_UUID_USD}

packages/wallet/backend/src/app.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { GateHubService } from '@/gatehub/service'
4444
import { CardController } from './card/controller'
4545
import { CardService } from './card/service'
4646
import { isRafikiSignedWebhook } from '@/middleware/isRafikiSignedWebhook'
47+
import { isGateHubSignedWebhook } from '@/middleware/isGateHubSignedWebhook'
4748

4849
export interface Bindings {
4950
env: Env
@@ -309,7 +310,11 @@ export class App {
309310

310311
// GateHub
311312
router.get('/iframe-urls/:type', isAuth, gateHubController.getIframeUrl)
312-
router.post('/gatehub-webhooks', gateHubController.webhook)
313+
router.post(
314+
'/gatehub-webhooks',
315+
isGateHubSignedWebhook,
316+
gateHubController.webhook
317+
)
313318
router.post(
314319
'/gatehub/add-user-to-gateway',
315320
isAuth,

packages/wallet/backend/src/config/env.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const envSchema = z.object({
1515
GATEHUB_ENV: z.enum(['production', 'sandbox']).default('sandbox'),
1616
GATEHUB_ACCESS_KEY: z.string().default('GATEHUB_ACCESS_KEY'),
1717
GATEHUB_SECRET_KEY: z.string().default('GATEHUB_SECRET_KEY'),
18+
GATEHUB_WEBHOOK_SECRET: z.string().default('GATEHUB_WEBHOOK_SECRET'),
1819
GATEHUB_GATEWAY_UUID: z.string().default('GATEHUB_GATEWAY_UUID'),
1920
GATEHUB_VAULT_UUID_EUR: z.string().default('GATEHUB_VAULT_UUID_EUR'),
2021
GATEHUB_VAULT_UUID_USD: z.string().default('GATEHUB_VAULT_UUID_USD'),

packages/wallet/backend/src/gatehub/controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ export class GateHubController implements IGateHubController {
5252
next: NextFunction
5353
) => {
5454
try {
55-
// TODO: implement signature check
5655
if (!req.body.uuid) {
56+
res.status(200).json()
5757
return
5858
}
5959

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { NextFunction, Request, Response } from 'express'
2+
import { Unauthorized } from '@shared/backend'
3+
import { env } from '@/config/env'
4+
import { createHmac } from 'crypto'
5+
6+
const TOLERANCE_MILLISECONDS = 5000
7+
export const isGateHubSignedWebhook = async (
8+
req: Request,
9+
res: Response,
10+
next: NextFunction
11+
): Promise<void> => {
12+
try {
13+
if (!req.body.timestamp) {
14+
// Respond with 200 on initial setup call
15+
res.status(200).json()
16+
return
17+
}
18+
19+
const body = JSON.stringify(req.body)
20+
const key = Buffer.from(env.GATEHUB_WEBHOOK_SECRET, 'hex')
21+
const messageBuffer = Buffer.from(body, 'utf-8')
22+
23+
const requestSignature = createHmac('sha256', key)
24+
.update(messageBuffer)
25+
.digest('hex')
26+
27+
const timeDiffMilliseconds =
28+
new Date().getTime() - parseFloat(req.body.timestamp)
29+
30+
// Check the difference of the received and current timestamp based on your tolerance
31+
const timestampIsValid = timeDiffMilliseconds < TOLERANCE_MILLISECONDS
32+
const signatureIsValid =
33+
requestSignature === req.headers['x-gh-webhook-signature']
34+
35+
const isSignatureValid = timestampIsValid && signatureIsValid
36+
37+
if (!isSignatureValid) {
38+
throw new Unauthorized('Invalid signature header')
39+
}
40+
} catch (e) {
41+
next(e)
42+
}
43+
44+
next()
45+
}

0 commit comments

Comments
 (0)