Skip to content

Commit 963081a

Browse files
authored
Merge pull request #10 from topcoder-platform/feat/github-webhook
Switch to auth headers
2 parents 52160a0 + edd0a39 commit 963081a

File tree

6 files changed

+88
-167
lines changed

6 files changed

+88
-167
lines changed

.env.sample

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=${P
33

44

55
# Gitea Webhook Configuration
6-
GITEA_WEBHOOK_SECRET="your_webhook_secret_here"
6+
GITEA_WEBHOOK_AUTH="your_webhook_secret_here"
77

88
# Kafka Configuration
99
KAFKA_BROKERS=localhost:9092

docs/GITEA_WEBHOOK_SETUP.md

Lines changed: 12 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Overview
44

5-
The Topcoder Review API includes a secure Gitea webhook integration that receives webhook events from Gitea repositories, validates them using HMAC-SHA256 signature verification, and stores them in the database for audit and future processing.
5+
The Topcoder Review API includes a secure Gitea webhook integration that receives webhook events from Gitea repositories, validates them using Authorization header validation, and stores them in the database for audit and future processing.
66

77
## Table of Contents
88

@@ -21,7 +21,7 @@ The Topcoder Review API includes a secure Gitea webhook integration that receive
2121

2222
For immediate setup, follow these steps:
2323

24-
1. Generate a secure webhook secret
24+
1. Generate a secure webhook auth secret
2525
2. Configure environment variables
2626
3. Set up Gitea webhook in repository settings
2727
4. Test with a sample event
@@ -34,7 +34,7 @@ Add the following environment variable to your application configuration:
3434

3535
```bash
3636
# .env file
37-
GITEA_WEBHOOK_SECRET=your_generated_secret_here
37+
GITEA_WEBHOOK_AUTH=your_generated_secret_here
3838
```
3939

4040
### Generate Webhook Secret
@@ -51,7 +51,7 @@ openssl rand -hex 32
5151
a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456
5252
```
5353

54-
⚠️ **Important:** Store this secret securely and use the same value in both your application environment and Gitea webhook configuration.
54+
⚠️ **Important:** Store this auth secret securely and use the same value in both your application environment and Gitea webhook configuration.
5555

5656
### Database Setup
5757

@@ -92,10 +92,10 @@ Note: The `/v6/review` prefix is only added in production when `NODE_ENV=product
9292

9393
- Select `application/json`
9494

95-
#### Secret
95+
#### Authorization
9696

97-
- Enter the webhook secret you generated earlier
98-
- This must exactly match your `GITEA_WEBHOOK_SECRET` environment variable
97+
- Enter the webhook auth secret you generated earlier
98+
- This must exactly match your `GITEA_WEBHOOK_AUTH` environment variable
9999

100100
#### SSL Verification
101101

@@ -192,33 +192,7 @@ git push origin main
192192
3. Push branch: `git push origin test-webhook`
193193
4. Open pull request on Gitea
194194

195-
### Testing with curl
196-
197-
You can test the webhook endpoint directly using curl with proper signature generation:
198-
199-
```bash
200-
#!/bin/bash
201-
202-
# Configuration
203-
WEBHOOK_URL="http://localhost:3000/webhooks/gitea" # Adjust for your environment
204-
WEBHOOK_SECRET="your_webhook_secret_here"
205-
PAYLOAD='{"test": "data", "repository": {"name": "test-repo"}}'
206-
DELIVERY_ID="test-delivery-$(date +%s)"
207-
EVENT_TYPE="push"
208-
209-
# Generate signature
210-
SIGNATURE="sha256=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | sed 's/^.* //')"
211-
212-
# Send test webhook
213-
curl -X POST "$WEBHOOK_URL" \
214-
-H "Content-Type: application/json" \
215-
-H "X-Gitea-Event: $EVENT_TYPE" \
216-
-H "X-Gitea-Delivery: $DELIVERY_ID" \
217-
-H "X-Hub-Signature-256: $SIGNATURE" \
218-
-d "$PAYLOAD"
219-
```
220-
221-
## API Endpoint Reference
195+
### API Endpoint Reference
222196

223197
### Webhook Endpoint
224198

@@ -229,7 +203,7 @@ curl -X POST "$WEBHOOK_URL" \
229203
- `Content-Type: application/json`
230204
- `X-Gitea-Event: {event_type}` - Gitea event type (push, pull_request, etc.)
231205
- `X-Gitea-Delivery: {delivery_id}` - Unique delivery identifier from Gitea
232-
- `X-Hub-Signature-256: sha256={signature}` - HMAC-SHA256 signature for verification
206+
- `Authorization: Bearer {GITEA_WEBHOOK_AUTH}` - Token used to verify authorization
233207

234208
**Request Body:**
235209

@@ -323,10 +297,8 @@ WHERE "eventId" = 'your-delivery-id';
323297

324298
The webhook implementation uses Gitea's recommended security practices:
325299

326-
1. **HMAC-SHA256 Signature:** All incoming webhooks are verified using HMAC-SHA256
327-
2. **Timing-Safe Comparison:** Uses `crypto.timingSafeEqual()` to prevent timing attacks
328-
3. **Secret Protection:** Webhook secrets are stored as environment variables
329-
4. **Header Validation:** Validates all required Gitea headers
300+
1. **Secret Protection:** Webhook auth secrets are stored as environment variables
301+
2. **Header Validation:** Validates all required Gitea headers
330302

331303
### Best Practices
332304

@@ -338,22 +310,7 @@ The webhook implementation uses Gitea's recommended security practices:
338310

339311
### Environment Security
340312

341-
- Store `GITEA_WEBHOOK_SECRET` securely using your deployment platform's secret management
313+
- Store `GITEA_WEBHOOK_AUTH` securely using your deployment platform's secret management
342314
- Never commit secrets to version control
343315
- Use different secrets for different environments
344316
- Implement proper secret rotation procedures
345-
346-
### Log Analysis
347-
348-
Key log messages to monitor:
349-
350-
```
351-
# Successful webhook processing
352-
[WebhookController] Successfully processed Gitea webhook
353-
354-
# Signature validation failures
355-
[GiteaSignatureGuard] Invalid webhook signature for delivery
356-
357-
# Configuration errors
358-
[GiteaSignatureGuard] Gitea_WEBHOOK_SECRET environment variable is not configured
359-
```

src/api/api.module.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { ReviewHistoryController } from './review-history/reviewHistory.controll
1515
import { ChallengeApiService } from 'src/shared/modules/global/challenge.service';
1616
import { WebhookController } from './webhook/webhook.controller';
1717
import { WebhookService } from './webhook/webhook.service';
18-
import { GiteaSignatureGuard } from '../shared/guards/gitea-signature.guard';
18+
import { GiteaWebhookAuthGuard } from '../shared/guards/gitea-webhook-auth.guard';
1919

2020
@Module({
2121
imports: [HttpModule, GlobalProvidersModule],
@@ -36,7 +36,7 @@ import { GiteaSignatureGuard } from '../shared/guards/gitea-signature.guard';
3636
ReviewApplicationService,
3737
ChallengeApiService,
3838
WebhookService,
39-
GiteaSignatureGuard,
39+
GiteaWebhookAuthGuard,
4040
],
4141
})
4242
export class ApiModule {}

