Skip to content
Merged
342 changes: 167 additions & 175 deletions src/content/docs/r2/api/s3/presigned-urls.mdx

Large diffs are not rendered by default.

91 changes: 56 additions & 35 deletions src/content/docs/r2/buckets/cors.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,44 +33,34 @@ Next, [add a CORS policy](#add-cors-policies-from-the-dashboard) to your bucket

## Use CORS with a presigned URL

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.

### Create a presigned URL

You will need a pair of S3-compatible credentials to use when you generate the presigned URL.

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.

```js
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const S3 = new S3Client({
endpoint: "https://<account_id>.r2.cloudflarestorage.com",
credentials: {
accessKeyId: "<access_key_id>",
secretAccessKey: "<access_key_secret>",
},
region: "auto",
});
const url = await getSignedUrl(
S3,
new PutObjectCommand({
Bucket: bucket,
Key: object,
}),
{
expiresIn: 60 * 60 * 24 * 7, // 7d
},
);
console.log(url);
```
[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.

### Test the presigned URL
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.

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`.
To enable browser-based access with presigned URLs:

```sh
curl --request PUT <URL> --header "Content-Type: text/plain" --data "123"
1. [Add a CORS policy](#add-cors-policies-from-the-dashboard) to your bucket that allows requests from your application's origin.

2. Set `AllowedMethods` to match the operations your presigned URLs perform, use `GET`, `PUT`, `HEAD`, and/or `DELETE`.

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.

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.

5. (Optional) Set `MaxAgeSeconds` to cache the preflight response and reduce the number of preflight requests the browser makes.

The following example allows browser-based uploads from `https://example.com` with a `Content-Type` header:

```json
[
{
"AllowedOrigins": ["https://example.com"],
"AllowedMethods": ["PUT"],
"AllowedHeaders": ["Content-Type"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3600
}
]
```

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

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

## Add CORS policies via Wrangler CLI

You can configure CORS rules using the [Wrangler CLI](/r2/reference/wrangler-commands/).

1. Create a JSON file with your CORS configuration:

```json title="cors.json"
{
"rules": [
{
"allowed": {
"origins": ["https://example.com"],
"methods": ["GET"]
}
}
]
}
```

2. Apply the CORS policy to your bucket:

```sh
npx wrangler r2 bucket cors set <BUCKET_NAME> --file cors.json
```

3. Verify the CORS policy was applied:

```sh
npx wrangler r2 bucket cors list <BUCKET_NAME>
```

## Response headers

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.
Expand Down
19 changes: 10 additions & 9 deletions src/content/docs/r2/examples/aws/aws-cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,23 @@ aws configure
```

```sh output
AWS Access Key ID [None]: <access_key_id>
AWS Secret Access Key [None]: <access_key_secret>
AWS Access Key ID [None]: <ACCESS_KEY_ID>
AWS Secret Access Key [None]: <SECRET_ACCESS_KEY>
Default region name [None]: auto
Default output format [None]: json
```

The `region` value can be set to `auto` since it is required by the SDK but not used by R2.

You may then use the `aws` CLI for any of your normal workflows.

```sh
aws s3api list-buckets --endpoint-url https://<accountid>.r2.cloudflarestorage.com
# Provide your Cloudflare account ID
aws s3api list-buckets --endpoint-url https://<ACCOUNT_ID>.r2.cloudflarestorage.com
# {
# "Buckets": [
# {
# "Name": "sdk-example",
# "Name": "my-bucket",
# "CreationDate": "2022-05-18T17:19:59.645000+00:00"
# }
# ],
Expand All @@ -38,7 +41,7 @@ aws s3api list-buckets --endpoint-url https://<accountid>.r2.cloudflarestorage.c
# }
# }

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

```sh
# You can pass the --expires-in flag to determine how long the presigned link is valid.
$ aws s3 presign --endpoint-url https://<accountid>.r2.cloudflarestorage.com s3://sdk-example/ferriswasm.png --expires-in 3600
# 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>
aws s3 presign --endpoint-url https://<accountid>.r2.cloudflarestorage.com s3://sdk-example/ferriswasm.png --expires-in 3600
# 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>
aws s3 presign --endpoint-url https://<ACCOUNT_ID>.r2.cloudflarestorage.com s3://my-bucket/ferriswasm.png --expires-in 3600
# 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>
```
11 changes: 7 additions & 4 deletions src/content/docs/r2/examples/aws/aws-sdk-go.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,16 @@ import (

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

cfg, err := config.LoadDefaultConfig(context.TODO(),
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(accessKeyId, accessKeySecret, "")),
config.WithRegion("auto"),
config.WithRegion("auto"), // Required by SDK but not used by R2
)
if err != nil {
log.Fatal(err)
Expand Down
13 changes: 8 additions & 5 deletions src/content/docs/r2/examples/aws/aws-sdk-java.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ public class CloudflareR2Client {

/**
* Configuration class for R2 credentials and endpoint
* - accountId: Your Cloudflare account ID
* - accessKey: Your R2 Access Key ID (see: https://developers.cloudflare.com/r2/api/tokens)
* - secretKey: Your R2 Secret Access Key (see: https://developers.cloudflare.com/r2/api/tokens)
*/
public static class S3Config {
private final String accountId;
Expand Down Expand Up @@ -70,7 +73,7 @@ public class CloudflareR2Client {
return S3Client.builder()
.endpointOverride(URI.create(config.getEndpoint()))
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.region(Region.of("auto"))
.region(Region.of("auto")) // Required by SDK but not used by R2
.serviceConfiguration(serviceConfiguration)
.build();
}
Expand Down Expand Up @@ -103,9 +106,9 @@ public class CloudflareR2Client {

public static void main(String[] args) {
S3Config config = new S3Config(
"your_account_id",
"your_access_key",
"your_secret_key"
"<ACCOUNT_ID>",
"<ACCESS_KEY_ID>",
"<SECRET_ACCESS_KEY>"
);

CloudflareR2Client r2Client = new CloudflareR2Client(config);
Expand Down Expand Up @@ -165,7 +168,7 @@ public class CloudflareR2Client {
return S3Presigner.builder()
.endpointOverride(URI.create(config.getEndpoint()))
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.region(Region.of("auto"))
.region(Region.of("auto")) // Required by SDK but not used by R2
.serviceConfiguration(S3Configuration.builder()
.pathStyleAccessEnabled(true)
.build())
Expand Down
72 changes: 61 additions & 11 deletions src/content/docs/r2/examples/aws/aws-sdk-js-v3.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ import {
} from "@aws-sdk/client-s3";

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

console.log(
await S3.send(new ListObjectsV2Command({ Bucket: "my-bucket-name" })),
await S3.send(new ListObjectsV2Command({ Bucket: "my-bucket" })),
);
// {
// '$metadata': {
Expand Down Expand Up @@ -91,7 +93,7 @@ console.log(
// IsTruncated: false,
// KeyCount: 8,
// MaxKeys: 1000,
// Name: 'my-bucket-name',
// Name: 'my-bucket',
// NextContinuationToken: undefined,
// Prefix: undefined,
// StartAfter: undefined
Expand All @@ -109,24 +111,72 @@ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
console.log(
await getSignedUrl(
S3,
new GetObjectCommand({ Bucket: "my-bucket-name", Key: "dog.png" }),
new GetObjectCommand({ Bucket: "my-bucket", Key: "dog.png" }),
{ expiresIn: 3600 },
),
);
// 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

// You can also create links for operations such as putObject to allow temporary write access to a specific key.
// You can also create links for operations such as PutObject to allow temporary write access to a specific key.
// Specify ContentType to restrict uploads to a specific file type.
console.log(
await getSignedUrl(
S3,
new PutObjectCommand({ Bucket: "my-bucket-name", Key: "dog.png" }),
new PutObjectCommand({
Bucket: "my-bucket",
Key: "dog.png",
ContentType: "image/png",
}),
{ expiresIn: 3600 },
),
);
```

You can use the link generated by the `putObject` example to upload to the specified bucket and key, until the presigned link expires.
```sh output
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
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
```

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.

```sh
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"
curl -X PUT "https://my-bucket.<ACCOUNT_ID>.r2.cloudflarestorage.com/dog.png?X-Amz-Algorithm=..." \
-H "Content-Type: image/png" \
--data-binary @dog.png
```

## Restrict uploads with CORS and Content-Type

When generating presigned URLs for uploads, you can limit abuse and misuse by:

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.

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:

```json
[
{
"AllowedOrigins": ["https://example.com"],
"AllowedMethods": ["PUT"],
"AllowedHeaders": ["Content-Type"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3600
}
]
```

Then generate a presigned URL with a Content-Type restriction:

```ts
const putUrl = await getSignedUrl(
S3,
new PutObjectCommand({
Bucket: "my-bucket",
Key: "dog.png",
ContentType: "image/png",
}),
{ expiresIn: 3600 },
);
```

When a client uses this presigned URL, they must:
- Make the request from an allowed origin (enforced by CORS)
- Include the `Content-Type: image/png` header (enforced by the signature)
Loading
Loading