hmac verification

hmac based authentication, similar to its session token counterpart for embedded apps, is the means of associating initial requests to open your application (from Mantle) with more traditional forms of authentication (cookies). Since non-embedded applications are not hosted in an embedded iframe, there are no 3rd party cookie concerns.

When a user opens your non-embedded extension in Mantle, your application will receive a request that looks something like this:

https://example.com/api?timestamp=1609459200&organizationId=org123&userId=user456&hmac=abcdef123456

To verify the hmac:

  • Extract the hmac parameter from the list of parameters
  • Sort the remaining parameters alphabetically by key into a string:
    organizationId=org123&timestamp=1609459200&userId=user456
  • Optionally (encouraged!) verify the timestamp parameter to make sure it is recent (within a minute or an hour) to prevent potential replay attacks.
  • Prepend the timestamp to the sorted parameters string payload:
    1609459200.organizationId=org123&timestamp=1609459200&userId=user456
  • Verify the payload with your shared extension secret and assert that the hmac parameter provided by Mantle matches your own generated one
  • If they match, you can trust assert that the parameters were signed by Mantle, and you can trust the userId and organizationId params as a way of associating this request with a user account. The user and organization records would have been fetched by your application after completing the oauth flow

oauth flow

Node.js example

  import { createHmac, timingSafeEqual } from "crypto";

  const { hmac, ...otherParams } = request.query;
  const { timestamp } = otherParams;

  const sortedSignedParams = Object.keys(otherParams)
    .sort()
    .map((k) => `${k}=${otherParams[k]}`)
    .join("&");

  // TODO: verify timestamp
  const hmacPayload = `${timestamp}.${sortedSignedParams}`;

  const _hmac = createHmac("sha256", process.env.MANTLE_EXT_SECRET);
  _hmac.update(hmacPayload, "utf8");
  const calculatedHmac = _hmac.digest("hex");

  if (!timingSafeEqual(Buffer.from(calculatedHmac), Buffer.from(hmac))) {
    logger.info(
      {
        hmac,
        calculatedHmac,
        hmacPayload,
      },
      "Invalid HMAC"
    );
    return reply.status(403).send({ success: false, error: "Invalid HMAC" });
  }

  const { organizationId, userId } = request.query;
  // ... proceed with authenticated request