src/api/webhook/webhook.controller.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
WebhookEventDto,
1414
WebhookResponseDto,
1515
} from '../../dto/webhook-event.dto';
16-
import { GiteaSignatureGuard } from '../../shared/guards/gitea-signature.guard';
16+
import { GiteaWebhookAuthGuard } from '../../shared/guards/gitea-webhook-auth.guard';
1717
import { LoggerService } from '../../shared/modules/global/logger.service';
1818

1919
@ApiTags('Webhooks')
@@ -25,7 +25,7 @@ export class WebhookController {
2525

2626
@Post('gitea')
2727
@HttpCode(HttpStatus.OK)
28-
@UseGuards(GiteaSignatureGuard)
28+
@UseGuards(GiteaWebhookAuthGuard)
2929
@ApiOperation({
3030
summary: 'Gitea Webhook Endpoint',
3131
description:
@@ -42,8 +42,8 @@ export class WebhookController {
4242
required: true,
4343
})
4444
@ApiHeader({
45-
name: 'X-Hub-Signature-256',
46-
description: 'HMAC-SHA256 signature for request verification',
45+
name: 'authorization',
46+
description: 'Authorization header for Gitea webhook',
4747
required: true,
4848
})
4949
@ApiResponse({

src/shared/guards/gitea-signature.guard.ts

Lines changed: 0 additions & 105 deletions
This file was deleted.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import {
2+
Injectable,
3+
CanActivate,
4+
ExecutionContext,
5+
ForbiddenException,
6+
BadRequestException,
7+
InternalServerErrorException,
8+
} from '@nestjs/common';
9+
import { Request } from 'express';
10+
import { LoggerService } from '../modules/global/logger.service';
11+
12+
@Injectable()
13+
export class GiteaWebhookAuthGuard implements CanActivate {
14+
private readonly logger = LoggerService.forRoot('GiteaWebhookAuthGuard');
15+
16+
canActivate(context: ExecutionContext): boolean {
17+
const request = context.switchToHttp().getRequest<Request>();
18+
const delivery = request.headers['x-gitea-delivery'] as string;
19+
const event = request.headers['x-gitea-event'] as string;
20+
const authHeader = request.headers['authorization'] as string;
21+
22+
// Check if GITEA_WEBHOOK_AUTH is configured
23+
const auth = process.env.GITEA_WEBHOOK_AUTH;
24+
if (!auth) {
25+
this.logger.error(
26+
'GITEA_WEBHOOK_AUTH environment variable is not configured',
27+
);
28+
throw new InternalServerErrorException('Webhook auth not configured');
29+
}
30+
31+
if (!delivery) {
32+
this.logger.error('Missing X-Gitea-Delivery header');
33+
throw new BadRequestException('Missing delivery header');
34+
}
35+
36+
if (!event) {
37+
this.logger.error('Missing X-Gitea-Event header');
38+
throw new BadRequestException('Missing event header');
39+
}
40+
41+
try {
42+
// Validate the authorization header
43+
if (!authHeader) {
44+
this.logger.error('Missing Authorization header');
45+
throw new BadRequestException('Missing authorization header');
46+
}
47+
48+
if (authHeader !== `Bearer ${auth}`) {
49+
this.logger.error('Invalid authorization header');
50+
throw new ForbiddenException('Invalid authorization');
51+
}
52+
53+
this.logger.log(
54+
`Valid webhook authorization verified for delivery ${delivery}, event ${event}`,
55+
);
56+
return true;
57+
} catch (error) {
58+
if (
59+
error instanceof ForbiddenException ||
60+
error instanceof BadRequestException
61+
) {
62+
throw error;
63+
}
64+
65+
this.logger.error(`Error validating webhook signature: ${error.message}`);
66+
throw new InternalServerErrorException('Signature validation failed');
67+
}
68+
}
69+
}

0 commit comments

Comments
 (0)