jeffersongoncalves/laravel-webhook-signatures
Composer 安装命令:
composer require jeffersongoncalves/laravel-webhook-signatures
包简介
Verificação centralizada e fail-closed de assinaturas de webhooks de provedores de email/serviços (Mailgun, SendGrid, Postmark, Resend e AWS SNS/SES) para aplicações Laravel.
关键字:
README 文档
README
Laravel Webhook Signatures
Centralized, fail-closed webhook signature verification for the major email and service providers (Mailgun, SendGrid, Postmark, Resend/Svix, AWS SNS/SES and GitHub) in Laravel applications.
This package was born out of the need to eliminate the duplicated and buggy signature-verification logic scattered across several packages (laravel-help-desk, laravel-service-desk, laravel-mail, laravel-satis). Instead of every package re-implementing — and getting wrong — the same verification, they all depend on a single, audited and tested source of truth.
Security principles
Every verifier follows the fail-closed principle:
- Missing or empty secret → verification fails (returns
false). There is never a fail-open path. - Secret material is always compared with
hash_equals(HMAC/basic-auth) oropenssl_verify(ECDSA/RSA) — constant time, no timing leaks. - Timestamp validation (replay protection) wherever the provider exposes a signed timestamp.
- Any missing or malformed header, field or certificate results in rejection.
Compatibility
| Item | Supported versions |
|---|---|
| PHP | 8.2, 8.3, 8.4 |
| Laravel | 11.x, 12.x, 13.x |
| Orchestra Testbench | 9.x, 10.x, 11.x |
| Required extension | ext-openssl |
Installation
composer require jeffersongoncalves/laravel-webhook-signatures
Publish the configuration file (optional):
php artisan vendor:publish --tag="webhook-signatures-config"
Configuration
The meaning of the "secret" varies per provider. Define the values via .env:
WEBHOOK_MAILGUN_SIGNING_KEY=... # Mailgun signing key WEBHOOK_SENDGRID_VERIFICATION_KEY=... # SendGrid ECDSA verification key WEBHOOK_POSTMARK_BASIC_AUTH=user:password # Postmark Basic Auth credentials WEBHOOK_RESEND_SECRET=whsec_... # Resend Svix secret WEBHOOK_SNS_TOPIC_ARN=arn:aws:sns:... # expected TopicArn (SES via SNS) GITHUB_WEBHOOK_SECRET=... # GitHub webhook secret (HMAC-SHA256)
| Provider | Scheme | Secret meaning |
|---|---|---|
mailgun |
HMAC-SHA256 over timestamp + token |
webhook signing key |
sendgrid |
ECDSA (P-256/SHA-256) over timestamp + body, Twilio headers |
ECDSA verification key (PEM or base64 DER) |
postmark |
Basic Auth (hash_equals) |
credentials in user:password format |
resend |
HMAC-SHA256 base64 over id.timestamp.payload, svix-* headers |
Svix secret (with or without whsec_ prefix) |
sns |
X.509 certificate + openssl_verify over canonical string |
expected TopicArn (message pinned to the topic) |
github |
HMAC-SHA256 over raw body, X-Hub-Signature-256 header (sha256=<hex>); legacy X-Hub-Signature (sha1) fallback |
GitHub webhook secret |
The timestamp tolerance (in seconds) is configurable:
// config/webhook-signatures.php 'tolerance' => [ 'default' => 300, // Mailgun, Resend, SendGrid 'sns' => 3600, // SNS may redeliver messages later ],
Usage
1. Middleware (recommended)
The package registers the webhook.signature middleware alias, parameterized by provider. It aborts with 403 when the signature cannot be verified:
use Illuminate\Support\Facades\Route; Route::post('/webhooks/mailgun', InboundController::class) ->middleware('webhook.signature:mailgun'); Route::post('/webhooks/resend', ResendController::class) ->middleware('webhook.signature:resend');
The secret is read automatically from config('webhook-signatures.providers.{provider}.secret').
2. Direct usage via Facade
use JeffersonGoncalves\WebhookSignatures\Facades\WebhookSignatures; public function handle(Request $request) { if (! WebhookSignatures::verify('sendgrid', $request)) { abort(403); } // ... process the event }
You can also pass the secret explicitly (bypassing the config):
WebhookSignatures::verify('mailgun', $request, $myKey);
3. Using a standalone verifier
Each verifier implements the SignatureVerifier interface:
use JeffersonGoncalves\WebhookSignatures\Verifiers\ResendSignatureVerifier; $verifier = new ResendSignatureVerifier(tolerance: 300); $valid = $verifier->verify($request, $secret); // bool
4. Registering a custom verifier
use JeffersonGoncalves\WebhookSignatures\Facades\WebhookSignatures; WebhookSignatures::extend('my-provider', MyVerifier::class);
MyVerifier must implement JeffersonGoncalves\WebhookSignatures\Contracts\SignatureVerifier and accept int $tolerance in the constructor.
What each verifier does
- Mailgun — computes
hash_hmac('sha256', timestamp.token, $key)and compares it with the received signature viahash_equals. Accepts the fields at the top level (inbound routes) or nested undersignature(event webhooks). Rejects timestamps outside the tolerance window. - SendGrid — verifies the ECDSA signature (P-256/SHA-256) over
timestamp + raw body, reading theX-Twilio-Email-Event-Webhook-Signatureand-Timestampheaders. Normalizes the verification key (PEM or base64 DER) and usesopenssl_verify. - Postmark — Postmark does not sign the payload; authentication is via Basic Auth. Compares user and password in constant time (
hash_equals). - Resend (Svix) — reconstructs
id.timestamp.payload, computes HMAC-SHA256 with the decoded key (whsec_prefix stripped), base64-encodes it and compares against eachversion,signaturepair from thesvix-signatureheader. Rejects timestamps outside the tolerance. - GitHub — computes
hash_hmac('sha256', raw body, $secret)and compares it, viahash_equals, against theX-Hub-Signature-256header (sha256=<hex>format). As a fallback it accepts the legacyX-Hub-Signatureheader (sha1=<hex>), but always prioritizes SHA-256. A missing or malformed header results in rejection. - AWS SNS/SES — pins the message to the expected
TopicArn, validates that theSigningCertURLpoints to a legitimate AWS host (sns.<region>.amazonaws.com), reconstructs the canonical string documented by SNS, downloads the X.509 certificate and verifies the signature withopenssl_verify(SHA1 forSignatureVersion 1, SHA256 for2). Rejects messages that are too old.
Testing
composer test # Pest composer analyse # PHPStan (level 5, Larastan) composer format # Laravel Pint
Each verifier has tests covering: valid signature accepted, invalid signature rejected, request without credentials rejected and (where applicable) old timestamp rejected. All cryptographic keys and fixtures are generated inside the tests.
Migration (consumer packages)
This package consolidates signature verification that used to be duplicated (and divergent) across:
laravel-help-desk→src/Http/Middleware/Verify{Mailgun,SendGrid,Postmark,Resend}Signature.phplaravel-service-desk→src/Http/Middleware/Verify{Mailgun,SendGrid,Postmark,Resend}Signature.phplaravel-mail→src/Webhooks/{SendGrid,Ses,...}WebhookHandler::validate()laravel-satis→ its own GitHub webhook verification
Real problems found in the duplication:
- Fail-open:
help-deskreturned$next($request)when the key was not configured — i.e. it accepted any request. Here the behavior is always fail-closed. - Non-constant comparison:
service-desk(SendGrid/Postmark) used!==instead ofhash_equals, exposing it to timing attacks. - No replay protection: some Mailgun implementations did not validate timestamp recency.
Suggested migration steps (to apply per package):
- Add
jeffersongoncalves/laravel-webhook-signaturesto the packagecomposer.json. - Replace the package's own middlewares with the
webhook.signature:{provider}alias, or callWebhookSignatures::verify(...)inside the existing handler. - Map the current secrets (e.g.
help-desk.email.inbound.mailgun.signing_key) toconfig('webhook-signatures.providers.mailgun.secret')— or pass the secret explicitly as the third argument ofverify(), preserving the package config. - Remove the duplicated
Verify*Signature.phpfiles and their redundant tests. - Run the consumer package test suite.
License
MIT. See LICENSE.md.
统计信息
- 总下载量: 0
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 4
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-06-23
