Can't create R2 presigned URLs with TempAccessCredentials

For Workes & Pages, what is the name of the domain?


What is the error message?

Invalid Argument: X-Amz-Security-Token

What is the issue or error you’re encountering

I can’t presign R2 URLs using Temporary Access Credentials

What are the steps to reproduce the issue?

First of all, I invoke this API: Cloudflare API Documentation and create some temporary credentials with the higher permissions possible. Until here, all good, I can use them to list bucket objects, get objects and so on.

Now, when I try to create a presigned URL with them, and accessing it, I get an error:

<Message>Invalid Argument: X-Amz-Security-Token</Message>

If I don’t pass the X-AmzSecurity-Token argument, I just get an invalid signature error:

<Message>The request signature we calculated does not match the signature you provided. Check your secret access key and signing method. </Message>

The code to create the presigned URL is the following. Also note that it works with non-temporary access credentials, so the signature is calculated correctly.

import * as CryptoJS from 'crypto-js';
import axios from 'axios';

export function generatePresignedURL(object: string, bucket: string, region: string, accessKeyId: string, secretAccessKey: string, sessionToken: string = ''): string {
  const amzDate = new Date().toISOString().replace(/[:-]/g, '').replace(/\.\d{3}/, '');
  const dateStamp = amzDate.slice(0, 8);
  const algorithm: string = "AWS4-HMAC-SHA256";
  const credentialScope: string = `${dateStamp}/${region}/s3/aws4_request`; // %2F is /
  const expires = 3600; // 1 hour
  //const url: URL = new URL(`https://${import.meta.env.VITE_APP_R2_ACCOUNT_ID}${bucket}/${object}?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=${accessKeyId}%2F${encodeURIComponent(credentialScope)}&X-Amz-Date=${amzDate}&X-Amz-Expires=${expires}&X-Amz-SignedHeaders=host`);
  const url: URL = new URL(`https://${bucket}.${import.meta.env.VITE_APP_R2_ACCOUNT_ID}${object}?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=${accessKeyId}%2F${encodeURIComponent(credentialScope)}&X-Amz-Date=${amzDate}&X-Amz-Expires=${expires}&X-Amz-SignedHeaders=host`);
  const host = url.hostname;
  const canonicalUri = new URL(url).pathname;
  const canonicalQueryString = new URL(url).searchParams.toString();
  var signedHeaders = 'host';
  var canonicalHeaders = `host:${host}\n`;
  const canonicalRequest = `GET\n${canonicalUri}\n${canonicalQueryString}\n${canonicalHeaders}\n${signedHeaders}\nUNSIGNED-PAYLOAD`
  const stringToSign = `AWS4-HMAC-SHA256\n${amzDate}\n${credentialScope}\n${CryptoJS.SHA256(canonicalRequest).toString()}`;
  const signingKey = getSignatureKey(secretAccessKey, dateStamp, region, 's3');
  const signature = CryptoJS.HmacSHA256(stringToSign, signingKey).toString();
  if (sessionToken !== '') {
    url.searchParams.append('X-Amz-Security-Token', encodeURIComponent(sessionToken));
  const presignedUrl = url.toString() + `&X-Amz-Signature=${signature}`;

  return presignedUrl;

Note that I’m following the reference Authenticating Requests: Using Query Parameters (AWS Signature Version 4) - Amazon Simple Storage Service strictly. And in the last part it says that if using temporary credentials, we should add the X-Amz-Security-Token header, but R2 seems to refuse it.

Also note, we just need to GET the objects. No need for more difficult operations like PUT and so on.

Anyways, this is an important use case for our application and without it we’ll probably have to move to AWS S3 (which gives no errors with this). Any input will be really appreciated, but I’ve spent a couple of days with this issue and still didn’t find a solution.


Also tried following the example here: aws-sdk-js-v3 · Cloudflare R2 docs with no luck again.

With the temporary access credentials I get a signature error, and with the non-temporary I just get:

<Message>The specified bucket does not exist.</Message>

Seems like the presigned URL API is completely broken in R2?