Skip to content

Commit dc4dff4

Browse files
authored
Merge branch 'main' into copilot/fix-28
2 parents 11b8a29 + f8eaf03 commit dc4dff4

File tree

8 files changed

+913
-65
lines changed

8 files changed

+913
-65
lines changed

docs/auth_plugins.md

Lines changed: 262 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,267 @@
11
# Auth Plugins
22

3-
This document provides an example of how to implement a custom authentication plugin for a hypothetical system. The plugin checks for a specific authorization header and validates it against a secret stored in an environment variable.
3+
This document provides information on how to use authentication plugins for webhook validation, including built-in plugins and how to implement custom authentication plugins.
4+
5+
In your global configuration file (e.g. `hooks.yml`) you would likely set `auth_plugin_dir` to something like `./plugins/auth`.
6+
7+
Here is an example snippet of how you might configure the global settings in `hooks.yml`:
8+
9+
```yaml
10+
# hooks.yml
11+
auth_plugin_dir: ./plugins/auth # Directory where custom auth plugins are stored
12+
```
13+
14+
## Built-in Auth Plugins
15+
16+
The system comes with several built-in authentication plugins that cover common webhook authentication patterns.
17+
18+
### HMAC Authentication
19+
20+
The HMAC plugin provides secure signature-based authentication using HMAC (Hash-based Message Authentication Code). This is the most secure authentication method and is used by major webhook providers like GitHub, GitLab, and Shopify.
21+
22+
It works well because it HMACs provide the ability to verify both the integrity and authenticity of the request, ensuring that the payload has not been tampered with and that it comes from a trusted source.
23+
24+
**Type:** `hmac`
25+
26+
#### HMAC Configuration Options
27+
28+
##### `secret_env_key` (required)
29+
30+
The name of the environment variable containing the shared secret used for HMAC signature generation.
31+
32+
**Example:** `GITHUB_WEBHOOK_SECRET`
33+
34+
##### `header`
35+
36+
The HTTP header containing the HMAC signature.
37+
38+
**Default:** `X-Signature`
39+
**Example:** `X-Hub-Signature-256`
40+
41+
##### `algorithm`
42+
43+
The hashing algorithm to use for HMAC signature generation.
44+
45+
**Default:** `sha256`
46+
**Valid values:** `sha1`, `sha256`, `sha384`, `sha512`
47+
**Example:** `sha256`
48+
49+
##### `format`
50+
51+
The format of the signature in the header. This determines how the signature is structured.
52+
53+
**Default:** `algorithm=signature`
54+
55+
**Valid values:**
56+
57+
- `algorithm=signature` - Produces "sha256=abc123..." (GitHub, GitLab style)
58+
- `signature_only` - Produces "abc123..." (Shopify style)
59+
- `version=signature` - Produces "v0=abc123..." (Slack style)
60+
61+
##### `version_prefix`
62+
63+
The version prefix used when `format` is set to `version=signature`.
64+
65+
**Default:** `v0`
66+
**Example:** `v1`
67+
68+
##### `timestamp_header` (optional)
69+
70+
The HTTP header containing the request timestamp for timestamp validation. When specified, requests must include a valid timestamp within the tolerance window.
71+
72+
**Example:** `X-Request-Timestamp`
73+
74+
##### `timestamp_tolerance`
75+
76+
The maximum age (in seconds) allowed for timestamped requests. Only used when `timestamp_header` is specified.
77+
78+
**Default:** `300` (5 minutes)
79+
**Example:** `600`
80+
81+
##### `payload_template` (optional)
82+
83+
A template for constructing the payload used in signature generation when timestamp validation is enabled. Use placeholders like `{version}`, `{timestamp}`, and `{body}`.
84+
85+
**Example:** `{version}:{timestamp}:{body}`
86+
87+
#### HMAC Examples
88+
89+
**Basic GitHub-style HMAC:**
90+
91+
```yaml
92+
auth:
93+
type: hmac
94+
secret_env_key: GITHUB_WEBHOOK_SECRET
95+
header: X-Hub-Signature-256
96+
algorithm: sha256
97+
format: "algorithm=signature" # produces "sha256=abc123..."
98+
```
99+
100+
**Shopify-style HMAC (signature only):**
101+
102+
```yaml
103+
auth:
104+
type: hmac
105+
secret_env_key: SHOPIFY_WEBHOOK_SECRET
106+
header: X-Shopify-Hmac-Sha256
107+
algorithm: sha256
108+
format: "signature_only" # produces "abc123..."
109+
```
110+
111+
**Slack-style HMAC with timestamp validation:**
112+
113+
This is the most secure authentication method as it includes timestamp validation directly in the HMAC signature, preventing replay attacks even if an attacker intercepts the request.
114+
115+
```yaml
116+
auth:
117+
type: hmac
118+
secret_env_key: SLACK_WEBHOOK_SECRET
119+
header: X-Slack-Signature
120+
timestamp_header: X-Slack-Request-Timestamp
121+
timestamp_tolerance: 300 # 5 minutes
122+
algorithm: sha256
123+
format: "version=signature" # produces "v0=abc123..."
124+
version_prefix: "v0"
125+
payload_template: "{version}:{timestamp}:{body}"
126+
```
127+
128+
**Security Benefits:**
129+
130+
The timestamp validation provides several critical security advantages:
131+
132+
1. **Replay Attack Prevention**: Even if an attacker captures a valid request, they cannot replay it after the timestamp tolerance window expires
133+
2. **HMAC Integrity**: The timestamp is included in the HMAC calculation itself (via `payload_template`), so tampering with either the timestamp or payload will invalidate the signature
134+
3. **Time-bound Validity**: Requests are only valid within a specific time window, reducing the attack surface
135+
136+
**How it works:**
137+
138+
1. The client includes the current Unix timestamp in the `X-Slack-Request-Timestamp` header
139+
2. The HMAC is calculated over a constructed payload using the template: `{version}:{timestamp}:{body}`
140+
3. For example, if the version is "v0", timestamp is "1609459200", and body is `{"event":"push"}`, the signed payload becomes: `v0:1609459200:{"event":"push"}`
141+
4. The resulting signature format is: `v0=computed_hmac_hash`
142+
143+
**Example curl request:**
144+
145+
```bash
146+
#!/bin/bash
147+
148+
# Configuration
149+
WEBHOOK_URL="https://your-hooks-server.com/webhooks/slack"
150+
SECRET="your_slack_webhook_secret"
151+
TIMESTAMP=$(date +%s)
152+
PAYLOAD='{"event":"push","repository":"my-repo"}'
153+
154+
# Construct the signing payload
155+
VERSION="v0"
156+
SIGNING_PAYLOAD="${VERSION}:${TIMESTAMP}:${PAYLOAD}"
157+
158+
# Generate HMAC signature
159+
SIGNATURE=$(echo -n "$SIGNING_PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" -hex | cut -d' ' -f2)
160+
FORMATTED_SIGNATURE="${VERSION}=${SIGNATURE}"
161+
162+
# Send the request
163+
curl -X POST "$WEBHOOK_URL" \
164+
-H "Content-Type: application/json" \
165+
-H "X-Slack-Signature: $FORMATTED_SIGNATURE" \
166+
-H "X-Slack-Request-Timestamp: $TIMESTAMP" \
167+
-d "$PAYLOAD"
168+
```
169+
170+
**Important Security Notes:**
171+
172+
- The timestamp must be included in the HMAC calculation (not just validated separately) to prevent signature reuse with different timestamps
173+
- Use a reasonable `timestamp_tolerance` (5-10 minutes) to account for clock skew while minimizing replay window
174+
- Always use HTTPS to prevent man-in-the-middle attacks
175+
- Store webhook secrets securely
176+
177+
**General HMAC with timestamp validation (no version):**
178+
179+
For services that require timestamp validation but don't use version prefixes, you can use a simpler template format with the standard `algorithm=signature` format.
180+
181+
```yaml
182+
auth:
183+
type: hmac
184+
secret_env_key: WEBHOOK_SECRET
185+
header: X-Signature
186+
timestamp_header: X-Timestamp
187+
timestamp_tolerance: 600 # 10 minutes
188+
algorithm: sha256
189+
format: "algorithm=signature" # produces "sha256=abc123..."
190+
payload_template: "{timestamp}:{body}"
191+
```
192+
193+
**Example curl request:**
194+
195+
```bash
196+
#!/bin/bash
197+
198+
# Configuration
199+
WEBHOOK_URL="https://your-hooks-server.com/webhooks/generic"
200+
SECRET="your_webhook_secret"
201+
TIMESTAMP=$(date +%s)
202+
PAYLOAD='{"event":"deployment","status":"success"}'
203+
204+
# Construct the signing payload (timestamp:body format)
205+
SIGNING_PAYLOAD="${TIMESTAMP}:${PAYLOAD}"
206+
207+
# Generate HMAC signature
208+
SIGNATURE=$(echo -n "$SIGNING_PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" -hex | cut -d' ' -f2)
209+
FORMATTED_SIGNATURE="sha256=${SIGNATURE}"
210+
211+
# Send the request
212+
curl -X POST "$WEBHOOK_URL" \
213+
-H "Content-Type: application/json" \
214+
-H "X-Signature: $FORMATTED_SIGNATURE" \
215+
-H "X-Timestamp: $TIMESTAMP" \
216+
-d "$PAYLOAD"
217+
```
218+
219+
This approach provides strong security through timestamp validation while using a simpler format than the Slack-style implementation. The signing payload becomes `1609459200:{"event":"deployment","status":"success"}` and the resulting signature format is `sha256=computed_hmac_hash`.
220+
221+
### Shared Secret Authentication
222+
223+
The SharedSecret plugin provides simple secret-based authentication by comparing a secret value sent in an HTTP header. While simpler than HMAC, it provides less security since the secret is transmitted directly in the request header.
224+
225+
**Type:** `shared_secret`
226+
227+
#### Shared Secret Configuration Options
228+
229+
##### `secret_env_key` (required for shared secrets)
230+
231+
The name of the environment variable containing the shared secret for validation.
232+
233+
**Example:** `WEBHOOK_SECRET`
234+
235+
##### `header` (contains the shared secret)
236+
237+
The HTTP header where the shared secret is transmitted.
238+
239+
**Default:** `Authorization`
240+
**Example:** `X-API-Key`
241+
242+
#### Shared Secret Examples
243+
244+
**Basic shared secret with Authorization header:**
245+
246+
```yaml
247+
auth:
248+
type: shared_secret
249+
secret_env_key: WEBHOOK_SECRET
250+
header: Authorization
251+
```
252+
253+
**Custom header shared secret:**
254+
255+
```yaml
256+
auth:
257+
type: shared_secret
258+
secret_env_key: API_KEY_SECRET
259+
header: X-API-Key
260+
```
261+
262+
## Custom Auth Plugins
263+
264+
This section provides an example of how to implement a custom authentication plugin for a hypothetical system. The plugin checks for a specific authorization header and validates it against a secret stored in an environment variable.
4265

5266
In your global configuration file (e.g. `hooks.yml`) you would likely set `auth_plugin_dir` to something like `./plugins/auth`.
6267

docs/configuration.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@ auth:
147147
format: "algorithm=signature" # produces "sha256=abc123..."
148148
```
149149

150+
See the [Auth Plugins documentation](./auth_plugins.md) for more details on how to implement custom authentication plugins. You will also find configurations for built-in authentication plugins in that document as well.
151+
150152
### `opts`
151153

152154
Additional options for the endpoint. This section can include any custom options that the handler may require. The options are specific to the handler and can vary based on its implementation. You can put anything your heart desires here.

lib/hooks/app/auth/auth.rb

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,8 @@ def validate_auth!(payload, headers, endpoint_config, global_config = {})
3737
end
3838

3939
log.debug("validating auth for request with auth_class: #{auth_class.name}")
40-
41-
unless auth_class.valid?(
42-
payload:,
43-
headers:,
44-
config: endpoint_config
45-
)
40+
unless auth_class.valid?(payload:, headers:, config: endpoint_config)
41+
log.warn("authentication failed for request with auth_class: #{auth_class.name}")
4642
error!("authentication failed", 401)
4743
end
4844
end

0 commit comments

Comments
 (0)