Skip to content

Commit 3898341

Browse files
authored
[3.x] Standardize Cloud signing (#169)
1 parent da77b04 commit 3898341

15 files changed

Lines changed: 509 additions & 233 deletions

README.md

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,106 @@ 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+
External systems can create compatible signatures without this PHP package. See
70+
[httpsig.org](https://httpsig.org/) for more information about HTTP message signatures. For example,
71+
install [`http-message-sig`](https://www.npmjs.com/package/http-message-sig) in a Node-based build
72+
environment:
73+
74+
```bash
75+
npm install http-message-sig
76+
```
77+
78+
Then a build script, e.g. on Vercel, can sign a Craft GraphQL request:
79+
80+
```js
81+
import crypto from 'node:crypto';
82+
import { signatureHeadersSync } from 'http-message-sig';
83+
84+
const method = 'POST';
85+
const url = process.env.CRAFT_GRAPHQL_URL;
86+
87+
const body = JSON.stringify({
88+
query: `
89+
{
90+
entries(section: "blog") {
91+
title
92+
url
93+
}
94+
}
95+
`,
96+
});
97+
98+
const headers = {
99+
'Content-Type': 'application/json',
100+
Authorization: `Bearer ${process.env.CRAFT_GRAPHQL_TOKEN}`,
101+
};
102+
103+
const signer = {
104+
keyid: 'hmac',
105+
alg: 'hmac-sha256',
106+
signSync(data) {
107+
return crypto
108+
.createHmac('sha256', process.env.CRAFT_CLOUD_SIGNING_KEY)
109+
.update(data)
110+
.digest();
111+
},
112+
};
113+
114+
const created = new Date();
115+
const signatureHeaders = signatureHeadersSync(
116+
{ method, url, headers, body },
117+
{
118+
key: 'sig',
119+
signer,
120+
components: ['@method', '@target-uri'],
121+
created,
122+
expires: new Date(created.getTime() + 300_000),
123+
},
124+
);
125+
126+
const response = await fetch(url, {
127+
method,
128+
headers: {
129+
...headers,
130+
...signatureHeaders,
131+
},
132+
body,
133+
});
134+
135+
const responseBody = await response.json();
136+
```
137+
138+
The `@target-uri` value must be the exact URL being requested, including any query string.
139+
140+
### Signed URLs
141+
142+
Use the module’s URL signer when you need a signed URL instead of a signed HTTP request.
143+
144+
```php
145+
use craft\cloud\Module;
146+
147+
$signer = Module::getInstance()->getUrlSigner();
148+
149+
$signedUrl = $signer->sign('https://example.test/downloads/report.pdf?version=latest');
150+
$isValid = $signer->verify($signedUrl);
151+
```
152+
153+
URL signatures cover the URL’s path and query string, so changing either invalidates the
154+
signature.
155+
56156
### Template Helpers
57157

58158
#### `cloud.artifactUrl()`
@@ -109,7 +209,7 @@ Most configuration (to Craft and the extension itself) is handled directly by Cl
109209
| `accessSecret` | `string` | AWS access secret, used in conjunction with the `accessKey`. |
110210
| `accessToken` | `string` | AWS access token. |
111211
| `redisUrl` | `string` | Connection string for the environment’s Redis instance. |
112-
| `signingKey` | `string` | A secret value used to protect transform URLs against abuse. |
212+
| `signingKey` | `string` | A secret value used to protect transform URLs and sign HTTP requests. |
113213
| `useAssetBundleCdn` | `boolean` | Whether or not to enable the CDN for asset bundles. |
114214
| `previewDomain` | `string\|null` | Set when accessing an environment from its [preview domain](https://craftcms.com/knowledge-base/cloud-domains#preview-domains). |
115215
| `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)