PHPackages                             jeffersongoncalves/laravel-webhook-signatures - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Security](/categories/security)
4. /
5. jeffersongoncalves/laravel-webhook-signatures

ActiveLibrary[Security](/categories/security)

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.

1.1.0(1mo ago)137↓50%MITPHPPHP ^8.2|^8.3|^8.4CI passing

Since Jun 23Pushed 1mo ago1 watchersCompare

[ Source](https://github.com/jeffersongoncalves/laravel-webhook-signatures)[ Packagist](https://packagist.org/packages/jeffersongoncalves/laravel-webhook-signatures)[ Docs](https://github.com/jeffersongoncalves/laravel-webhook-signatures)[ RSS](/packages/jeffersongoncalves-laravel-webhook-signatures/feed)WikiDiscussions main Synced 2w ago

READMEChangelog (2)Dependencies (9)Versions (3)Used By (0)

[![Laravel Webhook Signatures](https://raw.githubusercontent.com/jeffersongoncalves/laravel-webhook-signatures/main/art/jeffersongoncalves-laravel-webhook-signatures.png)](https://raw.githubusercontent.com/jeffersongoncalves/laravel-webhook-signatures/main/art/jeffersongoncalves-laravel-webhook-signatures.png)

Laravel Webhook Signatures
==========================

[](#laravel-webhook-signatures)

[![Latest Version on Packagist](https://camo.githubusercontent.com/35fc52177e88db51221a53c9d0dc012ee6c7d81444ba7ec22b962c3ccc8749c2/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6a6566666572736f6e676f6e63616c7665732f6c61726176656c2d776562686f6f6b2d7369676e6174757265732e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/jeffersongoncalves/laravel-webhook-signatures)[![GitHub Tests Action Status](https://camo.githubusercontent.com/e0e912e2da19a76facd71b94c117261bcffcf1b317585bda3e696aadd7ca5791/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6a6566666572736f6e676f6e63616c7665732f6c61726176656c2d776562686f6f6b2d7369676e6174757265732f72756e2d74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/jeffersongoncalves/laravel-webhook-signatures/actions?query=workflow%3Arun-tests+branch%3Amain)[![GitHub Code Style Action Status](https://camo.githubusercontent.com/b7d417b18044104c3cc64b2c3b4629d327c365fe6cd3a3a1a0da84154ccec9e9/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6a6566666572736f6e676f6e63616c7665732f6c61726176656c2d776562686f6f6b2d7369676e6174757265732f6669782d7068702d636f64652d7374796c652d6973737565732e796d6c3f6272616e63683d6d61696e266c6162656c3d636f64652532307374796c65267374796c653d666c61742d737175617265)](https://github.com/jeffersongoncalves/laravel-webhook-signatures/actions?query=workflow%3A%22Fix+PHP+code+style+issues%22+branch%3Amain)[![Total Downloads](https://camo.githubusercontent.com/44d9e67d2cc49824b7abf5b18419bdfda171b04044a4bd2fa120fea173cc7f93/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6a6566666572736f6e676f6e63616c7665732f6c61726176656c2d776562686f6f6b2d7369676e6174757265732e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/jeffersongoncalves/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
-------------------

[](#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) or `openssl_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
-------------

[](#compatibility)

ItemSupported versionsPHP8.2, 8.3, 8.4Laravel11.x, 12.x, 13.xOrchestra Testbench9.x, 10.x, 11.xRequired extension`ext-openssl`Installation
------------

[](#installation)

```
composer require jeffersongoncalves/laravel-webhook-signatures
```

Publish the configuration file (optional):

```
php artisan vendor:publish --tag="webhook-signatures-config"
```

Configuration
-------------

[](#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)
```

ProviderSchemeSecret meaning`mailgun`HMAC-SHA256 over `timestamp + token`webhook signing key`sendgrid`ECDSA (P-256/SHA-256) over `timestamp + body`, Twilio headersECDSA 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-*` headersSvix secret (with or without `whsec_` prefix)`sns`X.509 certificate + `openssl_verify` over canonical stringexpected TopicArn (message pinned to the topic)`github`HMAC-SHA256 over raw body, `X-Hub-Signature-256` header (`sha256=`); legacy `X-Hub-Signature` (sha1) fallbackGitHub webhook secretThe timestamp tolerance (in seconds) is configurable:

```
// config/webhook-signatures.php
'tolerance' => [
    'default' => 300,   // Mailgun, Resend, SendGrid
    'sns'     => 3600,  // SNS may redeliver messages later
],
```

Usage
-----

[](#usage)

### 1. Middleware (recommended)

[](#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

[](#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

[](#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

[](#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
-----------------------

[](#what-each-verifier-does)

- **Mailgun** — computes `hash_hmac('sha256', timestamp.token, $key)` and compares it with the received signature via `hash_equals`. Accepts the fields at the top level (inbound routes) or nested under `signature` (event webhooks). Rejects timestamps outside the tolerance window.
- **SendGrid** — verifies the ECDSA signature (P-256/SHA-256) over `timestamp + raw body`, reading the `X-Twilio-Email-Event-Webhook-Signature` and `-Timestamp` headers. Normalizes the verification key (PEM or base64 DER) and uses `openssl_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 each `version,signature` pair from the `svix-signature` header. Rejects timestamps outside the tolerance.
- **GitHub** — computes `hash_hmac('sha256', raw body, $secret)` and compares it, via `hash_equals`, against the `X-Hub-Signature-256` header (`sha256=` format). As a fallback it accepts the legacy `X-Hub-Signature` header (`sha1=`), 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 the `SigningCertURL` points to a legitimate AWS host (`sns..amazonaws.com`), reconstructs the canonical string documented by SNS, downloads the X.509 certificate and verifies the signature with `openssl_verify` (SHA1 for `SignatureVersion 1`, SHA256 for `2`). Rejects messages that are too old.

Testing
-------

[](#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)
-----------------------------

[](#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.php`
- `laravel-service-desk` → `src/Http/Middleware/Verify{Mailgun,SendGrid,Postmark,Resend}Signature.php`
- `laravel-mail` → `src/Webhooks/{SendGrid,Ses,...}WebhookHandler::validate()`
- `laravel-satis` → its own GitHub webhook verification

Real problems found in the duplication:

- **Fail-open**: `help-desk` returned `$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 of `hash_equals`, exposing it to timing attacks.
- **No replay protection**: some Mailgun implementations did not validate timestamp recency.

Suggested migration steps (to apply per package):

1. Add `jeffersongoncalves/laravel-webhook-signatures` to the package `composer.json`.
2. Replace the package's own middlewares with the `webhook.signature:{provider}` alias, **or** call `WebhookSignatures::verify(...)` inside the existing handler.
3. Map the current secrets (e.g. `help-desk.email.inbound.mailgun.signing_key`) to `config('webhook-signatures.providers.mailgun.secret')` — or pass the secret explicitly as the third argument of `verify()`, preserving the package config.
4. Remove the duplicated `Verify*Signature.php` files and their redundant tests.
5. Run the consumer package test suite.

License
-------

[](#license)

MIT. See [LICENSE.md](LICENSE.md).

###  Health Score

42

—

FairBetter than 88% of packages

Maintenance90

Actively maintained with recent releases

Popularity11

Limited adoption so far

Community7

Small or concentrated contributor base

Maturity52

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

45d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/411493?v=4)[Jefferson Gonçalves](/maintainers/jeffersongoncalves)[@jeffersongoncalves](https://github.com/jeffersongoncalves)

---

Top Contributors

[![jeffersongoncalves](https://avatars.githubusercontent.com/u/411493?v=4)](https://github.com/jeffersongoncalves "jeffersongoncalves (5 commits)")

---

Tags

laravelphpsecuritywebhooklaravelsignaturesendgridSNSresendwebhookmailgunjeffersongoncalvespostmarklaravel-webhook-signatures

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/jeffersongoncalves-laravel-webhook-signatures/health.svg)

```
[![Health](https://phpackages.com/badges/jeffersongoncalves-laravel-webhook-signatures/health.svg)](https://phpackages.com/packages/jeffersongoncalves-laravel-webhook-signatures)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.4M353](/packages/psalm-plugin-laravel)[laravel/mcp

Rapidly build MCP servers for your Laravel applications.

79227.1M206](/packages/laravel-mcp)[defstudio/telegraph

A laravel facade to interact with Telegram Bots

818336.8k3](/packages/defstudio-telegraph)[harris21/laravel-fuse

Circuit breaker for Laravel queue jobs. Protect your workers from cascading failures.

46273.9k](/packages/harris21-laravel-fuse)[masterix21/laravel-licensing

Laravel licensing package with polymorphic assignment to any model, activation keys, expirations/renewals, and seat control via LicenseUsage. Supports offline verification with public-key–signed tokens, a CLI to generate/rotate/revoke keys, and an extensible architecture via config and contracts.

1614.1k4](/packages/masterix21-laravel-licensing)[simplestats-io/laravel-client

Server-side analytics for Laravel that follows the full funnel from visit to registration to payment, attributed to the channel that drove it. Revenue, MRR, churn and ad-spend profit (ROAS/CAC) per channel. GDPR compliant, ad-blocker proof.

5222.6k](/packages/simplestats-io-laravel-client)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
