Syncor

Authentication and signatures

How we authenticate to your endpoint and how to check the signature.

Authentication

Choose one on the subscription:

  • None. Rely on the signature (recommended) or your network controls.
  • Bearer token. We send Authorization: Bearer {your token}.
  • OAuth 2.0 client credentials (Microsoft Entra). Add an OAuth connection in the developer portal and choose it on the subscription. We request a token from your tenant and send it as a bearer token. If your endpoint answers 401, we get a fresh token and try once more straight away.

Any of these can be combined with a signing secret.

Checking the signature

When the subscription has a signing secret, each request carries:

X-Syncor-Signature: sha256={hex HMAC-SHA256 of "{X-Syncor-Timestamp}.{raw body}" using your signing secret}

To check it:

  1. Read the raw request body, exactly as received, before parsing it.
  2. Join the X-Syncor-Timestamp header, a full stop and the raw body.
  3. Compute the HMAC-SHA256 of that with your signing secret, as lowercase hex, and put sha256= in front.
  4. Compare it with X-Syncor-Signature using a constant-time comparison.
  5. Reject requests whose timestamp is more than 5 minutes from your clock. Every attempt, including retries, gets a fresh timestamp.

C# (.NET 5 or later):

using System.Linq;
using System.Security.Cryptography;
using System.Text;

static bool IsValidSyncorSignature(byte[] rawBody, string timestamp, string signature, string secret)
{
    using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
    var signed = Encoding.UTF8.GetBytes(timestamp + ".").Concat(rawBody).ToArray();
    var expected = "sha256=" + Convert.ToHexString(hmac.ComputeHash(signed)).ToLowerInvariant();

    return CryptographicOperations.FixedTimeEquals(
        Encoding.UTF8.GetBytes(expected),
        Encoding.UTF8.GetBytes(signature ?? ""));
}

Node.js:

import crypto from 'node:crypto';

function isValidSyncorSignature(rawBody, timestamp, signature, secret) {
  const expected = 'sha256=' + crypto.createHmac('sha256', secret)
    .update(`${timestamp}.`)
    .update(rawBody) // a Buffer with the raw request body
    .digest('hex');
  const received = Buffer.from(signature ?? '');

  return received.length === expected.length
    && crypto.timingSafeEqual(received, Buffer.from(expected));
}

PHP:

function isValidSyncorSignature(string $rawBody, string $timestamp, string $signature, string $secret): bool
{
    $expected = 'sha256='.hash_hmac('sha256', $timestamp.'.'.$rawBody, $secret);

    return hash_equals($expected, $signature);
}

Rotating the secret

Rotate secret on the subscription replaces the secret at once and shows the new one a single time. Deliveries after that are signed with the new secret, so update your receiver straight away. A delivery that fails your check in between is retried and succeeds once your receiver has the new secret.