Skip to main content
More in Learn

Performing Signature Verification

Signature verification allows users to be sure that it's really Crystallize calling their endpoint with a valid request. This is important for (among other things) protecting against man-in-the-middle attacks on things like webhooks, orders, and Apps, and also ensuring that custom views aren't visible to everyone who happens to know or guess the custom view URL.

Security is the user's responsibility. Crystallize will sign its requests, and it's on every user to verify them, or not, as they see fit.

Signature Access

Crystallize signs its requests using a secret that's unique to your tenant so your app can more confidently verify whether requests from Crystallize are authentic.

The Crystallize signature is a generated string (actually a JWT Token) attached to HTTP requests (for webhooks, Apps, or frontend previews) to prove and ensure those requests were made from Crystallize.

You can access the signature by getting:

  • the HTTP header with the name X-Crystallize-Signature for webhooks.
  • the query parameter with the name crystallizeSignature for apps and frontend previews. 

Notes:

  • The signature should not be used for authentication or authorization.
  • It does not contain any secret information.
  • The token expires the next second to make it almost unique for even more security.
  • It is confidential information that should not be stored in Git, for instance.

Format

The signature format is JSON Web Token (JWT) using the HS256 algorithm. JWT is an industry standard, and these resources provide good information about the JWT format: 

The payload has the following structure:

interface CrystallizeSignature {
  // Registered claims...
  aud: "webhook" | "app" | "frontend"
  sub: "signature"
  iss: "crystallize"
  exp: number // NumericDate
  iat: number // NumericDate
  
  // Additional data...
  userId: string
  tenantId: string
  tenantIdentifier: string
  hmac: string
}

const payload = {
  "iss": "crystallize",
  "iat": 1516239022,
  "exp": 1516239022,
  "aud": "webhook",
  "sub": "signature",
  "tenantIdentifier": "my-awesome-tenant",
  "tenantId": "123",
  "userId": "123",
  "hmac": "1101b34dac8c55e5590a37271f1c41c3d745463854613494a1624a15be24f1f8"
}

Verifying the Signature

The signature uses a secret for encryption and decryption. The Crystallize API generates a signatureSecret for each tenant:

# https://pim.crystallize.com/graphql
query GetSignatureSecret {
  tenant {
    get(identifier: "my-awesome-tenant") {
      signatureSecret
    }
  }
}

While everyone can use Apps, only Tenant Admins can read secrets. Other users will receive an empty string if they try to read signatureSecret or staticAuthToken. You can read more about authorization in Crystallize and managing user roles and permissions by following the links. 

We recommend storing your secret securely server-side and regenerating the signatureSecret in case of accidental exposure (more detail on this below).

Request signing follows this pattern:

  • Your App receives a request from Crystallize.
  • Your App retrieves the token, and with the help of the signature secret, it verifies the token.

Our JS API Client library includes a function that will handle verification for you.

A valid signature is a Signature that:

  • has been signed by Crystallize: can be checked by verifying the JWT signature.
  • has not been altered by anything: can be checked with the `hmac` present in the JWT.

This is the code to achieve the verification:

const guard = createSignatureVerifier({
    secret: `${process.env.CRYSTALLIZE_SIGNATURE_SECRET}`,
    sha256: (data: string) => crypto.createHash('sha256').update(data).digest('hex'),
    jwtVerify: (token: string, secret: string) => jwt.verify(token, secret) as CrystallizeSignature,
});

guard(signatureJwt, {
    url: request.url, // full URL here, including https:// (request.href in some framework)
    method: 'POST',
    body: 'THE RAW JSON BODY', // the library parse it for you
});

There are many libraries in different programming languages for decoding JWTs: https://jwt.io/libraries and to hash string. The above example uses Typescript and the jsonwebtoken library, but the `createSignatureVerifier` lets you provide the function that you prefer.

Note with Webhook using GET :

When using Webhook you may select GET as a HTTP Method. Even if you don't provide any GraphQL query Crystallize is going to call your endpoint with new parameters, in this case you need to provide an extra parameter to the guard function.

guard(signatureJwt, {
    url: request.url, // full URL here, including https://  etc. request.href in some framework
    webhookUrl: 'https://webhook.site/xxx', // the URL you have setup in the Webhook
    method: 'GET',
}),

Using that will instruct the JS API Client to extract URL Parameter and use it as a payload to verify the HMAC.

If you’re not using TypeScript, the procedure is the following:

  • get the Signature JWT from the header (or the URL param if not a webhook call).
  • Verify the JWT so you can trust the payload.
  • Get the HMAC in the payload.
  • Create a JSON string that contains (from the request)
    • the full URL (including https://) in the key: `url`
    • the method in the key: `method`
    • the body in the key: `body`
  • Hash the JSON string using the SHA256 algorithm and the result must be equal to the HMAC from the payload.

In Javascript, the procedure looks like:

const signatureJwt = request.headers['x-crystallize-signature'];
const payload = jwt.verify(signatureJwt, process.env.CRYSTALLIZE_SIGNATURE_SECRET);
const hmac = payload.hmac;
const objectToHash = {
    url: request.href,
    method: request.method,
    body: JSON.parse(request.body) // JSON.parse! We want an object here. JS API CLIENT does this for you
}
const isHMACValid = crypto.createHash('sha256').update(JSON.stringify(objectToHash)).digest('hex') === hmac;

Regenerating the Signature Secret

The signature secret for decoding signatures should be stored securely. Anyone with access to this secret can create valid signatures. There is a mutation to regenerate the signatureSecret for a tenant: 

# https://pim.crystallize.com/graphql
mutation RegenerateSignatureSecret {
  tenant {
    regenerateSecrets(tenantId: "123", input: { signatureSecret: true }) {
      signatureSecret
    }
  }
}

Only Tenant Admins can regenerate secrets. Other users will receive an error if they try to run this mutation. You can read more about authorization in Crystallize and managing user roles and permissions by following the links. 

People showing thumbs up

Need further assistance?

Ask the Crystallize team or other enthusiasts in our slack community.

Join our slack community