Stefano Sambruna
Stefano Sambruna

Reputation: 797

JWPlayer and S3: calculated signature does not match the provided one

I'm trying to upload videos to JWPlayer using AWS S3.

My backend is coded in Spring so I'm using Java. I manage to request the upload URL but every time I try to submit a POST request with it I get back a "SignatureDoesNotMatch" error from AWS.

Without s3 it works just fine... The documentation is not that clear in this sense... There isn't a well done example from the beginning to the end, I mean, an example with the whole process: authentication, url request and upload of the video. So I'm struggling trying to understand what's the problem here.

With this method I get the update url.

public String createVideo(String title, String description, String username) throws UnsupportedEncodingException {
        Map<String, String> params = new HashMap<>();
        String nonce = generateNonce(8);
        String timestamp = Long.toString(System.currentTimeMillis() / 1000L);
        params.put("api_format", "json");
        params.put("author", username);
        params.put("title", title);
        params.put("description", description);
        params.put("upload_method", "s3");
        params.put("api_key", jwPlayerConfig.getKey());
        params.put("api_nonce", nonce);
        params.put("api_timestamp", timestamp);
        params.put("api_signature", generateAPISignature(params, jwPlayerConfig.getSecret()));
        String urlParameters = getParamsString(params);
        String response = requestAuthenticationToken(jwPlayerConfig.getUrl() + jwPlayerConfig.getCreateVideoUrl(), urlParameters);
        JSONObject myObject = new JSONObject(response.toString());
        System.out.println(myObject);
        JSONObject link = myObject.getJSONObject("link");
        return "https://" + link.getString("address") + link.getString("path") +
                "?api_format=json" +
                "&redirect_address=" + jwPlayerConfig.getCreateVideoRedirectAddress() +
                "&key_in_path=True" +
                "&AWSAccessKeyId=" + link.getJSONObject("query").getString("AWSAccessKeyId") +
                "&Expires=" + link.getJSONObject("query").get("Expires").toString() +
                "&Signature=" + link.getJSONObject("query").getString("Signature");
    }

This method is called by a controller method of Spring and it uses these other methods in order to generate the upload url:

public String generateAPISignature(Map<String, String> params, String api_secret){
        final Map<String, String> sorted = params.entrySet()
                .stream()
                .sorted(Comparator.comparing(Map.Entry::getKey))
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new));
        StringBuilder concat = new StringBuilder();
        for(String key: sorted.keySet()){
            concat.append(key);
            concat.append("=");
            concat.append(sorted.get(key));
            concat.append("&");
        }
        concat.delete(concat.length() - 1, concat.length());
        concat.append(api_secret);
        return DigestUtils.sha1Hex(concat.toString());
    }

This method generates a nonce:

public static String generateNonce(int length){
        Random rnd = new Random();
        StringBuilder nonce = new StringBuilder();
        do{
            nonce.append(rnd.nextInt(10));
        }while(nonce.length() < length);
        return nonce.toString();
    }

And this other one builds the parameters string from the parameters Map:

public String getParamsString(Map<String, String> params) throws UnsupportedEncodingException {
        StringBuilder result = new StringBuilder();

        for (Map.Entry<String, String> entry : params.entrySet()) {
            result.append(URLEncoder.encode(entry.getKey(), "UTF-8"));
            result.append("=");
            result.append(URLEncoder.encode(entry.getValue(), "UTF-8"));
            result.append("&");
        }

        String resultString = result.toString();

        return resultString.length() > 0
                ? resultString.substring(0, resultString.length() - 1)
                : resultString;
    }

The html form looks like this:

<form method="POST" action="url">
    <input type="file" name="file" id="file"/>
    <button type="submit">Upload video</button>
</form>

The error message I get is the following:

