Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
263 changes: 262 additions & 1 deletion docs/auth_plugins.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,267 @@
# Auth Plugins

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.
This document provides information on how to use authentication plugins for webhook validation, including built-in plugins and how to implement custom authentication plugins.

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

Here is an example snippet of how you might configure the global settings in `hooks.yml`:

```yaml
# hooks.yml
auth_plugin_dir: ./plugins/auth # Directory where custom auth plugins are stored
```

## Built-in Auth Plugins

The system comes with several built-in authentication plugins that cover common webhook authentication patterns.

### HMAC Authentication

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.

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.

**Type:** `hmac`

#### HMAC Configuration Options

##### `secret_env_key` (required)

The name of the environment variable containing the shared secret used for HMAC signature generation.

**Example:** `GITHUB_WEBHOOK_SECRET`

##### `header`

The HTTP header containing the HMAC signature.

**Default:** `X-Signature`
**Example:** `X-Hub-Signature-256`

##### `algorithm`

The hashing algorithm to use for HMAC signature generation.

**Default:** `sha256`
**Valid values:** `sha1`, `sha256`, `sha384`, `sha512`
**Example:** `sha256`

##### `format`

The format of the signature in the header. This determines how the signature is structured.

**Default:** `algorithm=signature`

**Valid values:**

- `algorithm=signature` - Produces "sha256=abc123..." (GitHub, GitLab style)
- `signature_only` - Produces "abc123..." (Shopify style)
- `version=signature` - Produces "v0=abc123..." (Slack style)

##### `version_prefix`

The version prefix used when `format` is set to `version=signature`.

**Default:** `v0`
**Example:** `v1`

##### `timestamp_header` (optional)

The HTTP header containing the request timestamp for timestamp validation. When specified, requests must include a valid timestamp within the tolerance window.

**Example:** `X-Request-Timestamp`

##### `timestamp_tolerance`

The maximum age (in seconds) allowed for timestamped requests. Only used when `timestamp_header` is specified.

**Default:** `300` (5 minutes)
**Example:** `600`

##### `payload_template` (optional)

A template for constructing the payload used in signature generation when timestamp validation is enabled. Use placeholders like `{version}`, `{timestamp}`, and `{body}`.

**Example:** `{version}:{timestamp}:{body}`

#### HMAC Examples

**Basic GitHub-style HMAC:**

```yaml
auth:
type: hmac
secret_env_key: GITHUB_WEBHOOK_SECRET
header: X-Hub-Signature-256
algorithm: sha256
format: "algorithm=signature" # produces "sha256=abc123..."
```

**Shopify-style HMAC (signature only):**

```yaml
auth:
type: hmac
secret_env_key: SHOPIFY_WEBHOOK_SECRET
header: X-Shopify-Hmac-Sha256
algorithm: sha256
format: "signature_only" # produces "abc123..."
```

**Slack-style HMAC with timestamp validation:**

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.

```yaml
auth:
type: hmac
secret_env_key: SLACK_WEBHOOK_SECRET
header: X-Slack-Signature
timestamp_header: X-Slack-Request-Timestamp
timestamp_tolerance: 300 # 5 minutes
algorithm: sha256
format: "version=signature" # produces "v0=abc123..."
version_prefix: "v0"
payload_template: "{version}:{timestamp}:{body}"
```

**Security Benefits:**

The timestamp validation provides several critical security advantages:

1. **Replay Attack Prevention**: Even if an attacker captures a valid request, they cannot replay it after the timestamp tolerance window expires
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
3. **Time-bound Validity**: Requests are only valid within a specific time window, reducing the attack surface

**How it works:**

1. The client includes the current Unix timestamp in the `X-Slack-Request-Timestamp` header
2. The HMAC is calculated over a constructed payload using the template: `{version}:{timestamp}:{body}`
3. For example, if the version is "v0", timestamp is "1609459200", and body is `{"event":"push"}`, the signed payload becomes: `v0:1609459200:{"event":"push"}`
4. The resulting signature format is: `v0=computed_hmac_hash`

