Skip to content
Open
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
128 changes: 128 additions & 0 deletions docs/cloud/request-signing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
---
description: Sign trusted programmatic requests to avoid bot rate limiting.
---

# Request Signing

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

This is useful for automated systems like headless build processes or CI/CD pipelines, which can correctly look like bots and be rate-limited more aggressively than browsers.

When Cloud verifies a request signature, it treats the request as project-approved and bypasses bot-specific rate limiting.

Signatures use the environment’s `$CRAFT_CLOUD_SIGNING_KEY` to generate signatures. Treat this as a secret!

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

## Signing Requests from Craft

The `craftcms/cloud` package can sign any PSR-7 request:

```php
use craft\cloud\Module;
use GuzzleHttp\Client;
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 = (new Client())->send($signedRequest);
```

To verify a signed PSR-7 request in Craft, use the same signing key:

```php
use craft\helpers\App;
use HttpMessageSignatures\Algorithm\HmacSha256;
use HttpMessageSignatures\Verifier;

$isValid = (new Verifier(new HmacSha256(App::env('CRAFT_CLOUD_SIGNING_KEY'))))
->verify($request);
```

## Signing Requests Externally

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

Signatures expire after 5 minutes when verified by the Craft Cloud gateway. Set `expires` about 5 minutes after `created`.

### Node.js

This example signs a request with [`http-message-sig`](https://www.npmjs.com/package/http-message-sig):

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

Then sign the request before sending it to Craft:

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

const method = 'POST';
const url = process.env.CRAFT_GRAPHQL_URL;

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

const headers = {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.CRAFT_GRAPHQL_TOKEN}`,
};

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,
expires: new Date(created.getTime() + 300_000),
},
);

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

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

Store the signing key in the external system’s secret manager. The `@target-uri` value must match the requested URL exactly, including any query string.
Loading