How to Strengthen Public API Security with HMAC
Mani Bhushan Shukla
Technology-neutral Architect experienced in Product Engineering, C#, Azure, AWS, Microservices, and Multitenant Applications. Specializes in Scalability, Digital Innovation, Cyber Security, and AI & ML.
Hash-based Message Authentication Code (HMAC) is a cryptographic method that ensures both the integrity and authenticity of a message by combining a secret key with a hash function. HMAC is widely used in API communication for authentication and message integrity. It prevents attackers from tampering with messages and allows servers to verify that requests come from authorized clients.
In a multi-client environment, each client has a unique secret key that is used to generate a secure HMAC signature. The server verifies this signature to confirm the request's authenticity. HMAC is an efficient and secure method that does not require complex setups like OAuth, making it ideal for API-to-API communication, secure data transfer, and webhook verification.
Part 1: Overview of HMAC and Its Use Cases
What is HMAC?
HMAC works by using a shared secret key and hashing the combination of this key and the message being sent. The generated HMAC signature ensures two things:
HMAC has the following properties:
Common Use Cases for HMAC
Part 2: Workflow for Authenticating Multiple Clients with HMAC
In an API-to-API communication setup with multiple clients, each client has its own Client ID and secret key. HMAC ensures that each client can be securely authenticated without using external authentication providers like OAuth.
Step 1: Client Sends Request with HMAC Signature and Client Identifier
Example Request (TypeScript):
const clientId = 'client-1234'; // Client identifier
const secretKey = 'your-client-specific-secret-key'; // Client's secret key
const message = `${apiEndpoint}+${requestBody}+${timestamp}`;
// Generate HMAC signature
import { createHmac } from 'crypto';
const hmac = createHmac('sha256', secretKey);
const signature = hmac.update(message).digest('hex');
// Send the request with HMAC signature and Client ID
const headers = {
'X-Client-ID': clientId,
'Authorization': `HMAC signature=${signature}`,
'Content-Type': 'application/json',
};
// Example API request
const response = await axios.post('https://your-api.com/resource', requestBody, { headers });
Step 2: Server Retrieves the Secret Key for the Client
Example (TypeScript):
// Mock function to retrieve secret key based on client ID
const getSecretKeyForClient = (clientId: string): string => {
const clientSecrets = {
'client-1234': 'client-1234-secret-key',
'client-5678': 'client-5678-secret-key',
};
return clientSecrets[clientId] || '';
};
const clientId = req.headers['x-client-id'];
const secretKey = getSecretKeyForClient(clientId);
if (!secretKey) {
return res.status(401).json({ error: 'Invalid client ID' });
}
Step 3: Server Verifies the HMAC Signature
Example (TypeScript):
// Extract received HMAC signature
const receivedSignature = req.headers['authorization'].split('signature=')[1];
// Recreate the HMAC signature using the secret key and message
const recreatedHmac = createHmac('sha256', secretKey);
const recreatedSignature = recreatedHmac.update(message).digest('hex');
// Verify if the signatures match
if (recreatedSignature === receivedSignature) {
console.log('Signature is valid. Client is authenticated.');
} else {
console.log('Invalid signature. Request denied.');
}
领英推荐
Part 3: Enhancing Security with Timestamps and Nonces
Using only HMAC for authentication can leave your system vulnerable to replay attacks, where a malicious user resends a previously valid request. To mitigate this, you can enhance the security of your API by adding timestamps and nonces to each request.
Step 1: Include Timestamps in the HMAC Message
A timestamp ensures that the request is time-bound, preventing attackers from using it after a certain period. The client should add a timestamp to the message when generating the HMAC signature.
Example (TypeScript):
const timestamp = Date.now();
const message = `${apiEndpoint}+${requestBody}+${timestamp}`;
// Generate HMAC signature
const hmac = createHmac('sha256', secretKey);
const signature = hmac.update(message).digest('hex');
// Send request with HMAC signature and timestamp
const headers = {
'X-Client-ID': clientId,
'Authorization': `HMAC signature=${signature}`,
'X-Timestamp': timestamp,
'Content-Type': 'application/json',
};
Step 2: Verify Timestamps on the Server
The server should check that the request timestamp is within an acceptable range (e.g., 5 minutes) to prevent replay attacks.
Example (TypeScript):
const receivedTimestamp = parseInt(req.headers['x-timestamp']);
const currentTime = Date.now();
const timeWindow = 5 * 60 * 1000; // 5 minutes in milliseconds
// Check if the request is within the allowed time window
if (Math.abs(currentTime - receivedTimestamp) > timeWindow) {
return res.status(403).json({ error: 'Request expired.' });
}
Step 3: Add a Nonce to the HMAC Message
A nonce is a unique value generated for each request. By storing used nonces on the server, you can prevent replay attacks from the same client, even if the timestamp is still valid.
Example (TypeScript):
const nonce = Math.random().toString(36).substring(2);
const message = `${apiEndpoint}+${requestBody}+${timestamp}+${nonce}`;
// Generate HMAC signature with nonce
const hmac = createHmac('sha256', secretKey);
const signature = hmac.update(message).digest('hex');
// Send request with HMAC signature, timestamp, and nonce
const headers = {
'X-Client-ID': clientId,
'Authorization': `HMAC signature=${signature}`,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'Content-Type': 'application/json',
};
Step 4: Verify the Nonce on the Server
The server should check if the nonce has been used before. If a request with the same nonce is received again, it should be rejected.
Example (TypeScript):
const receivedNonce = req.headers['x-nonce'];
// Mock database of used nonces
const usedNonces = new Set();
// Check if nonce has already been used
if (usedNonces.has(receivedNonce)) {
return res.status(403).json({ error: 'Replay attack detected.' });
}
// Store the nonce to prevent future reuse
usedNonces.add(receivedNonce);
Step 5: Clean Up Old Nonces
To prevent the nonce store from growing indefinitely, you should clean up nonces that are older than the time window (e.g., nonces older than 5 minutes).
Example (TypeScript):
const cleanUpOldNonces = () => {
const currentTime = Date.now();
usedNonces.forEach((timestamp, nonce) => {
if (currentTime - timestamp > timeWindow) {
usedNonces.delete(nonce);
}
});
};