**Example curl request:**

```bash
#!/bin/bash

# Configuration
WEBHOOK_URL="https://your-hooks-server.com/webhooks/slack"
SECRET="your_slack_webhook_secret"
TIMESTAMP=$(date +%s)
PAYLOAD='{"event":"push","repository":"my-repo"}'

# Construct the signing payload
VERSION="v0"
SIGNING_PAYLOAD="${VERSION}:${TIMESTAMP}:${PAYLOAD}"

# Generate HMAC signature
SIGNATURE=$(echo -n "$SIGNING_PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" -hex | cut -d' ' -f2)
FORMATTED_SIGNATURE="${VERSION}=${SIGNATURE}"

# Send the request
curl -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-H "X-Slack-Signature: $FORMATTED_SIGNATURE" \
-H "X-Slack-Request-Timestamp: $TIMESTAMP" \
-d "$PAYLOAD"
```

**Important Security Notes:**

- The timestamp must be included in the HMAC calculation (not just validated separately) to prevent signature reuse with different timestamps
- Use a reasonable `timestamp_tolerance` (5-10 minutes) to account for clock skew while minimizing replay window
- Always use HTTPS to prevent man-in-the-middle attacks
- Store webhook secrets securely

**General HMAC with timestamp validation (no version):**

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.

```yaml
auth:
type: hmac
secret_env_key: WEBHOOK_SECRET
header: X-Signature
timestamp_header: X-Timestamp
timestamp_tolerance: 600 # 10 minutes
algorithm: sha256
format: "algorithm=signature" # produces "sha256=abc123..."
payload_template: "{timestamp}:{body}"
```

**Example curl request:**

```bash
#!/bin/bash

# Configuration
WEBHOOK_URL="https://your-hooks-server.com/webhooks/generic"
SECRET="your_webhook_secret"
TIMESTAMP=$(date +%s)
PAYLOAD='{"event":"deployment","status":"success"}'

# Construct the signing payload (timestamp:body format)
SIGNING_PAYLOAD="${TIMESTAMP}:${PAYLOAD}"

# Generate HMAC signature
SIGNATURE=$(echo -n "$SIGNING_PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" -hex | cut -d' ' -f2)
FORMATTED_SIGNATURE="sha256=${SIGNATURE}"

# Send the request
curl -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-H "X-Signature: $FORMATTED_SIGNATURE" \
-H "X-Timestamp: $TIMESTAMP" \
-d "$PAYLOAD"
```

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`.

### Shared Secret Authentication

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.

**Type:** `shared_secret`

#### Shared Secret Configuration Options

##### `secret_env_key` (required for shared secrets)

The name of the environment variable containing the shared secret for validation.

**Example:** `WEBHOOK_SECRET`

##### `header` (contains the shared secret)

The HTTP header where the shared secret is transmitted.

**Default:** `Authorization`
**Example:** `X-API-Key`

#### Shared Secret Examples

**Basic shared secret with Authorization header:**

```yaml
auth:
type: shared_secret
secret_env_key: WEBHOOK_SECRET
header: Authorization
```

**Custom header shared secret:**

```yaml
auth:
type: shared_secret
secret_env_key: API_KEY_SECRET
header: X-API-Key
```

## Custom Auth Plugins

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.

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

Expand Down
2 changes: 2 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ auth:
format: "algorithm=signature" # produces "sha256=abc123..."
```

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.

### `opts`

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.
8 changes: 2 additions & 6 deletions lib/hooks/app/auth/auth.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,8 @@ def validate_auth!(payload, headers, endpoint_config, global_config = {})
end

log.debug("validating auth for request with auth_class: #{auth_class.name}")

unless auth_class.valid?(
payload:,
headers:,
config: endpoint_config
)
unless auth_class.valid?(payload:, headers:, config: endpoint_config)
log.warn("authentication failed for request with auth_class: #{auth_class.name}")
error!("authentication failed", 401)
end
end
Expand Down
Loading
Loading