Skip to content

Commit 2054015

Browse files
Update R2 presigned url, download, upload, and delete docs (#26824)
* Updated R2 presigned url, download, and upload docs * Add an example of a presigned url and their signature parameters * Provide consistency in variable naming and comments across aws sdk example documentation * Copy updates from suggested edits * add cors rule and content-type header in aws-sdk-js-v3 aws-sdk-js aws4fetch and boto3 * small copy update * Updated CORS presigned url configuration, and added ways to add CORS with CLI. Also added best practice in presigned url * add inline comments to s3 client in object docs * Standardize placeholder syntax to use angle brackets * small copy fix for clarity --------- Co-authored-by: Anni Wang <anni@cloudflare.com>
1 parent 09e16b2 commit 2054015

18 files changed

+880
-395
lines changed

src/content/docs/r2/api/s3/presigned-urls.mdx

Lines changed: 167 additions & 175 deletions
Large diffs are not rendered by default.

src/content/docs/r2/buckets/cors.mdx

Lines changed: 56 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -33,44 +33,34 @@ Next, [add a CORS policy](#add-cors-policies-from-the-dashboard) to your bucket
3333

3434
## Use CORS with a presigned URL
3535

36-
Presigned URLs are an S3 concept that contain a special signature that encodes details of an S3 action, such as `GetObject` or `PutObject`. Presigned URLs are only used for authentication, which means they are generally safe to distribute publicly without revealing any secrets.
37-
38-
### Create a presigned URL
39-
40-
You will need a pair of S3-compatible credentials to use when you generate the presigned URL.
41-
42-
The example below shows how to generate a presigned `PutObject` URL using the [`@aws-sdk/client-s3`](https://www.npmjs.com/package/@aws-sdk/client-s3) package for JavaScript.
43-
44-
```js
45-
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
46-
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
47-
const S3 = new S3Client({
48-
endpoint: "https://<account_id>.r2.cloudflarestorage.com",
49-
credentials: {
50-
accessKeyId: "<access_key_id>",
51-
secretAccessKey: "<access_key_secret>",
52-
},
53-
region: "auto",
54-
});
55-
const url = await getSignedUrl(
56-
S3,
57-
new PutObjectCommand({
58-
Bucket: bucket,
59-
Key: object,
60-
}),
61-
{
62-
expiresIn: 60 * 60 * 24 * 7, // 7d
63-
},
64-
);
65-
console.log(url);
66-
```
36+
[Presigned URLs](/r2/api/s3/presigned-urls/) allow temporary access to perform specific actions on your bucket without exposing your credentials. While presigned URLs handle authentication, you still need to configure CORS when making requests from a browser.
6737

68-
### Test the presigned URL
38+
When a browser makes a request to a presigned URL on a different origin, the browser enforces CORS. Without a CORS policy, browser-based uploads and downloads using presigned URLs will fail, even though the presigned URL itself is valid.
6939

70-
Test the presigned URL by uploading an object using cURL. The example below would upload the `123` text to R2 with a `Content-Type` of `text/plain`.
40+
To enable browser-based access with presigned URLs:
7141

72-
```sh
73-
curl --request PUT <URL> --header "Content-Type: text/plain" --data "123"
42+
1. [Add a CORS policy](#add-cors-policies-from-the-dashboard) to your bucket that allows requests from your application's origin.
43+
44+
2. Set `AllowedMethods` to match the operations your presigned URLs perform, use `GET`, `PUT`, `HEAD`, and/or `DELETE`.
45+
46+
3. Set `AllowedHeaders` to include any headers the client will send when using the presigned URL, such as headers for content type, checksums, caching, or custom metadata.
47+
48+
4. (Optional) Set `ExposeHeaders` to allow your JavaScript to read response headers like `ETag`, which contains the object's hash and is useful for verifying uploads.
49+
50+
5. (Optional) Set `MaxAgeSeconds` to cache the preflight response and reduce the number of preflight requests the browser makes.
51+
52+
The following example allows browser-based uploads from `https://example.com` with a `Content-Type` header:
53+
54+
```json
55+
[
56+
{
57+
"AllowedOrigins": ["https://example.com"],
58+
"AllowedMethods": ["PUT"],
59+
"AllowedHeaders": ["Content-Type"],
60+
"ExposeHeaders": ["ETag"],
61+
"MaxAgeSeconds": 3600
62+
}
63+
]
7464
```
7565

7666
## Add CORS policies from the dashboard
@@ -86,6 +76,37 @@ curl --request PUT <URL> --header "Content-Type: text/plain" --data "123"
8676

8777
Your policy displays on the **Settings** page for your bucket.
8878

79+
## Add CORS policies via Wrangler CLI
80+
81+
You can configure CORS rules using the [Wrangler CLI](/r2/reference/wrangler-commands/).
82+
83+
1. Create a JSON file with your CORS configuration:
84+
85+
```json title="cors.json"
86+
{
87+
"rules": [
88+
{
89+
"allowed": {
90+
"origins": ["https://example.com"],
91+
"methods": ["GET"]
92+
}
93+
}
94+
]
95+
}
96+
```
97+
98+
2. Apply the CORS policy to your bucket:
99+
100+
```sh
101+
npx wrangler r2 bucket cors set <BUCKET_NAME> --file cors.json
102+
```
103+
104+
3. Verify the CORS policy was applied:
105+
106+
```sh
107+
npx wrangler r2 bucket cors list <BUCKET_NAME>
108+
```
109+
89110
## Response headers
90111

91112
The following fields in an R2 CORS policy map to HTTP response headers. These response headers are only returned when the incoming HTTP request is a valid CORS request.

src/content/docs/r2/examples/aws/aws-cli.mdx

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,23 @@ aws configure
1515
```
1616

1717
```sh output
18-
AWS Access Key ID [None]: <access_key_id>
19-
AWS Secret Access Key [None]: <access_key_secret>
18+
AWS Access Key ID [None]: <ACCESS_KEY_ID>
19+
AWS Secret Access Key [None]: <SECRET_ACCESS_KEY>
2020
Default region name [None]: auto
2121
Default output format [None]: json
2222
```
2323

24+
The `region` value can be set to `auto` since it is required by the SDK but not used by R2.
25+
2426
You may then use the `aws` CLI for any of your normal workflows.
2527

2628
```sh
27-
aws s3api list-buckets --endpoint-url https://<accountid>.r2.cloudflarestorage.com
29+
# Provide your Cloudflare account ID
30+
aws s3api list-buckets --endpoint-url https://<ACCOUNT_ID>.r2.cloudflarestorage.com
2831
# {
2932
# "Buckets": [
3033
# {
31-
# "Name": "sdk-example",
34+
# "Name": "my-bucket",
3235
# "CreationDate": "2022-05-18T17:19:59.645000+00:00"
3336
# }
3437
# ],
@@ -38,7 +41,7 @@ aws s3api list-buckets --endpoint-url https://<accountid>.r2.cloudflarestorage.c
3841
# }
3942
# }
4043

41-
aws s3api list-objects-v2 --endpoint-url https://<accountid>.r2.cloudflarestorage.com --bucket sdk-example
44+
aws s3api list-objects-v2 --endpoint-url https://<ACCOUNT_ID>.r2.cloudflarestorage.com --bucket my-bucket
4245
# {
4346
# "Contents": [
4447
# {
@@ -58,8 +61,6 @@ You can also generate presigned links which allow you to share public access to
5861

5962
```sh
6063
# You can pass the --expires-in flag to determine how long the presigned link is valid.
61-
$ aws s3 presign --endpoint-url https://<accountid>.r2.cloudflarestorage.com s3://sdk-example/ferriswasm.png --expires-in 3600
62-
# https://<accountid>.r2.cloudflarestorage.com/sdk-example/ferriswasm.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=<credential>&X-Amz-Date=<timestamp>&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=<signature>
63-
aws s3 presign --endpoint-url https://<accountid>.r2.cloudflarestorage.com s3://sdk-example/ferriswasm.png --expires-in 3600
64-
# https://<accountid>.r2.cloudflarestorage.com/sdk-example/ferriswasm.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=<credential>&X-Amz-Date=<timestamp>&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=<signature>
64+
aws s3 presign --endpoint-url https://<ACCOUNT_ID>.r2.cloudflarestorage.com s3://my-bucket/ferriswasm.png --expires-in 3600
65+
# https://<ACCOUNT_ID>.r2.cloudflarestorage.com/my-bucket/ferriswasm.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=<credential>&X-Amz-Date=<timestamp>&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=<signature>
6566
```

src/content/docs/r2/examples/aws/aws-sdk-go.mdx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,16 @@ import (
2626

2727
func main() {
2828
var bucketName = "sdk-example"
29-
var accountId = "<accountid>"
30-
var accessKeyId = "<access_key_id>"
31-
var accessKeySecret = "<access_key_secret>"
29+
// Provide your Cloudflare account ID
30+
var accountId = "<ACCOUNT_ID>"
31+
// Retrieve your S3 API credentials for your R2 bucket via API tokens
32+
// (see: https://developers.cloudflare.com/r2/api/tokens)
33+
var accessKeyId = "<ACCESS_KEY_ID>"
34+
var accessKeySecret = "<SECRET_ACCESS_KEY>"
3235

3336
cfg, err := config.LoadDefaultConfig(context.TODO(),
3437
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(accessKeyId, accessKeySecret, "")),
35-
config.WithRegion("auto"),
38+
config.WithRegion("auto"), // Required by SDK but not used by R2
3639
)
3740
if err != nil {
3841
log.Fatal(err)

src/content/docs/r2/examples/aws/aws-sdk-java.mdx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ public class CloudflareR2Client {
3535

3636
/**
3737
* Configuration class for R2 credentials and endpoint
38+
* - accountId: Your Cloudflare account ID
39+
* - accessKey: Your R2 Access Key ID (see: https://developers.cloudflare.com/r2/api/tokens)
40+
* - secretKey: Your R2 Secret Access Key (see: https://developers.cloudflare.com/r2/api/tokens)
3841
*/
3942
public static class S3Config {
4043
private final String accountId;
@@ -70,7 +73,7 @@ public class CloudflareR2Client {
7073
return S3Client.builder()
7174
.endpointOverride(URI.create(config.getEndpoint()))
7275
.credentialsProvider(StaticCredentialsProvider.create(credentials))
73-
.region(Region.of("auto"))
76+
.region(Region.of("auto")) // Required by SDK but not used by R2
7477
.serviceConfiguration(serviceConfiguration)
7578
.build();
7679
}
@@ -103,9 +106,9 @@ public class CloudflareR2Client {
103106

104107
public static void main(String[] args) {
105108
S3Config config = new S3Config(
106-
"your_account_id",
107-
"your_access_key",
108-
"your_secret_key"
109+
"<ACCOUNT_ID>",
110+
"<ACCESS_KEY_ID>",
111+
"<SECRET_ACCESS_KEY>"
109112
);
110113

111114
CloudflareR2Client r2Client = new CloudflareR2Client(config);
@@ -165,7 +168,7 @@ public class CloudflareR2Client {
165168
return S3Presigner.builder()
166169
.endpointOverride(URI.create(config.getEndpoint()))
167170
.credentialsProvider(StaticCredentialsProvider.create(credentials))
168-
.region(Region.of("auto"))
171+
.region(Region.of("auto")) // Required by SDK but not used by R2
169172
.serviceConfiguration(S3Configuration.builder()
170173
.pathStyleAccessEnabled(true)
171174
.build())

src/content/docs/r2/examples/aws/aws-sdk-js-v3.mdx

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ import {
2424
} from "@aws-sdk/client-s3";
2525

2626
const S3 = new S3Client({
27-
region: "auto",
27+
region: "auto", // Required by SDK but not used by R2
28+
// Provide your Cloudflare account ID
2829
endpoint: `https://${ACCOUNT_ID}.r2.cloudflarestorage.com`,
30+
// Retrieve your S3 API credentials for your R2 bucket via API tokens (see: https://developers.cloudflare.com/r2/api/tokens)
2931
credentials: {
3032
accessKeyId: ACCESS_KEY_ID,
3133
secretAccessKey: SECRET_ACCESS_KEY,
@@ -44,7 +46,7 @@ console.log(await S3.send(new ListBucketsCommand({})));
4446
// },
4547
// Buckets: [
4648
// { Name: 'user-uploads', CreationDate: 2022-04-13T21:23:47.102Z },
47-
// { Name: 'my-bucket-name', CreationDate: 2022-05-07T02:46:49.218Z }
49+
// { Name: 'my-bucket', CreationDate: 2022-05-07T02:46:49.218Z }
4850
// ],
4951
// Owner: {
5052
// DisplayName: '...',
@@ -53,7 +55,7 @@ console.log(await S3.send(new ListBucketsCommand({})));
5355
// }
5456

5557
console.log(
56-
await S3.send(new ListObjectsV2Command({ Bucket: "my-bucket-name" })),
58+
await S3.send(new ListObjectsV2Command({ Bucket: "my-bucket" })),
5759
);
5860
// {
5961
// '$metadata': {
@@ -91,7 +93,7 @@ console.log(
9193
// IsTruncated: false,
9294
// KeyCount: 8,
9395
// MaxKeys: 1000,
94-
// Name: 'my-bucket-name',
96+
// Name: 'my-bucket',
9597
// NextContinuationToken: undefined,
9698
// Prefix: undefined,
9799
// StartAfter: undefined
@@ -109,24 +111,72 @@ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
109111
console.log(
110112
await getSignedUrl(
111113
S3,
112-
new GetObjectCommand({ Bucket: "my-bucket-name", Key: "dog.png" }),
114+
new GetObjectCommand({ Bucket: "my-bucket", Key: "dog.png" }),
113115
{ expiresIn: 3600 },
114116
),
115117
);
116-
// https://my-bucket-name.<accountid>.r2.cloudflarestorage.com/dog.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential<credential>&X-Amz-Date=<timestamp>&X-Amz-Expires=3600&X-Amz-Signature=<signature>&X-Amz-SignedHeaders=host&x-id=GetObject
117-
118-
// You can also create links for operations such as putObject to allow temporary write access to a specific key.
118+
// You can also create links for operations such as PutObject to allow temporary write access to a specific key.
119+
// Specify ContentType to restrict uploads to a specific file type.
119120
console.log(
120121
await getSignedUrl(
121122
S3,
122-
new PutObjectCommand({ Bucket: "my-bucket-name", Key: "dog.png" }),
123+
new PutObjectCommand({
124+
Bucket: "my-bucket",
125+
Key: "dog.png",
126+
ContentType: "image/png",
127+
}),
123128
{ expiresIn: 3600 },
124129
),
125130
);
126131
```
127132

128-
You can use the link generated by the `putObject` example to upload to the specified bucket and key, until the presigned link expires.
133+
```sh output
134+
https://my-bucket.<ACCOUNT_ID>.r2.cloudflarestorage.com/dog.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=<credential>&X-Amz-Date=<timestamp>&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=<signature>&x-id=GetObject
135+
https://my-bucket.<ACCOUNT_ID>.r2.cloudflarestorage.com/dog.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=<credential>&X-Amz-Date=<timestamp>&X-Amz-Expires=3600&X-Amz-SignedHeaders=content-type%3Bhost&X-Amz-Signature=<signature>&x-id=PutObject
136+
```
137+
138+
You can use the link generated by the `PutObject` example to upload to the specified bucket and key, until the presigned link expires. When using a presigned URL with `ContentType`, the client must include a matching `Content-Type` header in the request.
129139

130140
```sh
131-
curl -X PUT https://my-bucket-name.<accountid>.r2.cloudflarestorage.com/dog.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential<credential>&X-Amz-Date=<timestamp>&X-Amz-Expires=3600&X-Amz-Signature=<signature>&X-Amz-SignedHeaders=host&x-id=PutObject -F "data=@dog.png"
141+
curl -X PUT "https://my-bucket.<ACCOUNT_ID>.r2.cloudflarestorage.com/dog.png?X-Amz-Algorithm=..." \
142+
-H "Content-Type: image/png" \
143+
--data-binary @dog.png
132144
```
145+
146+
## Restrict uploads with CORS and Content-Type
147+
148+
When generating presigned URLs for uploads, you can limit abuse and misuse by:
149+
150+
1. **Restricting Content-Type**: Specify the allowed content type in the `PutObjectCommand`. The upload will fail if the client sends a different `Content-Type` header.
151+
152+
2. **Configuring CORS**: Set up [CORS rules](/r2/buckets/cors/#add-cors-policies-from-the-dashboard) on your bucket to control which origins can upload files. Configure CORS via the [Cloudflare dashboard](https://dash.cloudflare.com/?to=/:account/r2/overview) by adding a JSON policy to your bucket settings:
153+
154+
```json
155+
[
156+
{
157+
"AllowedOrigins": ["https://example.com"],
158+
"AllowedMethods": ["PUT"],
159+
"AllowedHeaders": ["Content-Type"],
160+
"ExposeHeaders": ["ETag"],
161+
"MaxAgeSeconds": 3600
162+
}
163+
]
164+
```
165+
166+
Then generate a presigned URL with a Content-Type restriction:
167+
168+
```ts
169+
const putUrl = await getSignedUrl(
170+
S3,
171+
new PutObjectCommand({
172+
Bucket: "my-bucket",
173+
Key: "dog.png",
174+
ContentType: "image/png",
175+
}),
176+
{ expiresIn: 3600 },
177+
);
178+
```
179+
180+
When a client uses this presigned URL, they must:
181+
- Make the request from an allowed origin (enforced by CORS)
182+
- Include the `Content-Type: image/png` header (enforced by the signature)

0 commit comments

Comments
 (0)