Can I upload to Spaces using a signed url?

March 29, 2019 1.1k views
JavaScript Node.js Ubuntu 18.04

Hello,

Im currently developing a React webapp that lets users upload images. I have it working with AWS S3 but would like to have everything on DO for simplicities sake.

The flow goes like this:

  • Send image info to server and receive a signed url
  • Post the image to the signed url

When i try this with spaces im constantly getting a "SignatureDoesNotMatch" error.

Please help, im going batty!

Here's my node server code:

spacesEndpoint = new aws.Endpoint(`${DO_REGION}.digitaloceanspaces.com`),
s3 = new aws.S3({
    endpoint: spacesEndpoint,
    accessKeyId: DO_ACCESS_KEY_ID,
    secretAccessKey: DO_SECRET_ACCESS_KEY,
    region: DO_REGION,
    signatureVersion: 'v4',
});

const s3Params = {
    Bucket: DO_SPACE,
    Expires: 60,
    Key: filePath,
    ContentType: fileType, // "image/jpeg"
    ACL: 'public-read',
};

const promise = new Promise((resolve, reject) => {
    s3.getSignedUrl('putObject', s3Params, (err, url) => {
        if (err) {
            reject(err);
        }
        resolve(url);
    });
});

and this is my javascript

const xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.open('PUT', payload.signedUrl);
xhr.setRequestHeader('Host', `${DO_SPACE}.${DO_REGION}.digitaloceanspaces.com');
xhr.setRequestHeader('x-amz-acl', 'public-read');
xhr.setRequestHeader('Content-Type', payload.file.type);
xhr.setRequestHeader('Content-Length', payload.file.size);
xhr.send(payload.file);
5 Answers

Hi from Spaces Engineering team!

When using presigned URLs to upload, getting SignatureDoesNotMatch usually means that the headers that were used in signing don't match the actual headers sent by the real request.

It would help tremendously if you could post one of the signed URLs, as well as the entire HTTP headers for both the request & response sent by the client using the presigned URL.

To avoid security issues, you should redact the following from them before posting:

  • AccessKey
  • Signature
  • BucketName (optional but recommended, because it might let somebody else to your user here to a specific bucket)
  • Filename (optional if sensitive, but useful in debugging)
  • Any and all help is greatly appreciated!

    Signed URL:
    https://my-space-name-here.sfo2.digitaloceanspaces.com/development/1/85ae1890526311e981f78b61e65b9a67/14390772_10157420640840534_5475048453553787003_n.jpg?Content-Type=image%2Fjpeg&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=MY_API_KEY_HERE%2F20190329%2Fsfo2%2Fs3%2Faws4_request&X-Amz-Date=20190329T204500Z&X-Amz-Expires=60&X-Amz-Signature=SIGNATURE_WAS_HERE&X-Amz-SignedHeaders=host%3Bx-amz-acl&x-amz-acl=public-read

    Request:
    OPTIONS /development/1/85ae1890526311e981f78b61e65b9a67/14390772101574206408405345475048453553787003n.jpg?Content-Type=image%2Fjpeg& X-Amz-Algorithm=AWS4-HMAC-SHA256& X-Amz-Credential=MYAPIKEYHERE%2F20190329%2Fsfo2%2Fs3%2Faws4request& X-Amz-Date=20190329T204500Z& X-Amz-Expires=60& X-Amz-Signature=SIGNATUREWASHERE& X-Amz-SignedHeaders=host%3Bx-amz-acl& x-amz-acl=public-read HTTP/1.1
    Host: my-space-name-here.sfo2.digitaloceanspaces.com
    Access-Control-Request-Method: PUT
    Origin: http://0.0.0.0:3001
    Referer: http://0.0.0.0:3001/pages/about
    User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10
    14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36
    Access-Control-Request-Headers: content-type
    cache-control: no-cache
    Postman-Token: c33844c8-5ca4-4e1e-ae3a-2c023f059b73

    Response:
    <?xml version="1.0" encoding="UTF-8"?>
    <Error>
    <Code>AccessDenied</Code>
    <BucketName>my-space-name-here</BucketName>
    <RequestId>tx0000000000000785bbcfd-005c9e8542-795ea-sfo2a</RequestId>
    <HostId>795ea-sfo2a-sfo</HostId>
    </Error>

    Content-Length →225
    x-amz-request-id →tx0000000000000785bbcfd-005c9e8542-795ea-sfo2a
    Accept-Ranges →bytes
    Content-Type →application/xml
    Date →Fri, 29 Mar 2019 20:51:14 GMT
    Strict-Transport-Security →max-age=15552000; includeSubDomains; preload

    const uuidv1 = require('uuid/v1'),
        aws = require('aws-sdk'),
        spacesEndpoint = new aws.Endpoint(`${DO_REGION}.digitaloceanspaces.com`),
        s3 = new aws.S3({
            endpoint: spacesEndpoint,
            accessKeyId: DO_ACCESS_KEY_ID,
            secretAccessKey: DO_SECRET_ACCESS_KEY,
            region: DO_REGION,
            signatureVersion: 'v4',
        });
    
    // file key, prevents collisions
    let fileKey = uuidv1();
    fileKey = fileKey.split('-').join('');
    
    // create files 'path' in s3
    let filePath = getFilePath(userID, { fileKey, fileName });
    
    const s3Params = {
        Bucket: DO_SPACE,
        Key: filePath,
        ContentType: fileType,
        ACL: 'public-read',
        Expires: 60,
    };
    
    const promise = new Promise((resolve, reject) => {
        s3.getSignedUrl('putObject', s3Params, (err, url) => {
            if (err) {
                reject(err);
            }
    
            resolve(url);
        });
    });
    
    const xhr = new XMLHttpRequest();
    xhr.withCredentials = true;
    xhr.open('PUT', payload.signedUrl);
    xhr.setRequestHeader('Content-Type', payload.file.type);
    xhr.send(payload.file);
    
  • Only an OPTIONS request and no PUT request?
  • Did you have CORS set up on the bucket?

Looks like your client is enforcing CORS, so you'll need to do that first: https://www.digitalocean.com/docs/spaces/how-to/cors/
Since your Origin is Origin: http://0.0.0.0:3001, I recommend setting a CORS wildcard to start, then changing it to match your production deployment later.

P.S. The bucket name appears in multiple places, I see it leaked in your response.

  • The OPTIONS request is first fired off but fails so he PUT request never gets to complete. Here are my CORS configs, there isn't an options header setting

    Origin: *
    Allowed Methods: GET, PUT, DELETE, POST, HEAD
    Allowed Headers:

    • Access-Control-Allow-Origin
    • Access-Control-Request-Headers
    • Access-Control-Request-Method
    • Origin
    • Referer
    • User-Agent Access Control Max Age: 60

OPTIONS is the HTTP method used for the CORS preflight request, to ask which other methods are permitted, based on the Access-Control-*, Origin headers that are sent, which are checked against the stores CORSConfiguration for a matching CORSRule.

A good group of settings for a single CORSRule during development:

  • Allowed Origin: *
  • AllowedMethod: (all of them)
  • AllowedHeader: * (important in dev, extra headers in SDKs can cause CORS preflight to fail)
  • MaxAgeSeconds: 600

AllowedHeader: * will help you the most I think, as I see Postman-Token in your request, you didn't include that in the allowed headers you showed.

You said you got a SignatureDoesNotMatch in the original post, could you please reproduce that error and share the request/response headers&body for that instance?

@admin94a05aa0b9579d1a73a36 did this help? wanted to check if you made any progress.

Hello

So i was able to get it workign with the CORS configuration you recommended, and the following server and client code.

Thank you very much for your help.

const aws = require('aws-sdk');
spacesEndpoint = new aws.Endpoint(`${DO_REGION}.digitaloceanspaces.com`),
s3 = new aws.S3({
    endpoint: spacesEndpoint,
    accessKeyId: DO_ACCESS_KEY_ID,
    secretAccessKey: DO_SECRET_ACCESS_KEY,
    region: DO_REGION,
    signatureVersion: 'v4',
});

const s3Params = {
    Bucket: DO_SPACE,
    Key: filePath,
    ContentType: fileType,
    ACL: 'public-read',
    Expires: 60,
};

s3.getSignedUrl('putObject', s3Params, (err, url) => {
    if (err) {
        reject(err);
    }

    resolve(url);
});
const xhr = new XMLHttpRequest();
xhr.open('PUT', signedUrl);
xhr.setRequestHeader('Content-Type', file.type);
xhr.setRequestHeader('x-amz-acl', 'public-read');
xhr.send(file);
Have another answer? Share your knowledge.