SignatureDoesNotMatch The request signature we calculated does not match the signature you provided. Check your key and signing method. AKKSAJBCJBSbXC3NQ POST application/x-www-form-urlencoded 1567098922 /jwplatform-upload/Qsafas6vB WWbjcskWBKlc/BLxbm6/RJg57u7M= 50 4f 53 54 0a 0a 61 70 70 6c 69 63 61 74 69 6f 6e 2f 31 2d 77 77 77 2d 66 6f 72 6d 2d 75 72 6c 65 6e 64 6f 64 65 64 0a 31 35 36 37 30 39 38 39 32 32 0a 2f 6a 77 70 6c 23 74 66 6f 72 6d 2d 75 70 6c 6f 61 64 2f 51 72 4e 53 33 36 76 42 8D9AA0A3719CE53F 3x6mnassasaQ2PEFVmc9GZwp0Y7yFS1FtasakDgY39EktjlwX2UsoViikqiE8bDcG6pKB4YPXvsH1Q=

Upvotes: 3

Views: 572

Answers (2)

Amo Wu
Amo Wu

Reputation: 2517

If you want to upload files in the browser, you should use HTTP PUT instead of POST, and the Content-Type of the request headers MUST be a MIME type instead of multipart/form-data, and the parameters of the API the MIME type MUST also be given (both v1 and v2), Finally, you CANNOT use FormData to make requests. Here is my complete sample code:

import { useState } from "react";
import axios from "axios";
import { Button, Upload } from "antd";
import JWPlatformAPI from "jwplatform";

const V1_API_CREDENTIALS_KEY = "<V1_API_Credentials_Key>";
const V1_API_CREDENTIALS_SECRET = "<V1_API_Credentials_Secret>";
const V2_API_CREDENTIALS_SECRET = "<V2_API_Credentials_Secret>";
const SITE_ID = "<SITE_ID>";

const jwApi = new JWPlatformAPI({
  apiKey: V1_API_CREDENTIALS_KEY,
  apiSecret: V1_API_CREDENTIALS_SECRET
});

export default function App() {
  const [uploadUrl, setUploadUrl] = useState(null);

  const createUploadUrlV1 = () => {
    jwApi.videos
      .create({
        upload_method: "s3",
        upload_content_type: "video/mp4"
      })
      .then((response) => {
        const {
          link: {
            protocol,
            address,
            path,
            query: { AWSAccessKeyId, Expires, Signature }
          }
        } = response;
        const encodedSignature = encodeURIComponent(Signature);
        setUploadUrl(
          `${protocol}://${address}${path}?AWSAccessKeyId=${AWSAccessKeyId}&Expires=${Expires}&Signature=${encodedSignature}&api_format=json`
        );
      });
  };
  const createUploadUrlV2 = () => {
    axios({
      method: "post",
      url: `https://api.jwplayer.com/v2/sites/${SITE_ID}/media/`,
      headers: {
        Authorization: V2_API_CREDENTIALS_SECRET
      },
      data: {
        upload: {
          method: "direct",
          mime_type: "video/mp4"
        }
      }
    }).then(({ data }) => {
      setUploadUrl(data.upload_link);
    });
  };

  return (
    <div>
      <button onClick={createUploadUrlV1}>
        v1 create S3 direct upload URL
      </button>
      <button onClick={createUploadUrlV2}>
        v2 create S3 direct upload URL
      </button>
      <div>S3 direct upload URL: {uploadUrl}</div>
      <Upload
        action={uploadUrl}
        headers={{
          "Content-Type": "video/mp4"
        }}
        method="PUT"
        name="file"
        customRequest={({
          action,
          file,
          headers,
          onError,
          onProgress,
          onSuccess
        }) => {
          axios
            .put(action, file, {
              headers,
              onUploadProgress: ({ total, loaded }) => {
                onProgress(
                  { percent: Math.round((loaded / total) * 100).toFixed(2) },
                  file
                );
              }
            })
            .then((event) => {
              const { data } = event;
              onSuccess(data, file);
            })
            .catch(onError);
          return {
            abort() {
              console.log("upload progress is aborted.");
            }
          };
        }}
      >
        <Button>Click to Upload</Button>
      </Upload>
    </div>
  );
}

I suggest that JWP should update the documentation as soon as possible.

Upvotes: 1

Todd
Todd

Reputation: 249

Try with a PUT, not a POST. We have documentation for this at https://developer.jwplayer.com/jw-platform/reference/v1/s3_uploads.html#uploading-file

Upvotes: 3

Related Questions