Signature verification ensures that incoming requests truly come from Crystallize. It helps protect against man-in-the-middle attacks and unauthorized access to sensitive features such as webhooks, orders, and custom views.
Each request from Crystallize includes a digital signature. Your system should verify this signature before processing the request. This confirms both the source and integrity of the message.
While Crystallize signs all outgoing requests, it’s up to you to implement verification on your end. Doing so is strongly recommended to maintain a secure integration.
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:
X-Crystallize-Signature for webhooks.crystallizeSignature for apps and frontend previews.
Notes:
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"
}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"
}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
}
}
}# 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:
Our JS API Client library includes a function that will handle verification for you.
A valid signature is a Signature that:
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
});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.
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',
}),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:
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;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;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
}
}
}# 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.