When building mobile applications—especially in the fintech, maritime, or ERP sectors—most developers think security stops at JWT Authentication and HTTPS.
But what happens if a malicious user or a compromised network proxy intercepts a perfectly valid POST request? For example:
POST /api/wallet/transfer { "amount": 100, "to": "user_b" }
Even with a valid JWT, the attacker can capture this payload and "replay" (resend) it 100 times, draining the victim's wallet. Your backend will process it because the token is technically valid.
To prevent this, enterprise systems use HMAC (Hash-based Message Authentication Code) Signatures combined with strict Timestamps. Here is how to implement this end-to-end between React Native and .NET Core.
The Concept
The Client (React Native) takes the request body, adds the current timestamp, and hashes it using a secret key. It sends this hash in the headers.
The Server (.NET) receives the request. First, it checks if the timestamp is older than 60 seconds (if yes, it rejects it immediately to prevent old replays). Then, the server hashes the body and timestamp using the same secret key.
If the server's hash matches the client's hash, the request is untouched and fresh.
Step 1: The React Native Implementation (Axios)
First, install a crypto library: npm install crypto-js.
Then, we use an Axios Interceptor to automatically sign every outgoing POST, PUT, or PATCH request.
import axios from 'axios';
import CryptoJS from 'crypto-js';
const api = axios.create({ baseURL: 'https://api.yourdomain.com' });
const CLIENT_SECRET = process.env.EXPO_PUBLIC_HMAC_SECRET; // Must match the backend
api.interceptors.request.use((config) => {
// Only sign requests that modify data
if (['post', 'put', 'patch'].includes(config.method ?? '')) {
const timestamp = Date.now().toString();
const bodyString = config.data ? JSON.stringify(config.data) : '';
// Create the signature payload: Timestamp + Body
const payloadToSign = `${timestamp}:${bodyString}`;
// Hash it using HMAC-SHA256
const signature = CryptoJS.HmacSHA256(payloadToSign, CLIENT_SECRET).toString(CryptoJS.enc.Hex);
// Attach to headers
config.headers['X-Request-Timestamp'] = timestamp;
config.headers['X-Request-Signature'] = signature;
}
return config;
});
export default api;
Step 2: The .NET 9 Backend Implementation
Instead of polluting our controllers, we write a clean, reusable ActionFilterAttribute in C# to intercept requests before they even hit our endpoints.
Create a file called ValidateHmacSignatureAttribute.cs:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System.Security.Cryptography;
using System.Text;
public class ValidateHmacSignatureAttribute : ActionFilterAttribute
{
private const int MaxAgeInSeconds = 60;
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var request = context.HttpContext.Request;
// 1. Extract Headers
if (!request.Headers.TryGetValue("X-Request-Timestamp", out var timestampValues) ||
!request.Headers.TryGetValue("X-Request-Signature", out var signatureValues))
{
context.Result = new UnauthorizedObjectResult("Missing security headers.");
return;
}
var timestampStr = timestampValues.ToString();
var clientSignature = signatureValues.ToString();
// 2. Prevent Replay Attacks via Timestamp Validation
if (!long.TryParse(timestampStr, out long timestampMs))
{
context.Result = new BadRequestObjectResult("Invalid timestamp format.");
return;
}
var requestTime = DateTimeOffset.FromUnixTimeMilliseconds(timestampMs);
if ((DateTimeOffset.UtcNow - requestTime).TotalSeconds > MaxAgeInSeconds)
{
context.Result = new UnauthorizedObjectResult("Request expired. Replay attack detected.");
return;
}
// 3. Rebuild the payload and validate signature
request.EnableBuffering();
using var reader = new StreamReader(request.Body, Encoding.UTF8, leaveOpen: true);
var body = await reader.ReadToEndAsync();
request.Body.Position = 0; // Reset stream for the controller
var payloadToSign = $"{timestampStr}:{body}";
// In a real app, inject this secret via IConfiguration
var serverSecret = "YOUR_SHARED_SECRET_KEY";
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(serverSecret));
var hashBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(payloadToSign));
var serverSignature = Convert.ToHexString(hashBytes).ToLower();
// 4. Compare
if (serverSignature != clientSignature.ToLower())
{
context.Result = new UnauthorizedObjectResult("Payload tampering detected. Invalid signature.");
return;
}
await next();
}
}
Step 3: Secure Your Endpoints
Now, to protect any financial transaction in your API, you simply add the attribute to the controller:
[HttpPost("transfer")]
[ValidateHmacSignature] // BOOM. Replay attacks are dead.
public IActionResult TransferFunds([FromBody] TransferDto request)
{
// Process the transaction safely
return Ok(new { Message = "Funds transferred securely." });
}
Conclusion
By combining timestamps with HMAC signatures, we ensure two things:
The payload was not altered in transit.
The request is fresh and cannot be intercepted and replayed later.
If you are building fintech apps, ERP systems, or anything handling real money, this level of security is non-negotiable.
What other advanced security layers do you guys implement in your mobile architectures? Let's discuss in the comments!
(P.S. Make sure to follow my profile for more deep dives into enterprise-grade .NET and React Native architecture!)
Top comments (0)