Skip to content

Commit 9e5cec2

Browse files
committed
Switch to request-time HTTP message signing
1 parent da77b04 commit 9e5cec2

15 files changed

Lines changed: 528 additions & 233 deletions

README.md

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,113 @@ composer update "craftcms/cms:^5" "craftcms/flysystem:^2.0" --with-all-dependenc
5353

5454
## Developer Features
5555

56+
### Signed HTTP Requests
57+
58+
Use the module’s request signer to sign a PSR-7 request with Cloud’s signing key before sending it to any destination that can verify HTTP message signatures.
59+
60+
```php
61+
use craft\cloud\Module;
62+
use GuzzleHttp\Psr7\Request;
63+
64+
$signer = Module::getInstance()->getRequestSigner();
65+
66+
$signedRequest = $signer->sign(new Request('POST', 'https://example.test/webhook'));
67+
```
68+
69+
For Guzzle clients, use the signer’s handler stack helper so each request is signed when it is sent.
70+
71+
```php
72+
use Craft;
73+
use craft\cloud\Module;
74+
use GuzzleHttp\RequestOptions;
75+
76+
$signer = Module::getInstance()->getRequestSigner();
77+
78+
$client = Craft::createGuzzleClient([
79+
'handler' => $signer->createHandlerStack(),
80+
]);
81+
82+
$client->post('https://example.test/webhook', [
83+
RequestOptions::JSON => [
84+
'event' => 'asset.saved',
85+
],
86+
]);
87+
```
88+
89+
External systems can create compatible signatures without this PHP package. For example, install
90+
[`http-message-signatures`](https://www.npmjs.com/package/http-message-signatures) in a Node-based build environment:
91+
92+
```bash
93+
npm install http-message-signatures
94+
```
95+
96+
Then a Vercel build script can sign a Craft GraphQL request:
97+
98+
```js
99+
import httpMessageSignatures from 'http-message-signatures';
100+
101+
const {
102+
createSigner,
103+
httpbis: { signMessage },
104+
} = httpMessageSignatures;
105+
106+
const method = 'POST';
107+
const url = process.env.CRAFT_GRAPHQL_URL;
108+
const signingKey = process.env.CRAFT_CLOUD_SIGNING_KEY;
109+
110+
if (!url || !signingKey) {
111+
throw new Error('CRAFT_GRAPHQL_URL and CRAFT_CLOUD_SIGNING_KEY are required.');
112+
}
113+
114+
const unsignedRequest = {
115+
method,
116+
headers: {
117+
'Content-Type': 'application/json',
118+
...(process.env.CRAFT_GRAPHQL_TOKEN
119+
? { Authorization: `Bearer ${process.env.CRAFT_GRAPHQL_TOKEN}` }
120+
: {}),
121+
},
122+
body: JSON.stringify({
123+
query: `
124+
query BuildData {
125+
entries(section: "news") {
126+
title
127+
url
128+
}
129+
}
130+
`,
131+
}),
132+
url,
133+
};
134+
135+
const signedRequest = await signMessage(
136+
{
137+
key: createSigner(signingKey, 'hmac-sha256', 'hmac'),
138+
fields: ['@method', '@target-uri'],
139+
params: ['created', 'expires', 'alg', 'keyid'],
140+
},
141+
unsignedRequest,
142+
);
143+
144+
const response = await fetch(url, {
145+
method: signedRequest.method,
146+
headers: signedRequest.headers,
147+
body: signedRequest.body,
148+
});
149+
150+
if (!response.ok) {
151+
throw new Error(`Craft GraphQL request failed: ${response.status}`);
152+
}
153+
154+
const responseBody = await response.json();
155+
156+
if (responseBody.errors) {
157+
throw new Error(`Craft GraphQL returned errors: ${JSON.stringify(responseBody.errors)}`);
158+
}
159+
```
160+
161+
The `@target-uri` value must be the exact URL being requested, including any query string.
162+
56163
### Template Helpers
57164

58165
#### `cloud.artifactUrl()`
@@ -109,7 +216,7 @@ Most configuration (to Craft and the extension itself) is handled directly by Cl
109216
| `accessSecret` | `string` | AWS access secret, used in conjunction with the `accessKey`. |
110217
| `accessToken` | `string` | AWS access token. |
111218
| `redisUrl` | `string` | Connection string for the environment’s Redis instance. |
112-
| `signingKey` | `string` | A secret value used to protect transform URLs against abuse. |
219+
| `signingKey` | `string` | A secret value used to protect transform URLs and sign HTTP requests. |
113220
| `useAssetBundleCdn` | `boolean` | Whether or not to enable the CDN for asset bundles. |
114221
| `previewDomain` | `string\|null` | Set when accessing an environment from its [preview domain](https://craftcms.com/knowledge-base/cloud-domains#preview-domains). |
115222
| `useQueue` | `boolean` | Whether or not to use Cloud’s SQS-backed queue driver. |

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@
88
"bref/extra-php-extensions": "^3",
99
"craftcms/cms": "^4.6 || ^5",
1010
"craftcms/flysystem": "^1.0.0 || ^2.0.0",
11+
"craftcms/http-message-signatures": "^0.1",
1112
"guzzlehttp/guzzle": "^7.4.5",
1213
"league/flysystem-aws-s3-v3": "^3.15",
1314
"league/uri": "^7.6",
1415
"league/uri-components": "^7.6",
1516
"yiisoft/yii2-redis": "^2.0",
1617
"yiisoft/yii2-queue": "^2.3.7",
1718
"phlak/semver": "^4.1",
18-
"99designs/http-signatures": "^4.0",
1919
"symfony/process": "^6",
2020
"aws/aws-sdk-php": "^3.342.6",
2121
"craftcms/yii2-cache-cascade": "^1.2.1"

0 commit comments

Comments
 (0)