14.2 Security
Webhook security is provided by shared secret request signing. The signature is placed in the X-Signature header. The request is first transformed into a text representation like:
POST /path
Date: <date>
Content-Type: application/json
Host: game-server.com
X-Idempotency: <random idempotency data>
{body: 'data'}
And then signed with code like the following:
export function sign(message: string, secret: string) {
return crypto.createHmac('sha256', secret).update(message).digest('base64');
}
export function getRequestText(
req: {
method?: string;
url: string | null | undefined;
body?: string;
headers?: Record<string, any>;
},
includeHeaders: string[]
) {
return `${req.method?.toUpperCase()} ${req.url}
${includeHeaders
.map(header => `${header}: ${req.headers?.[header]}`)
.join('\n')}
${req.body}`;
}
export function getAxiosRequestText(
{ method, headers, data, ...req }: AxiosRequestConfig<string>,
includeHeaders: string[]
) {
// borrowed from axios code
let combinedUrl: string;
if (req.baseURL) {
combinedUrl = req.url
? req.baseURL.replace(/\/+$/, '') + '/' + req.url.replace(/^\/+/, '')
: req.baseURL;
} else {
combinedUrl = req.url as string;
}
const { host, pathname, search } = new URL(combinedUrl);
return getRequestText(
{
method,
url: pathname + search,
headers: {
...headers,
Host: host,
},
body: data,
},
includeHeaders
);
}
const requestText = getAxiosRequestText(config, this.signedHeaders);
config.headers['X-Signature'] = sign(requestText, key);
config.headers['X-Signed-Headers'] = this.signedHeaders.join(',');
config.headers['X-Signed-Value'] = encodeURIComponent(requestText);
The signature is computed and placed in the X-Signature header. It is up to you to implement signing in the same manner and compare the two signed values to ensure they originate from the Gala platform. X-Signed-Value can be enabled for debugging purposes which will contain the signed value as computed by the Gala platform to ensure it matches on both sides.