{"id":80772,"date":"2026-07-23T14:35:21","date_gmt":"2026-07-23T09:05:21","guid":{"rendered":"https:\/\/www.tothenew.com\/blog\/?p=80772"},"modified":"2026-07-30T15:57:26","modified_gmt":"2026-07-30T10:27:26","slug":"signing-api-requests-with-hmac-in-react-native","status":"publish","type":"post","link":"https:\/\/www.tothenew.com\/blog\/signing-api-requests-with-hmac-in-react-native\/","title":{"rendered":"Signing API Requests with HMAC in React Native"},"content":{"rendered":"<h2>Introduction<\/h2>\n<p>HMAC request signing proves two things to your backend: the caller knows a shared secret, and the request was not altered after it was signed. It is not encryption \u2014 the body stays readable \u2014 but it is a strong, lightweight integrity and authenticity check for mobile APIs.<\/p>\n<hr \/>\n<h2>What is HMAC?<br \/>\nHMAC (Hash-based Message Authentication Code) mixes a hash function (usually SHA-256) with a secret key.<\/h2>\n<p>You pass in:<\/p>\n<ol>\n<li>A message string<\/li>\n<li>A secret both client and server share<\/li>\n<\/ol>\n<p>You get back a fixed-length signature (typically hex).<\/p>\n<table style=\"border-collapse: collapse; width: 46.3294%; height: 149px;\">\n<tbody>\n<tr style=\"height: 77px;\">\n<td style=\"width: 19.9305%; height: 77px;\">\n<h4><span style=\"color: #000000;\"><strong>\u00a0 \u00a0 Property<\/strong><\/span><\/h4>\n<\/td>\n<td style=\"width: 20.5573%; height: 77px;\">\n<h4><strong><span style=\"color: #000000;\">\u00a0 \u00a0 Meaning<\/span><\/strong><\/h4>\n<\/td>\n<\/tr>\n<tr style=\"height: 24px;\">\n<td style=\"width: 19.9305%; height: 24px;\"><span style=\"color: #000000;\">\u00a0 \u00a0 Integrity<\/span><\/td>\n<td style=\"width: 20.5573%; height: 24px;\"><span style=\"color: #000000;\">\u00a0 \u00a0 Change one character of the message \u2192 signature changes<\/span><\/td>\n<\/tr>\n<tr style=\"height: 24px;\">\n<td style=\"width: 19.9305%; height: 24px;\"><span style=\"color: #000000;\">\u00a0 \u00a0 Authenticity<\/span><\/td>\n<td style=\"width: 20.5573%; height: 24px;\"><span style=\"color: #000000;\">\u00a0 \u00a0 Only holders of the secret can produce a valid signature<\/span><\/td>\n<\/tr>\n<tr style=\"height: 24px;\">\n<td style=\"width: 19.9305%; height: 24px;\"><span style=\"color: #000000;\">\u00a0 \u00a0 Not encryption<\/span><\/td>\n<td style=\"width: 20.5573%; height: 24px;\"><span style=\"color: #000000;\">\u00a0 \u00a0 Payload remains readable; HMAC proves who signed it<\/span><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>API flow:<\/p>\n<blockquote><p><span style=\"color: #008000;\"><!--more--><!--more--><strong>Client \u2192 build canonical string \u2192 HMAC-SHA256 \u2192 send signature header<\/strong><\/span><br \/>\n<strong><span style=\"color: #008000;\">Server \u2192 rebuild same canonical string \u2192 HMAC-SHA256 \u2192 compare (constant-time)<\/span><\/strong><\/p>\n<p>Same canonical string + same secret = match. Anything else fails.<\/p><\/blockquote>\n<hr \/>\n<h2>Generate a strong secret<\/h2>\n<blockquote><p><span style=\"color: #008000;\"><strong>openssl rand -hex 32<\/strong><\/span><\/p><\/blockquote>\n<p>That yields 32 random bytes as 64 hex characters \u2014 a 256-bit key that fits HMAC-SHA256 well.<\/p>\n<p>Why bother?<\/p>\n<table style=\"border-collapse: collapse; width: 64.9505%;\">\n<tbody>\n<tr>\n<td style=\"width: 29.0063%;\">\n<h4><span style=\"color: #000000;\">\u00a0 \u00a0 <strong>Weak choice<\/strong><\/span><\/h4>\n<\/td>\n<td style=\"width: 35.9445%;\">\n<h4><span style=\"color: #000000;\">\u00a0 \u00a0 Problem<\/span><\/h4>\n<\/td>\n<\/tr>\n<tr>\n<td style=\"width: 29.0063%;\"><span style=\"color: #000000;\">\u00a0 \u00a0 &#8220;mySecret123&#8221; \/ app-name keys<\/span><\/td>\n<td style=\"width: 35.9445%;\"><span style=\"color: #000000;\">\u00a0 \u00a0 Easy to guess or brute-force<\/span><\/td>\n<\/tr>\n<tr>\n<td style=\"width: 29.0063%;\"><span style=\"color: #000000;\">\u00a0 \u00a0 Short \/ human-picked secrets<\/span><\/td>\n<td style=\"width: 35.9445%;\"><span style=\"color: #000000;\">\u00a0 \u00a0 Low entropy<\/span><\/td>\n<\/tr>\n<tr>\n<td style=\"width: 29.0063%;\"><span style=\"color: #000000;\">\u00a0 \u00a0 Secrets in git or logs<\/span><\/td>\n<td style=\"width: 35.9445%;\"><span style=\"color: #000000;\">\u00a0 \u00a0 Instant leak<\/span><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Use a CSPRNG (openssl rand), store the value in a secrets manager, and never commit or log it.<\/p>\n<hr \/>\n<p>The canonical string<\/p>\n<p>HMAC does not understand HTTP. You pick the fields that matter, join them in a fixed order, then sign that string.<\/p>\n<p><span style=\"color: #000000;\"><strong>METHOD<\/strong><\/span><br \/>\n<span style=\"color: #000000;\"><strong>PATH<\/strong><\/span><br \/>\n<span style=\"color: #000000;\"><strong>TIMESTAMP<\/strong><\/span><br \/>\n<span style=\"color: #000000;\"><strong>CLIENT_PLATFORM<\/strong><\/span><br \/>\n<span style=\"color: #000000;\"><strong>SECRET_ID<\/strong><\/span><br \/>\n<span style=\"color: #000000;\"><strong>SIGNED_HEADERS<\/strong><\/span><br \/>\n<span style=\"color: #000000;\"><strong>BODY_HASH<\/strong><\/span><\/p>\n<p>Example:<\/p>\n<p><strong><span style=\"color: #000000;\">POST<\/span><\/strong><br \/>\n<strong><span style=\"color: #000000;\">\/api\/v1\/orders<\/span><\/strong><br \/>\n<strong><span style=\"color: #000000;\">1710000000000<\/span><\/strong><br \/>\n<strong><span style=\"color: #000000;\">ios<\/span><\/strong><br \/>\n<strong><span style=\"color: #000000;\">mobile-app-v1<\/span><\/strong><br \/>\n<strong><span style=\"color: #000000;\">accept:application\/json;x-api-version:1<\/span><\/strong><br \/>\n<strong><span style=\"color: #000000;\">a3f2c9&#8230; \u2190 sha256 hex of the body<\/span><\/strong><\/p>\n<p>More fields \u2192 stronger binding. Each line ties the signature to another part of the request:<\/p>\n<table style=\"border-collapse: collapse; width: 61.9959%; height: 220px;\">\n<tbody>\n<tr style=\"height: 76px;\">\n<td style=\"width: 24.1293%; height: 76px;\">\n<h4><strong><span style=\"color: #000000;\">\u00a0 \u00a0 Field<\/span><\/strong><\/h4>\n<\/td>\n<td style=\"width: 33.981%; height: 76px;\">\n<h4><strong><span style=\"color: #000000;\">\u00a0 \u00a0 Guards against<\/span><\/strong><\/h4>\n<\/td>\n<\/tr>\n<tr style=\"height: 24px;\">\n<td style=\"width: 24.1293%; height: 24px;\">\u00a0 \u00a0 Method + path<\/td>\n<td style=\"width: 33.981%; height: 24px;\">\u00a0 \u00a0 Replaying a body on a different endpoint<\/td>\n<\/tr>\n<tr style=\"height: 24px;\">\n<td style=\"width: 24.1293%; height: 24px;\">\u00a0 \u00a0 Timestamp<\/td>\n<td style=\"width: 33.981%; height: 24px;\">\u00a0 \u00a0 Stale \/ replayed requests<\/td>\n<\/tr>\n<tr style=\"height: 24px;\">\n<td style=\"width: 24.1293%; height: 24px;\">\u00a0 \u00a0 Client platform<\/td>\n<td style=\"width: 33.981%; height: 24px;\">\u00a0 \u00a0 Cross-client misuse of the same secret<\/td>\n<\/tr>\n<tr style=\"height: 24px;\">\n<td style=\"width: 24.1293%; height: 24px;\">\u00a0 \u00a0 Secret id<\/td>\n<td style=\"width: 33.981%; height: 24px;\">\u00a0 \u00a0 Ambiguity during key rotation<\/td>\n<\/tr>\n<tr style=\"height: 24px;\">\n<td style=\"width: 24.1293%; height: 24px;\">\u00a0 \u00a0 Signed headers<\/td>\n<td style=\"width: 33.981%; height: 24px;\">\u00a0 \u00a0 Spoofing headers you care about<\/td>\n<\/tr>\n<tr style=\"height: 24px;\">\n<td style=\"width: 24.1293%; height: 24px;\">\u00a0 \u00a0 Body hash (bodyHex)<\/td>\n<td style=\"width: 33.981%; height: 24px;\">\u00a0 \u00a0 Tampering with the payload<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Signing only method + path is weak. Bind method, path, time, identity, headers, and body hash \u2014 and keep that list identical on client and server.<\/p>\n<hr \/>\n<h2>Reference implementation<\/h2>\n<p>Uses [react-native-quick-crypto](https:\/\/github.com\/margelo\/react-native-quick-crypto). Names are illustrative \u2014 align them with your backend contract.<\/p>\n<p><strong><span style=\"color: #339966;\">import Crypto from &#8220;react-native-quick-crypto&#8221;;<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">import { resolvePath, sortBodyKeys } from &#8220;.\/signingHelpers&#8221;;<\/span><\/strong><\/p>\n<p><strong><span style=\"color: #339966;\">\/** HMAC-SHA256 \u2192 hex digest *\/<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">export const createHmacDigest = (message, sharedSecret) =&gt; {<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">if (!sharedSecret) {<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">throw new Error(&#8220;Shared signing secret is required&#8221;);<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">}<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">const hmac = Crypto.createHmac(&#8220;sha256&#8221;, sharedSecret);<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">hmac.update(message);<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">return hmac.digest(&#8220;hex&#8221;);<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">};<\/span><\/strong><\/p>\n<p><strong><span style=\"color: #339966;\">const sha256Hex = (data) =&gt;<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">Crypto.createHash(&#8220;sha256&#8221;).update(data).digest(&#8220;hex&#8221;);<\/span><\/strong><\/p>\n<p><strong><span style=\"color: #339966;\">\/** Build canonical string and sign it *\/<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">export const buildRequestSignature = ({<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">method,<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">url,<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">issuedAt,<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">body,<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">sharedSecret,<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">secretId,<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">signedHeaderBlock,<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">clientRuntime, \/\/ e.g. &#8220;ios&#8221; | &#8220;android&#8221; | &#8220;web&#8221;<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">}) =&gt; {<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">let bodyStr = &#8220;&#8221;;<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">if (body != null) {<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">bodyStr = typeof body === &#8220;string&#8221; ? body : JSON.stringify(body);<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">}<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">const bodyHex = sha256Hex(bodyStr);<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">const path = resolvePath(url);<\/span><\/strong><\/p>\n<p><strong><span style=\"color: #339966;\">\/\/ Order must match the backend exactly.<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">\/\/ More lines = signature covers more of the request.<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">const canonical = [<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">method.toUpperCase(),<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">path,<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">issuedAt,<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">clientRuntime,<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">secretId,<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">signedHeaderBlock,<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">bodyHex,<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">].join(&#8220;\\n&#8221;);<\/span><\/strong><\/p>\n<p><strong><span style=\"color: #339966;\">return createHmacDigest(canonical, sharedSecret);<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">};<\/span><\/strong><\/p>\n<p><strong><span style=\"color: #339966;\">\/** Attach time + signature headers to an outgoing request *\/<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">export const signOutgoingRequest = (request, sharedSecret) =&gt; {<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">if (!sharedSecret) {<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">return request;<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">}<\/span><\/strong><\/p>\n<p><strong><span style=\"color: #339966;\">try {<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">const issuedAt = String(Date.now());<\/span><\/strong><\/p>\n<p><strong><span style=\"color: #339966;\">\/\/ Sort body keys so client\/server stringify &amp; hash the same JSON.<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">const sortedBody = sortBodyKeys(request.data);<\/span><\/strong><\/p>\n<p><strong><span style=\"color: #339966;\">if (sortedBody != null &amp;&amp; typeof sortedBody === &#8220;object&#8221;) {<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">request.data = sortedBody;<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">}<\/span><\/strong><\/p>\n<p><strong><span style=\"color: #339966;\">const signature = buildRequestSignature({<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">method: request?.method,<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">url: request.url,<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">issuedAt,<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">body: sortedBody,<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">sharedSecret,<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">secretId: &#8220;mobile-app-v1&#8221;,<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">signedHeaderBlock: &#8220;accept:application\/json;x-api-version:1&#8221;,<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">clientRuntime: &#8220;ios&#8221;, \/\/ or detect at runtime<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">});<\/span><\/strong><\/p>\n<p><strong><span style=\"color: #339966;\">request.headers[&#8220;x-request-time&#8221;] = issuedAt;<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">request.headers[&#8220;x-request-signature&#8221;] = signature;<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">} catch (error) {<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">console.error(&#8220;[signing] Failed to sign request:&#8221;, error);<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">}<\/span><\/strong><\/p>\n<p><strong><span style=\"color: #339966;\">return request;<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">};<\/span><\/strong><\/p>\n<table style=\"border-collapse: collapse; width: 52.8645%;\">\n<tbody>\n<tr>\n<td style=\"width: 26.41%;\">\n<h4><strong><span style=\"color: #000000;\">\u00a0 \u00a0 Field<\/span><\/strong><\/h4>\n<\/td>\n<td style=\"width: 26.4548%;\">\n<h4><strong><span style=\"color: #000000;\">\u00a0 \u00a0 Role<\/span><\/strong><\/h4>\n<\/td>\n<\/tr>\n<tr>\n<td style=\"width: 26.41%;\"><span style=\"color: #000000;\">\u00a0 \u00a0 secretId<\/span><\/td>\n<td style=\"width: 26.4548%;\"><span style=\"color: #000000;\">\u00a0 \u00a0 &#8220;mobile-app-v1&#8221; \u2014 server looks up which secret to use (rotation)<\/span><\/td>\n<\/tr>\n<tr>\n<td style=\"width: 26.41%;\"><span style=\"color: #000000;\">\u00a0 \u00a0 signedHeaderBlock<\/span><\/td>\n<td style=\"width: 26.4548%;\"><span style=\"color: #000000;\">\u00a0 \u00a0 Headers bound into the signature, same format on both sides<\/span><\/td>\n<\/tr>\n<tr>\n<td style=\"width: 26.41%;\"><span style=\"color: #000000;\">\u00a0 \u00a0 bodyHex<\/span><\/td>\n<td style=\"width: 26.4548%;\"><span style=\"color: #000000;\">\u00a0 \u00a0 sha256Hex(bodyStr) \u2014 hash of the sorted, stringified body<\/span><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<hr \/>\n<h2>How to call signOutgoingRequest<\/h2>\n<p>Hook it into your HTTP client request interceptor after headers and body are ready:<\/p>\n<p><strong><span style=\"color: #339966;\">import axios from &#8220;axios&#8221;;<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">import { signOutgoingRequest } from &#8220;.\/requestSigning&#8221;;<\/span><\/strong><\/p>\n<p><strong><span style=\"color: #339966;\">const api = axios.create({<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">baseURL: &#8220;https:\/\/api.example.com&#8221;,<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">});<\/span><\/strong><\/p>\n<p><strong><span style=\"color: #339966;\">api.interceptors.request.use(<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">(request) =&gt; {<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">request.headers = request.headers ?? {};<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">request.headers[&#8220;Content-Type&#8221;] = &#8220;application\/json&#8221;;<\/span><\/strong><\/p>\n<p><strong><span style=\"color: #339966;\">signOutgoingRequest(request, sharedSecret); \/\/ adds x-request-time + x-request-signature<\/span><\/strong><\/p>\n<p><strong><span style=\"color: #339966;\">return request;<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">},<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">(error) =&gt; Promise.reject(error),<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">);<\/span><\/strong><\/p>\n<p><strong><span style=\"color: #339966;\">await api.post(&#8220;\/api\/v1\/orders&#8221;, { itemId: &#8220;123&#8221;, qty: 2 });<\/span><\/strong><\/p>\n<p>One-off:<\/p>\n<p><strong><span style=\"color: #339966;\">const request = {<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">method: &#8220;post&#8221;,<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">url: &#8220;\/api\/v1\/orders&#8221;,<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">data: { itemId: &#8220;123&#8221;, qty: 2 },<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">headers: {},<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">};<\/span><\/strong><\/p>\n<p><strong><span style=\"color: #339966;\">signOutgoingRequest(request, sharedSecret);<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">await api.request(request);<\/span><\/strong><\/p>\n<table style=\"border-collapse: collapse; width: 45.7921%;\">\n<tbody>\n<tr>\n<td style=\"width: 20.3671%;\">\n<h4><span style=\"color: #000000;\"><strong>\u00a0 \u00a0 Argument<\/strong><\/span><\/h4>\n<\/td>\n<td style=\"width: 25.4252%;\">\n<h4><span style=\"color: #000000;\"><strong>\u00a0 \u00a0 Meaning<\/strong><\/span><\/h4>\n<\/td>\n<\/tr>\n<tr>\n<td style=\"width: 20.3671%;\"><span style=\"color: #000000;\">\u00a0 \u00a0 request<\/span><\/td>\n<td style=\"width: 25.4252%;\"><span style=\"color: #000000;\">\u00a0 \u00a0 Needs method, url, data, headers<\/span><\/td>\n<\/tr>\n<tr>\n<td style=\"width: 20.3671%;\"><span style=\"color: #000000;\">\u00a0 \u00a0 sharedSecret<\/span><\/td>\n<td style=\"width: 25.4252%;\"><span style=\"color: #000000;\">\u00a0 \u00a0 From openssl rand -hex 32 (or secrets store). Empty \u2192 request left unsigned<\/span><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Headers set on success:<\/p>\n<ul>\n<li>x-request-time \u2014 same value inside the canonical string<\/li>\n<li>x-request-signature \u2014 hex HMAC of the canonical string<\/li>\n<\/ul>\n<p>Use whatever header names your backend defines.<\/p>\n<hr \/>\n<h2>Sort the body before hashing<\/h2>\n<p>JSON key order is not guaranteed:<\/p>\n<p><strong><span style=\"color: #339966;\">{&#8220;b&#8221;:1,&#8221;a&#8221;:2} vs {&#8220;a&#8221;:2,&#8221;b&#8221;:1}<\/span><\/strong><\/p>\n<p>Same meaning, different bytes \u2192 different bodyHex \u2192 signature mismatch.<\/p>\n<p><strong>Body sort fixes it:<\/strong> both sides run the same key-order pass (sortBodyKeys) before JSON.stringify and sha256Hex. Send that sorted body on the wire so what you transmit matches what you signed.<\/p>\n<hr \/>\n<h2>Challenges I faced<\/h2>\n<p>Canonical strings did not match<br \/>\nEven with the correct secret, signatures failed until both sides built the exact same string.<\/p>\n<p>Common causes:<\/p>\n<ul>\n<li>Field order differs (timestamp before path on one side)<\/li>\n<li>Different newlines \/ separators<\/li>\n<li>Method casing (post vs POST)<\/li>\n<li>Path shape (\/api\/foo vs full URL vs trailing slash)<\/li>\n<li>Empty body as &#8220;&#8221; vs &#8220;{}&#8221; vs omitted<\/li>\n<li>Body hashed before sort on one side, after sort on the other<\/li>\n<\/ul>\n<p>Fix: in non-prod, log the full canonical string (or its hash) on client and server and diff them.<\/p>\n<h4>Signature still wrong when \u201clogic looks fine\u201d<\/h4>\n<p>Usually the message encoding differs, not the crypto:<\/p>\n<ul>\n<li>Algorithm (sha256) and digest form (hex vs base64)<\/li>\n<li>Secret as UTF-8 string vs raw bytes<\/li>\n<li>Timestamp as string vs number<\/li>\n<li>Header block casing \/ trimming<\/li>\n<\/ul>\n<h4>Sequence is the protocol<\/h4>\n<p>If the backend expects:<\/p>\n<p><strong><span style=\"color: #339966;\">METHOD \\n PATH \\n TIMESTAMP \\n &#8230;<\/span><\/strong><\/p>\n<p>and the client sends:<\/p>\n<p><strong><span style=\"color: #339966;\">METHOD \\n TIMESTAMP \\n PATH \\n &#8230;<\/span><\/strong><\/p>\n<p>it will never verify. HMAC will not \u201cfigure out\u201d order \u2014 you must keep the same sequence as the backend (or document a shared sort both sides apply).<\/p>\n<hr \/>\n<p>Best practice: mirror the backend<\/p>\n<p>Treat the canonical format as a shared protocol \u2014 not a client-only convenience.<\/p>\n<table style=\"border-collapse: collapse; width: 48.9255%; height: 96px;\">\n<tbody>\n<tr style=\"height: 24px;\">\n<td style=\"width: 22.1576%; height: 24px;\">\n<h4><span style=\"color: #000000;\">\u00a0 \u00a0 Do<\/span><\/h4>\n<\/td>\n<td style=\"width: 26.7681%; height: 24px;\">\n<h4><span style=\"color: #000000;\">\u00a0 \u00a0 Why<\/span><\/h4>\n<\/td>\n<\/tr>\n<tr style=\"height: 24px;\">\n<td style=\"width: 22.1576%; height: 24px;\"><span style=\"color: #000000;\">\u00a0 \u00a0 Same fields, same order, same separators<\/span><\/td>\n<td style=\"width: 26.7681%; height: 24px;\"><span style=\"color: #000000;\">\u00a0 \u00a0 HMAC is order-sensitive<\/span><\/td>\n<\/tr>\n<tr style=\"height: 24px;\">\n<td style=\"width: 22.1576%; height: 24px;\"><span style=\"color: #000000;\">\u00a0 \u00a0 Sort body keys on both sides<\/span><\/td>\n<td style=\"width: 26.7681%; height: 24px;\"><span style=\"color: #000000;\">\u00a0 \u00a0 Avoids JSON key-order drift<\/span><\/td>\n<\/tr>\n<tr style=\"height: 24px;\">\n<td style=\"width: 22.1576%; height: 24px;\"><span style=\"color: #000000;\">\u00a0 \u00a0 Stabilize only the headers you sign<\/span><\/td>\n<td style=\"width: 26.7681%; height: 24px;\"><span style=\"color: #000000;\">\u00a0 \u00a0 Keeps signedHeaderBlock identical<\/span><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Do not invent a friendlier order on the client. Align with BE first; any body-sort step must run the same way on both sides.<\/p>\n<hr \/>\n<h2>Checklist<\/h2>\n<ol>\n<li>One written canonicalization spec for mobile, web, and API.<\/li>\n<li>Bind many request dimensions (method, path, time, secret id, headers, body hash).<\/li>\n<li>Strong secret via openssl rand -hex 32; rotate with secretId.<\/li>\n<li>Server rejects old timestamps (e.g. \u00b12\u20135 minutes).<\/li>\n<li>Hash the body (bodyHex); do not dump huge payloads raw into HMAC.<\/li>\n<li>Constant-time signature compare on the server.<\/li>\n<li>Never log secrets; debug canonical strings only in non-prod.<\/li>\n<li>Fail closed if signing fails when the API requires HMAC.<\/li>\n<li>Version the scheme if field order ever changes.<\/li>\n<li>Golden-vector tests: fixed inputs \u2192 expected signature on both sides.<\/li>\n<\/ol>\n<hr \/>\n<h2>Mental model<\/h2>\n<p><strong><span style=\"color: #339966;\">openssl rand -hex 32 \u2192 shared secret<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">sort body + pick fields \u2192 canonical string (fixed order)<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">HMAC-SHA256 \u2192 x-request-signature<\/span><\/strong><br \/>\n<strong><span style=\"color: #339966;\">server repeats \u2192 accept or 401<\/span><\/strong><\/p>\n<p>HMAC itself is simple. The hard part is discipline: identical inputs, identical order, identical encoding. Get the canonical string right on both sides, and request signing becomes boring \u2014 in the best way.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Introduction HMAC request signing proves two things to your backend: the caller knows a shared secret, and the request was not altered after it was signed. It is not encryption \u2014 the body stays readable \u2014 but it is a strong, lightweight integrity and authenticity check for mobile APIs. What is HMAC? HMAC (Hash-based Message [&hellip;]<\/p>\n","protected":false},"author":1816,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"iawp_total_views":4},"categories":[5881],"tags":[8746,1303,8570,5853],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/80772"}],"collection":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/users\/1816"}],"replies":[{"embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/comments?post=80772"}],"version-history":[{"count":2,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/80772\/revisions"}],"predecessor-version":[{"id":80972,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/80772\/revisions\/80972"}],"wp:attachment":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/media?parent=80772"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/categories?post=80772"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/tags?post=80772"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}