Signature Authentication

As briefly mentioned in our concepts, Signature Authentication is a security measure that can prevent outsiders from tampering with your Assembly Instructions. It provides trust in untrusted environments.

Warning: We strongly recommend enabling Signature Authentication when interfacing with our API, particularly in untrusted environments where users may be able to access your Auth Key.

Given that there are only two parties in the world that have your account's Auth Secret (Transloadit and you), we can leverage that to generate a cryptographic signature on both our ends for the data that we exchange. We compare signatures after receiving a message, and know this exact message could have only come from someone that has the secret.

Since the signature is calculated via one-way encryption, the signature itself is not a secret, and someone seeing it could not derive the Auth Secret used to generate it. That's great because signatures are generated by servers that can keep a secret safe, and then injected into web browsers that don't share that quality.

For creating Assemblies with Transloadit, your back-end could calculate a Signature that only covers certain parameters, authenticated users, and a timeframe that it deems legitimate usage. For instance, it would refuse to generate a signature for users that are not logged in. You could use any business logic on the server-side here to decide if you hand out a signature, or not, and Transloadit can be configured to reject any request for your Account that is not accompanied by a correct signature for the payload.

If you want to make Signature Authentication mandatory for all requests concerning your account:

  1. Go to the Workspace Settings in your account.
  2. In the API Settings section, enable the Require a correct Signature option.
  3. Hit the Save button.

Note: Most back-end SDKs automatically use Signature Authentication when you supply your Auth Secret. So perhaps, just this introduction is all you need to know. If you are integrating Transloadit into untrusted environments, however, such as browsers (Uppy!), you'll want to continue reading to see how your back-end can supply signatures to it.

How to generate Signatures

So, how does this all look?

The typical params field when creating an Assembly without Signature Authentication is as follows:

{
  "auth": {
    "key": "23c96d084c744219a2ce156772ec3211"
  },
  "steps": { ... }
}

The auth.key in this example is the Auth Key from API Credentials in your account.

To sign this request, the additional auth.expires field needs to be added. This adds it to our payload, which is protected by our signature. If someone would change it, Transloadit would reject the request as the signature no longer matches. You signed a different payload than the one we received. If the signature does match, then we will naturally compare and reject by date as instructed. This way, requests become very hard to indefinitely repeat by a third party that got a hold of this payload. Because even though our A+ grade HTTPS should already go a long way in preventing that, browser cache could be easier to snoop on.

The expires property must contain a timestamp in the (near) future. Use YYYY/MM/DD HH:mm:ss+00:00 as the date format, making sure that UTC is used for the timezone. For example:

{
  "auth": {
    "key": "23c96d084c744219a2ce156772ec3211",
    "expires": "2024/01/31 16:53:14+00:00"
  },
  "steps": { ... }
}

To calculate the signature for this request:

  1. From your front-end, stringify the above JavaScript object into JSON and send it to your back-end.
  2. From your back-end, calculate an RFC 6234-compliant HMAC hex signature on the string, with your Auth Secret as the key, and SHA384 as the hash algorithm. Prefix the signature string with the algorithm name in lowercase. For example, for SHA384, use sha384:<HMAC-signature>. You can send that string to your front-end (as long as you made the appropriate checks to guarantee it was a genuine request from your front-end).
  3. From your front-end, add a signature multipart POST field containing this value to your request (e.g., with a hidden field in an HTML form).

Note: If your implementation uses a template_id instead of steps, there's no need to generate a signature for the Instructions that your Template contains. We should only sign communication payloads.

Important: To generate a signature for Smart CDN URLs please use the auth key designated for Smart CDN usage from your Credentials page when logged in. For those signatures you must use the sha256 algorithm. This is due to restrictions from Amazon Cloudfront which we use as a CDN. For non-Smart CDN signatures please use sha384.

Signature Authentication is offered on requests that you send to Transloadit, but also the other way around. For instance, in Async Mode you will want to ensure that any Assembly Notifications that Transloadit sends to your back-end are actually coming from us. The example Node.js code in the Assembly Notifications docs illustrates this flow, and we have example code for generating signatures in different languages right below.

Example code for different languages

When you deal with JSON, please keep in mind that your language of choice might escape some characters (i.e. it might turn occurrences of / into \/, or é into "\\u00e9"). We calculate the signatures on our end with unescaped strings! Please make sure to remove backslashes from your JSON before calculating its signature.

If you use PHP for example, please check the JSON_UNESCAPED_SLASHES option of the json_encode function.

const crypto = require('node:crypto')

