Flag of Ukraine

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 App 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.

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);
    }
  }
}