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
1 change: 1 addition & 0 deletions docs/.vuepress/sets/craft-cloud.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ module.exports = {
"compatibility",
"static-caching",
"esi",
"request-signing",
"quotas",
"licensing",
"backups",
Expand Down
134 changes: 134 additions & 0 deletions docs/cloud/request-signing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
---
description: Sign trusted programmatic requests to avoid bot rate limiting.
---

# Request Signing

Request signing allows trusted systems to make programmatic requests to Craft Cloud without being treated like unsanctioned bot traffic.

This is useful for automated systems like static site builds or CI/CD pipelines, which will often be identified (correctly!) as “bots” and be rate-limited more aggressively than browsers.

Each environment’s `$CRAFT_CLOUD_SIGNING_KEY` [system variable](environments.md#variables) is used as a shared secret when generating and validating signed requests.

::: tip
For more details on RFC 9421 HTTP Message Signatures, see [httpsig.org](https://httpsig.org/).
:::

## Creating a Signed Request

External systems can generate valid signatures for a Craft Cloud environment, provided the corresponding `$CRAFT_CLOUD_SIGNING_KEY`.

Signatures are valid at the Craft Cloud gateway for a maximum of **five minutes**.
A signed request is not consumed (like a token URL is, in Craft), and they are not idempotent.

### From Node.js

This example uses [`http-message-sig`](https://www.npmjs.com/package/http-message-sig) to generate an RFC 9421-compliant signature:

```bash
npm install http-message-sig
```

Build and send a signed request like this:

```js
import crypto from 'node:crypto';
import { signatureHeadersSync } from 'http-message-sig';

const method = 'POST';

// These variables/secrets can be store in (and fetched from) the environment, instead:
const url = 'https://my-env.some-domain.com/api';
const schemaToken = 'WcVqivS64CCRQN9ohVcKk5FB6RIFTApd';

const body = JSON.stringify({
query: `
{
entries(section: "blog") {
title
url
}
}
`,
});

const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${schemaToken}`,
};

const created = new Date();

const signer = {
keyid: 'hmac',
alg: 'hmac-sha256',
signSync(data) {
return crypto
.createHmac('sha256', process.env.CRAFT_CLOUD_SIGNING_KEY)
.update(data)
.digest();
},
};

const signatureHeaders = signatureHeadersSync(
{ method, url, headers, body },
{
key: 'sig',
signer,
components: ['@method', '@target-uri'],
created,
// This is optional (and cannot exceed five minutes, to validate at the edge):
expires: new Date(created.getTime() + 60_000),
},
);

const response = await fetch(url, {
method,
headers: {
...headers,
...signatureHeaders,
},
body,
});

const result = await response.json();
```

::: tip
Requests signed using the `@target-uri` [component](https://www.rfc-editor.org/rfc/rfc9421.html#name-derived-components) are only valid when sent to a URL that matches _exactly_, including the scheme, hostname, path, and query string.
The example above satisfies this by using the same `url` variable for the signed request and the `fetch()` call.
:::

### From Craft

Any Craft project running on Cloud can sign requests.
This can be useful when making HTTP requests from a console command, queue job, or for communication between environments or projects.

```php
use Craft;

use craft\cloud\Module;
use GuzzleHttp\Psr7\Request;

$signer = Module::getInstance()->getRequestSigner();

$request = new Request(
'POST',
'https://api.example.test/webhook',
['Content-Type' => 'application/json'],
json_encode([
'event' => 'order.paid',
], JSON_THROW_ON_ERROR),
);

$signedRequest = $signer->sign($request);

$response = Craft::createGuzzleClient()->send($signedRequest);
```

## Signature Verification

Craft Cloud automatically tries to validate signed requests, at the gateway.
If validation _fails_, normal bot- and rate-limiting rules are applied; if no policies are triggered, the request is forwarded to Craft like any other.

Once the request reaches your application, you are free to perform additional verification (like checking a separate shared secret).
Loading