const utcDateString = (ms) => {
  return new Date(ms)
    .toISOString()
    .replace(/-/g, '/')
    .replace(/T/, ' ')
    .replace(/\.\d+Z$/, '+00:00')
}

// expire 1 hour from now (this must be milliseconds)
const expires = utcDateString(Date.now() + 1 * 60 * 60 * 1000)
const authKey = 'YOUR_TRANSLOADIT_KEY'
const authSecret = 'YOUR_TRANSLOADIT_SECRET'

const params = JSON.stringify({
  auth: {
    key: authKey,
    expires,
  },
  template_id: 'YOUR_TRANSLOADIT_TEMPLATE_ID',
  // your other params like template_id, notify_url, etc.
})
const signatureBytes = crypto.createHmac('sha384', authSecret).update(Buffer.from(params, 'utf-8'))
// The final signature needs the hash name in front, so
// the hashing algorithm can be updated in a backwards-compatible
// way when old algorithms become insecure.
const signature = `sha384:${signatureBytes.digest('hex')}`

console.log(`${expires} ${signature}`)

<?php
// expire 1 hours from now
$expires = gmdate('Y/m/d H:i:s+00:00', strtotime('+1 hour'));
$authKey = 'YOUR_TRANSLOADIT_KEY';
$authSecret = 'YOUR_TRANSLOADIT_SECRET';

$params = json_encode(
  [
    'auth' => [
      'key' => $authKey,
      'expires' => $expires,
    ],
    'template_id' => 'YOUR_TRANSLOADIT_TEMPLATE_ID',
  ],
  JSON_UNESCAPED_SLASHES
);
$signature = hash_hmac('sha384', $params, $authSecret);

echo $expires . ' sha384:' . $signature . PHP_EOL;

require 'rubygems'
require 'openssl'
require 'json'

# expire one hour from now
expires     = (Time.now.utc + 1 * 60 * 60).strftime('%Y/%m/%d %H:%M:%S+00:00')
auth_key    = 'YOUR_TRANSLOADIT_KEY'
auth_secret = 'YOUR_TRANSLOADIT_SECRET'

params = JSON.generate({
  :auth => {
    :key     => auth_key,
    :expires => expires,
  },
  :template_id => 'YOUR_TRANSLOADIT_TEMPLATE_ID',
})
signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha384'), auth_secret, params)

puts(expires + " sha384:" + signature)

import hmac
import hashlib
import json
from datetime import datetime, timedelta

expires = (timedelta(seconds=60 * 60) + datetime.utcnow()).strftime("%Y/%m/%d %H:%M:%S+00:00")
auth_key = 'YOUR_TRANSLOADIT_KEY'
auth_secret = 'YOUR_TRANSLOADIT_SECRET'
params = {
    'auth': {
        'key': auth_key,
        'expires': expires,
    },
    'template_id': 'YOUR_TRANSLOADIT_TEMPLATE_ID'
    # your other params like template_id, notify_url, etc.
}

message = json.dumps(params, separators=(',', ':'), ensure_ascii=False)
signature = hmac.new(auth_secret.encode('utf-8'),
                    message.encode('utf-8'),
                    hashlib.sha384).hexdigest()

print(expires, "sha384:" + signature)

package com.transloadit.sdk;

import java.nio.charset.Charset;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Hex;
import org.joda.time.Instant;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

public class JavaSignature {

  public static void main(String[] args) {
    String authKey = "YOUR_TRANSLOADIT_KEY";
    String authSecret = "YOUR_TRANSLOADIT_SECRET";
    String templateId = "YOUR_TRANSLOADIT_TEMPLATE_ID";
    DateTimeFormatter formatter = DateTimeFormat.forPattern("Y/MM/dd HH:mm:ss+00:00").withZoneUTC();
    String expiry = formatter.print(Instant.now().plus(60 * 60 * 1000));

    String messageFormat = "{\"auth\":{\"key\":\"%s\",\"expires\":\"%s\"},\"template_id\":\"%s\"}";
    String message = String.format(messageFormat, authKey, expiry, templateId);

    byte[] kSecret = authSecret.getBytes(Charset.forName("UTF-8"));
    byte[] rawHmac = HmacSHA384(kSecret, message);
    byte[] hexBytes = new Hex().encode(rawHmac);

    System.out.println(expiry + " sha384:" + new String(hexBytes, Charset.forName("UTF-8")));
  }

  private static byte[] HmacSHA384(byte[] key, String data) {
    final String ALGORITHM = "HmacSHA384";
    Mac mac;

    try {
      mac = Mac.getInstance(ALGORITHM);
      mac.init(new SecretKeySpec(key, ALGORITHM));
      return mac.doFinal(data.getBytes(Charset.forName("UTF-8")));
    } catch (NoSuchAlgorithmException e) {
      throw new RuntimeException(e);
    } catch (InvalidKeyException e) {
      throw new RuntimeException(e);
    }
  }
}

