You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
- Webhook authentication token type (Simple hashed token vs. JWT token) can now be configured with new env var `WEBHOOK_USE_JWT` and no longer depends on whether you pass the token as query param (not recommended for JWT tokens) or `Authorization: Bearer` header.
8
+
- Webhook JWT token now contains all necessary claims for strict verification: `sub` (Subject), `iss` (Issuer), `iat` (Issued At), `nbf` (Not Before), `exp` (Expiration Time).
9
+
10
+
**Fixed:**
11
+
12
+
- Webhook JWT token is now correctly encoded using base64 decoded secret (`WEBHOOK_SECRET`) as key. Previously, we forgot to decode it, but always recommended (and still do!) to use a base64 encoded secret.
Copy file name to clipboardExpand all lines: README.md
+22-9Lines changed: 22 additions & 9 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -34,7 +34,7 @@ But let me name some features that make it stand out from other solutions:
34
34
- A multi-threaded app that uses [DBUtils PooledDB (pooled_db)](https://github.com/WebwareForPython/DBUtils) for **robust and efficient DB connection handling**.
35
35
- Can be used with any [DB-API 2 (PEP 249)](https://peps.python.org/pep-0249/) conformant database adapter (currently supported: PyMySQL, sqlite3)
36
36
- A super slick minimal codebase with **only a few dependencies** ([PyMySQL](https://pypi.org/project/pymysql/), [DBUtils](https://webwareforpython.github.io/DBUtils/), [python-dotenv](https://pypi.org/project/python-dotenv/), [yoyo-migrations](https://pypi.org/project/yoyo-migrations/)), using Python virtual environment for easy `pip` install. PyMySQL is a pure-Python MySQL client library, so you won't have any trouble on any future major system upgrades.
37
-
-**Supports external API webhooks** with simple token based authentication (passed as query param) or JWT token (passed as `Authorization: Bearer` header). When configured, the webhook is triggered whenever a sender reaches his quota limit for the first time and you can send out notification through your own or any 3rd-party app.
37
+
-**Supports external API webhooks** with two variants of authentication tokens: simple hashed token or JWT token. The authentication token can be configured to be passed either as query param or as `Authorization: Bearer` header. When enabled, the webhook is triggered whenever a sender reaches his quota limit for the first time and you can send out notifications through your own or any 3rd-party app.
38
38
- Provides an Ansible Galaxy role [`onlime.policyd_rate_guard`](https://galaxy.ansible.com/onlime/policyd_rate_guard) for easy installation on a Debian mailserver.
39
39
- A **well maintained** project, as it is in active use at [Onlime GmbH](https://www.onlime.ch/), a Swiss webhoster with a rock-solid mailserver architecture.
40
40
@@ -244,8 +244,11 @@ Optional configuration for external service integration:
244
244
Sentry environment. Suggested values: `dev` or `prod`, but can be any custom string. Defaults to `dev`.
245
245
-`WEBHOOK_ENABLED` (bool)
246
246
Enable external API webhook to be called when sender reached his quota limit (first time he's blocked). Possible values: `True` or `False`. Defaults to `False`.
247
+
-`WEBHOOK_USE_JWT` (bool)
248
+
Use JWT for webhook token authentication, instead of a simple hashed token. This more advanced authentication is recommended only to be used when passing the token as `Authorization: Bearer` header, and not as query param (by using the `{token}` placeholder in your `WEBHOOK_URL`). If not using JWT, the token will be a simple hash from your secret appended to the sender address.
249
+
Possible values: `True` or `False`. Defaults to `False`.
247
250
-`WEBHOOK_URL`
248
-
Webhook API URL of the external service that should be called if `WEBHOOK_ENABLED=True`. It supports the following placeholders, which are both optional: `{sender}`, `{token}`. You may provide a URL in the following form: `https://api.example.com/policyd/{sender}?token={token}` (the token will be a simple hash from your secret appended to the sender address), or if you omit the `{token}` in the URL, a signed JWT token will be passed as `Bearer` token in the `Authorization` header, which will also contain the sender in its payload.
251
+
Webhook API URL of the external service that should be called if `WEBHOOK_ENABLED=True`. It supports any key of the webhook payload (JSON object) as placeholder, usually the following: `{sender}`, `{token}`. All placeholders are optional. You may provide a URL in the following form: `https://api.example.com/policyd/{sender}?token={token}`, or if you omit the `{token}` in the URL, make sure to enable `WEBHOOK_USE_JWT=True`, so a signed JWT token (including the sender as its `sub` claim) will be passed as `Authorization: Bearer` header.
249
252
-`WEBHOOK_SECRET`
250
253
The shared secret to generate the webhook token. Configure this shared secret also on the external API's webhook to verify the token for authentication. Recommended way to generate a secret: `base64.b64encode(secrets.token_bytes(32))`
251
254
@@ -311,9 +314,11 @@ or with PHP (e.g. using `php artisan tinker` in Laravel, or `php -a` interactive
311
314
> base64_encode(Str::random(32))
312
315
```
313
316
317
+
We always expect your secret in `WEBHOOK_SECRET` to be base64 encoded!
318
+
314
319
Depending on your external API, PolicydRateGuard supports two different ways of authentication:
315
320
316
-
**Variant 1) Simple token as query param**
321
+
**Variant 1) Simple token**
317
322
318
323
The authentication token can be passed as a query param to your external API webhook. In this case, you need to use the `{token}` placeholder in your `WEBHOOK_URL`, no matter if you use any other (optional) placeholders like `{sender}` or not. The sender will always be part of the JSON data (payload) passed to your webhook anyway.
if (! hash_equals($token, $request->query('token') ?: $request->bearerToken())) {
358
363
abort(403, 'You are not allowed to access this webhook.');
359
364
}
360
365
return $next($request);
@@ -364,19 +369,27 @@ class AccessApiWebhookPolicyd
364
369
365
370
**Variant 2) JWT token in Authorization header**
366
371
367
-
If your `WEBHOOK_URL` does not contain a `{token}` placeholder, we assume you don't want to pass it as query param, but as JWT token in the `Authorization: Bearer <token>` header instead. PolicydRateGuard will take care of it and generate a valid JWT token, basically like this:
372
+
If you have `WEBHOOK_USE_JWT` enabled in your `.env`, PolicydRateGuard will generate a JWT token instead of the previously mentioned simple hashed token. It's recommended not to put a `{token}` placeholder in your `WEBHOOK_URL`, so that the JWT token will be passed in the `Authorization: Bearer <token>` header.
373
+
374
+
PolicydRateGuard will generate a valid JWT like this:
368
375
369
376
```python
370
377
import jwt
378
+
from base64 import b64decode
371
379
from datetime import datetime, timedelta, timezone
The token is valid for 60s and contains the `sub` (subject, in our case the `sender`) in its payload. The subject in the JWT token is always the same as the `sender` in the JSON data passed via POST request.
392
+
The token is valid for 60s and contains the `sub` ("Subject", in our case the `sender`) in its payload. The subject in the JWT token is always the same as the `sender` in the JSON data passed via POST request.
380
393
381
394
If your external API webhook runs on PHP, we recommend to use the [`lcobucci/jwt`](https://github.com/lcobucci/jwt) library to decode and verify the JWT token. In a Laravel app you can go for a similar implementation as described in Variant 1) and decode the JWT token in your `AccessApiWebhookPolicyd` middleware.
0 commit comments