From edd0a39fcae49ab034fce1d72b7970a3b23cec29 Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Mon, 4 Aug 2025 11:17:06 +0300 Subject: [PATCH] Switch to auth headers --- .env.sample | 2 +- docs/GITEA_WEBHOOK_SETUP.md | 67 ++--------- src/api/api.module.ts | 4 +- src/api/webhook/webhook.controller.ts | 8 +- src/shared/guards/gitea-signature.guard.ts | 105 ------------------ src/shared/guards/gitea-webhook-auth.guard.ts | 69 ++++++++++++ 6 files changed, 88 insertions(+), 167 deletions(-) delete mode 100644 src/shared/guards/gitea-signature.guard.ts create mode 100644 src/shared/guards/gitea-webhook-auth.guard.ts diff --git a/.env.sample b/.env.sample index c63ec5b..e2acf9e 100644 --- a/.env.sample +++ b/.env.sample @@ -3,7 +3,7 @@ DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=${P # Gitea Webhook Configuration -GITEA_WEBHOOK_SECRET="your_webhook_secret_here" +GITEA_WEBHOOK_AUTH="your_webhook_secret_here" # Kafka Configuration KAFKA_BROKERS=localhost:9092 diff --git a/docs/GITEA_WEBHOOK_SETUP.md b/docs/GITEA_WEBHOOK_SETUP.md index 551e47b..a68d2d0 100644 --- a/docs/GITEA_WEBHOOK_SETUP.md +++ b/docs/GITEA_WEBHOOK_SETUP.md @@ -2,7 +2,7 @@ ## Overview -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. +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. ## Table of Contents @@ -21,7 +21,7 @@ The Topcoder Review API includes a secure Gitea webhook integration that receive For immediate setup, follow these steps: -1. Generate a secure webhook secret +1. Generate a secure webhook auth secret 2. Configure environment variables 3. Set up Gitea webhook in repository settings 4. Test with a sample event @@ -34,7 +34,7 @@ Add the following environment variable to your application configuration: ```bash # .env file -GITEA_WEBHOOK_SECRET=your_generated_secret_here +GITEA_WEBHOOK_AUTH=your_generated_secret_here ``` ### Generate Webhook Secret @@ -51,7 +51,7 @@ openssl rand -hex 32 a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456 ``` -⚠️ **Important:** Store this secret securely and use the same value in both your application environment and Gitea webhook configuration. +⚠️ **Important:** Store this auth secret securely and use the same value in both your application environment and Gitea webhook configuration. ### Database Setup @@ -92,10 +92,10 @@ Note: The `/v6/review` prefix is only added in production when `NODE_ENV=product - Select `application/json` -#### Secret +#### Authorization -- Enter the webhook secret you generated earlier -- This must exactly match your `GITEA_WEBHOOK_SECRET` environment variable +- Enter the webhook auth secret you generated earlier +- This must exactly match your `GITEA_WEBHOOK_AUTH` environment variable #### SSL Verification @@ -192,33 +192,7 @@ git push origin main 3. Push branch: `git push origin test-webhook` 4. Open pull request on Gitea -### Testing with curl - -You can test the webhook endpoint directly using curl with proper signature generation: - -```bash -#!/bin/bash - -# Configuration -WEBHOOK_URL="http://localhost:3000/webhooks/gitea" # Adjust for your environment -WEBHOOK_SECRET="your_webhook_secret_here" -PAYLOAD='{"test": "data", "repository": {"name": "test-repo"}}' -DELIVERY_ID="test-delivery-$(date +%s)" -EVENT_TYPE="push" - -# Generate signature -SIGNATURE="sha256=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | sed 's/^.* //')" - -# Send test webhook -curl -X POST "$WEBHOOK_URL" \ - -H "Content-Type: application/json" \ - -H "X-Gitea-Event: $EVENT_TYPE" \ - -H "X-Gitea-Delivery: $DELIVERY_ID" \ - -H "X-Hub-Signature-256: $SIGNATURE" \ - -d "$PAYLOAD" -``` - -## API Endpoint Reference +### API Endpoint Reference ### Webhook Endpoint @@ -229,7 +203,7 @@ curl -X POST "$WEBHOOK_URL" \ - `Content-Type: application/json` - `X-Gitea-Event: {event_type}` - Gitea event type (push, pull_request, etc.) - `X-Gitea-Delivery: {delivery_id}` - Unique delivery identifier from Gitea -- `X-Hub-Signature-256: sha256={signature}` - HMAC-SHA256 signature for verification +- `Authorization: Bearer {GITEA_WEBHOOK_AUTH}` - Token used to verify authorization **Request Body:** @@ -323,10 +297,8 @@ WHERE "eventId" = 'your-delivery-id'; The webhook implementation uses Gitea's recommended security practices: -1. **HMAC-SHA256 Signature:** All incoming webhooks are verified using HMAC-SHA256 -2. **Timing-Safe Comparison:** Uses `crypto.timingSafeEqual()` to prevent timing attacks -3. **Secret Protection:** Webhook secrets are stored as environment variables -4. **Header Validation:** Validates all required Gitea headers +1. **Secret Protection:** Webhook auth secrets are stored as environment variables +2. **Header Validation:** Validates all required Gitea headers ### Best Practices @@ -338,22 +310,7 @@ The webhook implementation uses Gitea's recommended security practices: ### Environment Security -- Store `GITEA_WEBHOOK_SECRET` securely using your deployment platform's secret management +- Store `GITEA_WEBHOOK_AUTH` securely using your deployment platform's secret management - Never commit secrets to version control - Use different secrets for different environments - Implement proper secret rotation procedures - -### Log Analysis - -Key log messages to monitor: - -``` -# Successful webhook processing -[WebhookController] Successfully processed Gitea webhook - -# Signature validation failures -[GiteaSignatureGuard] Invalid webhook signature for delivery - -# Configuration errors -[GiteaSignatureGuard] Gitea_WEBHOOK_SECRET environment variable is not configured -``` diff --git a/src/api/api.module.ts b/src/api/api.module.ts index e75677c..af307c6 100644 --- a/src/api/api.module.ts +++ b/src/api/api.module.ts @@ -15,7 +15,7 @@ import { ReviewHistoryController } from './review-history/reviewHistory.controll import { ChallengeApiService } from 'src/shared/modules/global/challenge.service'; import { WebhookController } from './webhook/webhook.controller'; import { WebhookService } from './webhook/webhook.service'; -import { GiteaSignatureGuard } from '../shared/guards/gitea-signature.guard'; +import { GiteaWebhookAuthGuard } from '../shared/guards/gitea-webhook-auth.guard'; @Module({ imports: [HttpModule, GlobalProvidersModule], @@ -36,7 +36,7 @@ import { GiteaSignatureGuard } from '../shared/guards/gitea-signature.guard'; ReviewApplicationService, ChallengeApiService, WebhookService, - GiteaSignatureGuard, + GiteaWebhookAuthGuard, ], }) export class ApiModule {} diff --git a/src/api/webhook/webhook.controller.ts b/src/api/webhook/webhook.controller.ts index 0d86561..9bf7f5c 100644 --- a/src/api/webhook/webhook.controller.ts +++ b/src/api/webhook/webhook.controller.ts @@ -13,7 +13,7 @@ import { WebhookEventDto, WebhookResponseDto, } from '../../dto/webhook-event.dto'; -import { GiteaSignatureGuard } from '../../shared/guards/gitea-signature.guard'; +import { GiteaWebhookAuthGuard } from '../../shared/guards/gitea-webhook-auth.guard'; import { LoggerService } from '../../shared/modules/global/logger.service'; @ApiTags('Webhooks') @@ -25,7 +25,7 @@ export class WebhookController { @Post('gitea') @HttpCode(HttpStatus.OK) - @UseGuards(GiteaSignatureGuard) + @UseGuards(GiteaWebhookAuthGuard) @ApiOperation({ summary: 'Gitea Webhook Endpoint', description: @@ -42,8 +42,8 @@ export class WebhookController { required: true, }) @ApiHeader({ - name: 'X-Hub-Signature-256', - description: 'HMAC-SHA256 signature for request verification', + name: 'authorization', + description: 'Authorization header for Gitea webhook', required: true, }) @ApiResponse({ diff --git a/src/shared/guards/gitea-signature.guard.ts b/src/shared/guards/gitea-signature.guard.ts deleted file mode 100644 index 5535489..0000000 --- a/src/shared/guards/gitea-signature.guard.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { - Injectable, - CanActivate, - ExecutionContext, - ForbiddenException, - BadRequestException, - InternalServerErrorException, -} from '@nestjs/common'; -import { Request } from 'express'; -import * as crypto from 'crypto'; -import { LoggerService } from '../modules/global/logger.service'; - -@Injectable() -export class GiteaSignatureGuard implements CanActivate { - private readonly logger = LoggerService.forRoot('GiteaSignatureGuard'); - - canActivate(context: ExecutionContext): boolean { - const request = context.switchToHttp().getRequest(); - const signature = request.headers['x-hub-signature-256'] as string; - const delivery = request.headers['x-gitea-delivery'] as string; - const event = request.headers['x-gitea-event'] as string; - - // Check if GITEA_WEBHOOK_SECRET is configured - const secret = process.env.GITEA_WEBHOOK_SECRET; - if (!secret) { - this.logger.error( - 'GITEA_WEBHOOK_SECRET environment variable is not configured', - ); - throw new InternalServerErrorException('Webhook secret not configured'); - } - - // Validate required headers - if (!signature) { - this.logger.error('Missing X-Hub-Signature-256 header'); - throw new BadRequestException('Missing signature header'); - } - - if (!delivery) { - this.logger.error('Missing X-Gitea-Delivery header'); - throw new BadRequestException('Missing delivery header'); - } - - if (!event) { - this.logger.error('Missing X-Gitea-Event header'); - throw new BadRequestException('Missing event header'); - } - - // Validate signature format - if (!signature.startsWith('sha256=')) { - this.logger.error('Invalid signature format'); - throw new BadRequestException('Invalid signature format'); - } - - try { - // Get the raw body for signature verification - const payload = request.body; - let bodyString: string; - - if (typeof payload === 'string') { - bodyString = payload; - } else if (Buffer.isBuffer(payload)) { - bodyString = payload.toString('utf8'); - } else { - bodyString = JSON.stringify(payload); - } - - // Compute HMAC-SHA256 signature - const computedSignature = crypto - .createHmac('sha256', secret) - .update(bodyString, 'utf8') - .digest('hex'); - - const expectedSignature = `sha256=${computedSignature}`; - - // Extract the signature hash from the header - const providedSignature = signature; - - // Perform timing-safe comparison to prevent timing attacks - const isValid = crypto.timingSafeEqual( - Buffer.from(expectedSignature, 'utf8'), - Buffer.from(providedSignature, 'utf8'), - ); - - if (!isValid) { - this.logger.error(`Invalid webhook signature for delivery ${delivery}`); - throw new ForbiddenException('Invalid signature'); - } - - this.logger.log( - `Valid webhook signature verified for delivery ${delivery}, event ${event}`, - ); - return true; - } catch (error) { - if ( - error instanceof ForbiddenException || - error instanceof BadRequestException - ) { - throw error; - } - - this.logger.error(`Error validating webhook signature: ${error.message}`); - throw new InternalServerErrorException('Signature validation failed'); - } - } -} diff --git a/src/shared/guards/gitea-webhook-auth.guard.ts b/src/shared/guards/gitea-webhook-auth.guard.ts new file mode 100644 index 0000000..1506cfa --- /dev/null +++ b/src/shared/guards/gitea-webhook-auth.guard.ts @@ -0,0 +1,69 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + ForbiddenException, + BadRequestException, + InternalServerErrorException, +} from '@nestjs/common'; +import { Request } from 'express'; +import { LoggerService } from '../modules/global/logger.service'; + +@Injectable() +export class GiteaWebhookAuthGuard implements CanActivate { + private readonly logger = LoggerService.forRoot('GiteaWebhookAuthGuard'); + + canActivate(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest(); + const delivery = request.headers['x-gitea-delivery'] as string; + const event = request.headers['x-gitea-event'] as string; + const authHeader = request.headers['authorization'] as string; + + // Check if GITEA_WEBHOOK_AUTH is configured + const auth = process.env.GITEA_WEBHOOK_AUTH; + if (!auth) { + this.logger.error( + 'GITEA_WEBHOOK_AUTH environment variable is not configured', + ); + throw new InternalServerErrorException('Webhook auth not configured'); + } + + if (!delivery) { + this.logger.error('Missing X-Gitea-Delivery header'); + throw new BadRequestException('Missing delivery header'); + } + + if (!event) { + this.logger.error('Missing X-Gitea-Event header'); + throw new BadRequestException('Missing event header'); + } + + try { + // Validate the authorization header + if (!authHeader) { + this.logger.error('Missing Authorization header'); + throw new BadRequestException('Missing authorization header'); + } + + if (authHeader !== `Bearer ${auth}`) { + this.logger.error('Invalid authorization header'); + throw new ForbiddenException('Invalid authorization'); + } + + this.logger.log( + `Valid webhook authorization verified for delivery ${delivery}, event ${event}`, + ); + return true; + } catch (error) { + if ( + error instanceof ForbiddenException || + error instanceof BadRequestException + ) { + throw error; + } + + this.logger.error(`Error validating webhook signature: ${error.message}`); + throw new InternalServerErrorException('Signature validation failed'); + } + } +}