Smart CDN

To sign a Smart CDN URL, a similar process as for regular API signatures is used. A HMAC digest is calculated on a string, that is derived from the Smart CDN URL, with the Auth Secret as the key. For the signature to be valid, the used Auth Key must be enabled for Smart CDN use.

The generation of a Smart CDN signature must be performed on the back-end. The process uses the Auth Secret, which is confidential and must not be exposed to your users on the front-end.

A typical Smart CDN URL has the following structure:

https://[your-workspace].tlcdn.com/[template-name]/[file-path]?[parameters]
  • [your-workspace] is your Transloadit Workspace name
  • [template-name] is the name of your Template
  • [file-path] is the path to the file you want to transform
  • [parameters] are desired transformation parameters (e.g. h=100)

A signature for this URL is generated by following these steps:

  1. Add the exp query parameter for defining a time in the future after which the signature is not accepted by the Smart CDN anymore. This is useful to limit temporal access to a file. The expiration moment is represented by the number of milliseconds since the UNIX epoch (the midnight at the beginning of January 1, 1970, UTC). While this parameter is optional, we highly recommend always setting an expiration time. For example, a signature using exp=1722517200000 is valid until Thu, 01 Aug 2024 13:00:00 GMT.
  2. Add the auth_key query parameter to define the Auth Key corresponding to the Auth Secret that is used to create the signature. If this parameter is not set, Transloadit's API assumes that the oldest, Smart-CDN-enabled Auth Key pair has been used for the signature. Setting the auth_key parameters allows you to rotate your Auth Key without interrupting your users, and we thus highly recommend setting it. For example: auth_key=23c96d084c744219a2ce156772ec3211
  3. Sort the query parameters according to unicode code points of the keys in descending order. The sorting should be stable, i.e. if a key appears multiple times in the query string, the corresponding values should retain their relative ordering. For example, h=100&f=png&f=jpg&auth_key=hello&exp=123 is sorted into auth_key=hello&exp=123&h=100&f=png&f=jpg.
  4. Construct the string to sign by concatenating the values:
    [your-workspace]/[template-name]/[file-path]?[sorted-parameters]
    
    The values for [your-workspace], [template-name], and [file-path] must be URL-encoded to ensure they only contain URL-safe characters. Note that the string does not start with a leading slash. The ? character must be omitted if [sorted-parameters] is empty.
  5. Calculate an RFC 6234-compliant HMAC hex signature on the string to sign, with your Auth Secret as the key, and SHA256 as the hash algorithm. Prefix the hex signature with the algorithm name in lowercase and a colon, i.e. sha256. For example, for SHA256, use sha256:[hmac-signature].
  6. Append the prefixed hex signature under the sig query parameter to the URL, giving the signed Smart CDN URL:
    https://[your-workspace].tlcdn.com/[template-name]/[file-path]?[parameters]&sig=sha256:[hmac-signature]
    
    The values for [your-workspace], [template-name], and [file-path] must be URL-encoded to ensure they only contain URL-safe characters. This signed URL can then be sent to or used in your front-end until the expiration date is reached.

Example code

const crypto = require('node:crypto')

export function signedSmartCDNUrl(workspaceSlug, templateSlug, inputField, params = {}) {
  const AUTH_KEY = 'YOUR_TRANSLOADIT_KEY'
  const AUTH_SECRET = 'YOUR_TRANSLOADIT_SECRET'

  const encodedWorkspaceSlug = encodeURIComponent(workspaceSlug)
  const encodedTemplateSlug = encodeURIComponent(templateSlug)
  const encodedInputField = encodeURIComponent(inputField)

  const queryParams = new URLSearchParams(params)
  queryParams.set('auth_key', AUTH_KEY)
  queryParams.set('exp', `${Date.now() + 1 * 60 * 60 * 1000}`) // 1 hour
  queryParams.sort()

  const stringToSign = `${encodedWorkspaceSlug}/${encodedTemplateSlug}/${encodedInputField}?${queryParams}`
  const algorithm = 'sha256'
  const signature = crypto.createHmac(algorithm, AUTH_SECRET).update(stringToSign).digest('hex')

  const signedUrl = `https://${encodedWorkspaceSlug}.tlcdn.com/${encodedTemplateSlug}/${encodedInputField}?${queryParams}&sig=${algorithm}:${signature}`
  return signedUrl
}