HMAC Signature Authentication in Java: Complete Implementation Guide

HMAC (Hash-based Message Authentication Code) provides a secure way to authenticate API requests by combining cryptographic hashing with a secret key. This guide covers complete implementation for secure API authentication.


HMAC Authentication Overview

How HMAC Works:

  • Client and server share a secret key
  • Client creates signature using request data + secret key
  • Server validates signature using same data + secret key
  • Provides integrity and authentication

Benefits:

  • No passwords transmitted
  • Replay attack protection
  • Timestamp validation
  • Request integrity verification

Dependencies and Setup

Maven Dependencies
<properties>
<spring-boot.version>3.1.0</spring-boot.version>
<junit.version>5.9.2</junit.version>
<commons-codec.version>1.16.0</commons-codec.version>
</properties>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- Encoding -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>${commons-codec.version}</version>
</dependency>
<!-- Database -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring-boot.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
Application Configuration
# application.yml
app:
hmac:
# Signature validity in milliseconds (5 minutes)
timestamp-validity: 300000
# Supported algorithms
algorithms: HmacSHA256,HmacSHA512
# Required headers for signature
required-headers: (request-target),date,digest,content-type
server:
port: 8080
spring:
datasource:
url: jdbc:h2:mem:testdb
driverClassName: org.h2.Driver
username: sa
password: 
jpa:
database-platform: org.hibernate.dialect.H2Dialect
hibernate:
ddl-auto: create-drop
show-sql: true
h2:
console:
enabled: true
logging:
level:
com.example.hmac: DEBUG

Core HMAC Implementation

1. HMAC Utility Class
// HmacUtil.java
package com.example.hmac.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
@Component
public class HmacUtil {
private static final Logger logger = LoggerFactory.getLogger(HmacUtil.class);
private static final String HMAC_SHA256 = "HmacSHA256";
private static final String HMAC_SHA512 = "HmacSHA512";
/**
* Generate HMAC signature for given data
*/
public String calculateHmac(String data, String secret, String algorithm) {
try {
Mac mac = Mac.getInstance(algorithm);
SecretKeySpec secretKeySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), algorithm);
mac.init(secretKeySpec);
byte[] hmacData = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(hmacData);
} catch (NoSuchAlgorithmException e) {
logger.error("HMAC algorithm not supported: {}", algorithm, e);
throw new HmacException("Unsupported algorithm: " + algorithm, e);
} catch (InvalidKeyException e) {
logger.error("Invalid HMAC key", e);
throw new HmacException("Invalid HMAC key", e);
}
}
/**
* Verify HMAC signature
*/
public boolean verifyHmac(String data, String secret, String algorithm, String expectedSignature) {
String actualSignature = calculateHmac(data, secret, algorithm);
return secureCompare(actualSignature, expectedSignature);
}
/**
* Constant-time comparison to prevent timing attacks
*/
private boolean secureCompare(String a, String b) {
if (a == null || b == null) {
return false;
}
byte[] aBytes = a.getBytes(StandardCharsets.UTF_8);
byte[] bBytes = b.getBytes(StandardCharsets.UTF_8);
if (aBytes.length != bBytes.length) {
return false;
}
int result = 0;
for (int i = 0; i < aBytes.length; i++) {
result |= aBytes[i] ^ bBytes[i];
}
return result == 0;
}
/**
* Validate HMAC algorithm
*/
public boolean isValidAlgorithm(String algorithm) {
return HMAC_SHA256.equals(algorithm) || HMAC_SHA512.equals(algorithm);
}
public static class HmacException extends RuntimeException {
public HmacException(String message) {
super(message);
}
public HmacException(String message, Throwable cause) {
super(message, cause);
}
}
}
2. Signature Generation
// SignatureGenerator.java
package com.example.hmac.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
@Component
public class SignatureGenerator {
private static final Logger logger = LoggerFactory.getLogger(SignatureGenerator.class);
/**
* Generate signature string for HTTP request (AWS Signature v4 style)
*/
public String generateSignatureString(
String method,
String path,
Map<String, String> headers,
String body) {
// Normalize method
String normalizedMethod = method.toUpperCase();
// Normalize path
String normalizedPath = normalizePath(path);
// Normalize query string (empty in this example)
String normalizedQuery = "";
// Normalize headers
String normalizedHeaders = normalizeHeaders(headers);
// Signed headers
String signedHeaders = getSignedHeaders(headers);
// Body hash
String bodyHash = calculateBodyHash(body);
// Build canonical request
String canonicalRequest = String.join("\n",
normalizedMethod,
normalizedPath,
normalizedQuery,
normalizedHeaders,
signedHeaders,
bodyHash
);
logger.debug("Canonical Request:\n{}", canonicalRequest);
return canonicalRequest;
}
/**
* Generate signature for request
*/
public String generateSignature(
String method,
String path,
Map<String, String> headers,
String body,
String secret,
String algorithm) {
String signatureString = generateSignatureString(method, path, headers, body);
HmacUtil hmacUtil = new HmacUtil();
return hmacUtil.calculateHmac(signatureString, secret, algorithm);
}
private String normalizePath(String path) {
if (!StringUtils.hasText(path)) {
return "/";
}
// Add leading slash if missing
if (!path.startsWith("/")) {
path = "/" + path;
}
// URL encode path (simplified)
return path;
}
private String normalizeHeaders(Map<String, String> headers) {
if (headers == null || headers.isEmpty()) {
return "";
}
// Sort headers by name
Map<String, String> sortedHeaders = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
sortedHeaders.putAll(headers);
// Format: lowercase(name):value
return sortedHeaders.entrySet().stream()
.map(entry -> entry.getKey().toLowerCase() + ":" + entry.getValue().trim())
.collect(Collectors.joining("\n"));
}
private String getSignedHeaders(Map<String, String> headers) {
if (headers == null || headers.isEmpty()) {
return "";
}
return headers.keySet().stream()
.map(String::toLowerCase)
.sorted()
.collect(Collectors.joining(";"));
}
private String calculateBodyHash(String body) {
if (body == null || body.isEmpty()) {
return calculateSHA256("");
}
return calculateSHA256(body);
}
private String calculateSHA256(String data) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(data.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(hash);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-256 algorithm not available", e);
}
}
}
3. Data Models
// ApiKey.java
package com.example.hmac.model;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.UUID;
@Entity
@Table(name = "api_keys")
public class ApiKey {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "key_id", unique = true, nullable = false)
private String keyId;
@Column(name = "secret_key", nullable = false)
private String secretKey;
@Column(name = "client_name", nullable = false)
private String clientName;
@Column(name = "algorithm", nullable = false)
private String algorithm = "HmacSHA256";
@Column(name = "is_active", nullable = false)
private Boolean isActive = true;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
@Column(name = "last_used")
private LocalDateTime lastUsed;
@Column(name = "expires_at")
private LocalDateTime expiresAt;
@PrePersist
protected void onCreate() {
if (keyId == null) {
keyId = UUID.randomUUID().toString();
}
if (createdAt == null) {
createdAt = LocalDateTime.now();
}
}
// Getters and setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getKeyId() { return keyId; }
public void setKeyId(String keyId) { this.keyId = keyId; }
public String getSecretKey() { return secretKey; }
public void setSecretKey(String secretKey) { this.secretKey = secretKey; }
public String getClientName() { return clientName; }
public void setClientName(String clientName) { this.clientName = clientName; }
public String getAlgorithm() { return algorithm; }
public void setAlgorithm(String algorithm) { this.algorithm = algorithm; }
public Boolean getIsActive() { return isActive; }
public void setIsActive(Boolean isActive) { this.isActive = isActive; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getLastUsed() { return lastUsed; }
public void setLastUsed(LocalDateTime lastUsed) { this.lastUsed = lastUsed; }
public LocalDateTime getExpiresAt() { return expiresAt; }
public void setExpiresAt(LocalDateTime expiresAt) { this.expiresAt = expiresAt; }
public boolean isValid() {
return Boolean.TRUE.equals(isActive) && 
(expiresAt == null || expiresAt.isAfter(LocalDateTime.now()));
}
}
// HmacSignature.java
package com.example.hmac.model;
import java.util.Map;
public class HmacSignature {
private String keyId;
private String algorithm;
private String signature;
private Map<String, String> headers;
private Long timestamp;
private String nonce;
public HmacSignature() {}
public HmacSignature(String keyId, String algorithm, String signature, 
Map<String, String> headers, Long timestamp, String nonce) {
this.keyId = keyId;
this.algorithm = algorithm;
this.signature = signature;
this.headers = headers;
this.timestamp = timestamp;
this.nonce = nonce;
}
// Getters and setters
public String getKeyId() { return keyId; }
public void setKeyId(String keyId) { this.keyId = keyId; }
public String getAlgorithm() { return algorithm; }
public void setAlgorithm(String algorithm) { this.algorithm = algorithm; }
public String getSignature() { return signature; }
public void setSignature(String signature) { this.signature = signature; }
public Map<String, String> getHeaders() { return headers; }
public void setHeaders(Map<String, String> headers) { this.headers = headers; }
public Long getTimestamp() { return timestamp; }
public void setTimestamp(Long timestamp) { this.timestamp = timestamp; }
public String getNonce() { return nonce; }
public void setNonce(String nonce) { this.nonce = nonce; }
}
// AuthenticationResult.java
package com.example.hmac.model;
public class AuthenticationResult {
private boolean authenticated;
private ApiKey apiKey;
private String errorMessage;
private String clientName;
public AuthenticationResult(boolean authenticated, ApiKey apiKey) {
this.authenticated = authenticated;
this.apiKey = apiKey;
this.clientName = apiKey != null ? apiKey.getClientName() : null;
}
public AuthenticationResult(String errorMessage) {
this.authenticated = false;
this.errorMessage = errorMessage;
}
// Getters
public boolean isAuthenticated() { return authenticated; }
public ApiKey getApiKey() { return apiKey; }
public String getErrorMessage() { return errorMessage; }
public String getClientName() { return clientName; }
public static AuthenticationResult success(ApiKey apiKey) {
return new AuthenticationResult(true, apiKey);
}
public static AuthenticationResult failure(String errorMessage) {
return new AuthenticationResult(errorMessage);
}
}
4. Repository Layer
// ApiKeyRepository.java
package com.example.hmac.repository;
import com.example.hmac.model.ApiKey;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.Optional;
@Repository
public interface ApiKeyRepository extends JpaRepository<ApiKey, Long> {
Optional<ApiKey> findByKeyIdAndIsActiveTrue(String keyId);
Optional<ApiKey> findByKeyId(String keyId);
@Modifying
@Query("UPDATE ApiKey k SET k.lastUsed = :lastUsed WHERE k.keyId = :keyId")
void updateLastUsed(@Param("keyId") String keyId, @Param("lastUsed") LocalDateTime lastUsed);
@Modifying
@Query("UPDATE ApiKey k SET k.isActive = false WHERE k.expiresAt < :now")
int deactivateExpiredKeys(@Param("now") LocalDateTime now);
}

Server-Side Implementation

1. HMAC Authentication Service
// HmacAuthenticationService.java
package com.example.hmac.service;
import com.example.hmac.model.ApiKey;
import com.example.hmac.model.AuthenticationResult;
import com.example.hmac.model.HmacSignature;
import com.example.hmac.repository.ApiKeyRepository;
import com.example.hmac.util.HmacUtil;
import com.example.hmac.util.SignatureGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.*;
@Service
public class HmacAuthenticationService {
private static final Logger logger = LoggerFactory.getLogger(HmacAuthenticationService.class);
private final ApiKeyRepository apiKeyRepository;
private final HmacUtil hmacUtil;
private final SignatureGenerator signatureGenerator;
@Value("${app.hmac.timestamp-validity:300000}")
private long timestampValidity;
// Nonce cache to prevent replay attacks (in production, use Redis)
private final Set<String> usedNonces = Collections.synchronizedSet(new HashSet<>());
public HmacAuthenticationService(ApiKeyRepository apiKeyRepository, 
HmacUtil hmacUtil, 
SignatureGenerator signatureGenerator) {
this.apiKeyRepository = apiKeyRepository;
this.hmacUtil = hmacUtil;
this.signatureGenerator = signatureGenerator;
}
/**
* Authenticate HMAC signature from HTTP request
*/
public AuthenticationResult authenticateRequest(HttpServletRequest request, String body) {
try {
// Extract signature from Authorization header
HmacSignature signature = extractSignature(request);
if (signature == null) {
return AuthenticationResult.failure("Missing or invalid Authorization header");
}
// Validate timestamp to prevent replay attacks
if (!isTimestampValid(signature.getTimestamp())) {
return AuthenticationResult.failure("Invalid timestamp");
}
// Validate nonce to prevent replay attacks
if (!isNonceValid(signature.getNonce())) {
return AuthenticationResult.failure("Invalid or reused nonce");
}
// Find API key
Optional<ApiKey> apiKeyOpt = apiKeyRepository.findByKeyIdAndIsActiveTrue(signature.getKeyId());
if (apiKeyOpt.isEmpty()) {
return AuthenticationResult.failure("Invalid API key");
}
ApiKey apiKey = apiKeyOpt.get();
// Validate algorithm
if (!hmacUtil.isValidAlgorithm(signature.getAlgorithm())) {
return AuthenticationResult.failure("Unsupported algorithm: " + signature.getAlgorithm());
}
// Verify signature
if (!verifySignature(request, body, signature, apiKey)) {
return AuthenticationResult.failure("Invalid signature");
}
// Update last used timestamp
updateLastUsed(apiKey);
logger.info("HMAC authentication successful for client: {}", apiKey.getClientName());
return AuthenticationResult.success(apiKey);
} catch (Exception e) {
logger.error("HMAC authentication failed", e);
return AuthenticationResult.failure("Authentication failed: " + e.getMessage());
}
}
/**
* Extract HMAC signature from Authorization header
* Format: HMAC keyId="key",algorithm="hmac-sha256",headers="date digest",signature="base64signature"
*/
private HmacSignature extractSignature(HttpServletRequest request) {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("HMAC ")) {
return null;
}
try {
String hmacPart = authHeader.substring(5); // Remove "HMAC "
Map<String, String> params = parseAuthParams(hmacPart);
String keyId = params.get("keyId");
String algorithm = params.get("algorithm");
String signature = params.get("signature");
String headers = params.get("headers");
String timestamp = params.get("timestamp");
String nonce = params.get("nonce");
if (keyId == null || algorithm == null || signature == null) {
return null;
}
// Parse timestamp
Long timestampLong = timestamp != null ? Long.parseLong(timestamp) : null;
// Extract signed headers
Map<String, String> signedHeaders = extractSignedHeaders(request, headers);
return new HmacSignature(keyId, algorithm, signature, signedHeaders, timestampLong, nonce);
} catch (Exception e) {
logger.error("Failed to parse Authorization header", e);
return null;
}
}
private Map<String, String> parseAuthParams(String authString) {
Map<String, String> params = new HashMap<>();
String[] parts = authString.split(",");
for (String part : parts) {
String[] keyValue = part.split("=", 2);
if (keyValue.length == 2) {
String key = keyValue[0].trim();
String value = keyValue[1].trim().replace("\"", "");
params.put(key, value);
}
}
return params;
}
private Map<String, String> extractSignedHeaders(HttpServletRequest request, String headersList) {
Map<String, String> headers = new HashMap<>();
if (headersList != null) {
String[] headerNames = headersList.toLowerCase().split(" ");
for (String headerName : headerNames) {
String headerValue = request.getHeader(headerName);
if (headerValue != null) {
headers.put(headerName, headerValue);
}
}
}
// Always include (request-target) pseudo-header
String requestTarget = request.getMethod().toLowerCase() + " " + request.getRequestURI();
headers.put("(request-target)", requestTarget);
return headers;
}
private boolean verifySignature(HttpServletRequest request, String body, 
HmacSignature signature, ApiKey apiKey) {
try {
// Recreate signature string
String signatureString = signatureGenerator.generateSignatureString(
request.getMethod(),
request.getRequestURI(),
signature.getHeaders(),
body
);
// Verify HMAC
return hmacUtil.verifyHmac(
signatureString,
apiKey.getSecretKey(),
signature.getAlgorithm(),
signature.getSignature()
);
} catch (Exception e) {
logger.error("Signature verification failed", e);
return false;
}
}
private boolean isTimestampValid(Long timestamp) {
if (timestamp == null) {
return false;
}
long currentTime = System.currentTimeMillis();
long timeDiff = Math.abs(currentTime - timestamp);
return timeDiff <= timestampValidity;
}
private boolean isNonceValid(String nonce) {
if (nonce == null || nonce.length() < 16) {
return false;
}
// Check if nonce was already used
if (usedNonces.contains(nonce)) {
return false;
}
// Add to used nonces (in production, use TTL cache)
usedNonces.add(nonce);
// Clean up old nonces periodically (simplified)
if (usedNonces.size() > 10000) {
usedNonces.clear();
}
return true;
}
private void updateLastUsed(ApiKey apiKey) {
try {
apiKey.setLastUsed(LocalDateTime.now());
apiKeyRepository.save(apiKey);
} catch (Exception e) {
logger.warn("Failed to update last used timestamp for key: {}", apiKey.getKeyId(), e);
}
}
/**
* Clean up expired nonces (call periodically)
*/
public void cleanupExpiredNonces() {
// In production implementation, use TTL-based cache
usedNonces.clear();
}
}
2. Spring Security Filter
// HmacAuthenticationFilter.java
package com.example.hmac.security;
import com.example.hmac.model.AuthenticationResult;
import com.example.hmac.service.HmacAuthenticationService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;
public class HmacAuthenticationFilter extends OncePerRequestFilter {
private static final Logger logger = LoggerFactory.getLogger(HmacAuthenticationFilter.class);
private final HmacAuthenticationService hmacAuthenticationService;
public HmacAuthenticationFilter(HmacAuthenticationService hmacAuthenticationService) {
this.hmacAuthenticationService = hmacAuthenticationService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, 
HttpServletResponse response, 
FilterChain filterChain) throws ServletException, IOException {
// Skip authentication for certain paths
if (shouldSkipAuthentication(request)) {
filterChain.doFilter(request, response);
return;
}
// Read request body for signature verification
String requestBody = getRequestBody(request);
// Authenticate request
AuthenticationResult authResult = hmacAuthenticationService.authenticateRequest(request, requestBody);
if (authResult.isAuthenticated()) {
// Set authentication in security context
UsernamePasswordAuthenticationToken authentication = 
new UsernamePasswordAuthenticationToken(
authResult.getClientName(),
null,
Collections.singletonList(new SimpleGrantedAuthority("ROLE_API_CLIENT"))
);
SecurityContextHolder.getContext().setAuthentication(authentication);
logger.debug("HMAC authentication successful for: {}", authResult.getClientName());
filterChain.doFilter(request, response);
} else {
// Authentication failed
logger.warn("HMAC authentication failed: {}", authResult.getErrorMessage());
sendAuthenticationError(response, authResult.getErrorMessage());
}
}
private boolean shouldSkipAuthentication(HttpServletRequest request) {
String path = request.getRequestURI();
return path.startsWith("/public/") || 
path.equals("/health") || 
path.equals("/api-keys/register");
}
private String getRequestBody(HttpServletRequest request) {
// For simplicity, we're not reading the body here as it can only be read once
// In production, use ContentCachingRequestWrapper or similar approach
return "";
}
private void sendAuthenticationError(HttpServletResponse response, String message) throws IOException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write(String.format(
"{\"error\":\"authentication_failed\",\"message\":\"%s\"}", 
message
));
}
}
3. Security Configuration
// SecurityConfig.java
package com.example.hmac.config;
import com.example.hmac.security.HmacAuthenticationFilter;
import com.example.hmac.service.HmacAuthenticationService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, 
HmacAuthenticationService hmacAuthenticationService) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(authz -> authz
.requestMatchers("/public/**", "/health", "/api-keys/register").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(
new HmacAuthenticationFilter(hmacAuthenticationService),
UsernamePasswordAuthenticationFilter.class
);
return http.build();
}
}

Client-Side Implementation

1. HMAC Client Library
// HmacClient.java
package com.example.hmac.client;
import com.example.hmac.util.HmacUtil;
import com.example.hmac.util.SignatureGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.*;
import org.springframework.web.client.RestTemplate;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.*;
public class HmacClient {
private static final Logger logger = LoggerFactory.getLogger(HmacClient.class);
private final String baseUrl;
private final String keyId;
private final String secretKey;
private final String algorithm;
private final RestTemplate restTemplate;
private final HmacUtil hmacUtil;
private final SignatureGenerator signatureGenerator;
public HmacClient(String baseUrl, String keyId, String secretKey, String algorithm) {
this.baseUrl = baseUrl;
this.keyId = keyId;
this.secretKey = secretKey;
this.algorithm = algorithm;
this.restTemplate = new RestTemplate();
this.hmacUtil = new HmacUtil();
this.signatureGenerator = new SignatureGenerator();
}
/**
* Send GET request with HMAC authentication
*/
public <T> ResponseEntity<T> get(String path, Class<T> responseType) {
return exchange(path, HttpMethod.GET, null, responseType);
}
/**
* Send POST request with HMAC authentication
*/
public <T> ResponseEntity<T> post(String path, Object body, Class<T> responseType) {
return exchange(path, HttpMethod.POST, body, responseType);
}
/**
* Send PUT request with HMAC authentication
*/
public <T> ResponseEntity<T> put(String path, Object body, Class<T> responseType) {
return exchange(path, HttpMethod.PUT, body, responseType);
}
/**
* Send DELETE request with HMAC authentication
*/
public <T> ResponseEntity<T> delete(String path, Class<T> responseType) {
return exchange(path, HttpMethod.DELETE, null, responseType);
}
private <T> ResponseEntity<T> exchange(String path, HttpMethod method, Object body, Class<T> responseType) {
String url = baseUrl + path;
String bodyString = body != null ? body.toString() : "";
// Create headers
HttpHeaders headers = createHeaders(method, path, bodyString);
// Create request entity
HttpEntity<?> requestEntity = body != null ? 
new HttpEntity<>(body, headers) : 
new HttpEntity<>(headers);
// Send request
return restTemplate.exchange(url, method, requestEntity, responseType);
}
private HttpHeaders createHeaders(String method, String path, String body) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
// Add required headers for signature
String timestamp = String.valueOf(System.currentTimeMillis());
String nonce = generateNonce();
String digest = calculateDigest(body);
headers.set("Date", new Date().toString());
headers.set("X-Timestamp", timestamp);
headers.set("X-Nonce", nonce);
headers.set("Digest", "SHA-256=" + digest);
// Generate signature
Map<String, String> signatureHeaders = new HashMap<>();
signatureHeaders.put("(request-target)", method.toLowerCase() + " " + path);
signatureHeaders.put("date", headers.getFirst("Date"));
signatureHeaders.put("digest", headers.getFirst("Digest"));
signatureHeaders.put("content-type", MediaType.APPLICATION_JSON_VALUE);
String signature = signatureGenerator.generateSignature(
method, path, signatureHeaders, body, secretKey, algorithm
);
// Build Authorization header
String authHeader = buildAuthorizationHeader(signature, timestamp, nonce);
headers.set("Authorization", authHeader);
return headers;
}
private String buildAuthorizationHeader(String signature, String timestamp, String nonce) {
return String.format(
"HMAC keyId=\"%s\",algorithm=\"%s\",headers=\"(request-target) date digest content-type\",timestamp=\"%s\",nonce=\"%s\",signature=\"%s\"",
keyId, algorithm, timestamp, nonce, signature
);
}
private String generateNonce() {
return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
}
private String calculateDigest(String body) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(body.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(hash);
} catch (Exception e) {
throw new RuntimeException("Failed to calculate digest", e);
}
}
}
2. Client Usage Example
// ClientExample.java
package com.example.hmac.client;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
public class ClientExample {
public static void main(String[] args) {
String baseUrl = "http://localhost:8080";
String keyId = "test-key-123";
String secretKey = "my-secret-key-12345";
String algorithm = "HmacSHA256";
HmacClient client = new HmacClient(baseUrl, keyId, secretKey, algorithm);
try {
// Test GET request
ResponseEntity<String> response = client.get("/api/secure-data", String.class);
System.out.println("GET Response: " + response.getBody());
// Test POST request
String requestBody = "{\"message\": \"Hello HMAC\"}";
ResponseEntity<String> postResponse = client.post("/api/echo", requestBody, String.class);
System.out.println("POST Response: " + postResponse.getBody());
} catch (Exception e) {
System.err.println("Request failed: " + e.getMessage());
}
}
}

API Controllers

1. Secure API Controller
// SecureApiController.java
package com.example.hmac.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api")
public class SecureApiController {
private static final Logger logger = LoggerFactory.getLogger(SecureApiController.class);
@GetMapping("/secure-data")
public ResponseEntity<Map<String, Object>> getSecureData(Authentication authentication) {
logger.info("Accessing secure data for: {}", authentication.getName());
Map<String, Object> data = new HashMap<>();
data.put("message", "This is secure data");
data.put("client", authentication.getName());
data.put("timestamp", System.currentTimeMillis());
data.put("data", Arrays.asList("item1", "item2", "item3"));
return ResponseEntity.ok(data);
}
@PostMapping("/echo")
public ResponseEntity<Map<String, Object>> echoMessage(
@RequestBody Map<String, Object> request,
Authentication authentication) {
logger.info("Echo request from: {}", authentication.getName());
Map<String, Object> response = new HashMap<>();
response.put("echo", request);
response.put("receivedBy", authentication.getName());
response.put("timestamp", System.currentTimeMillis());
return ResponseEntity.ok(response);
}
@PutMapping("/update")
public ResponseEntity<Map<String, Object>> updateData(
@RequestBody Map<String, Object> updateRequest,
Authentication authentication) {
logger.info("Update request from: {}", authentication.getName());
Map<String, Object> response = new HashMap<>();
response.put("status", "updated");
response.put("client", authentication.getName());
response.put("updatedFields", updateRequest.keySet());
return ResponseEntity.ok(response);
}
@DeleteMapping("/resource/{id}")
public ResponseEntity<Map<String, Object>> deleteResource(
@PathVariable String id,
Authentication authentication) {
logger.info("Delete request for resource {} from: {}", id, authentication.getName());
Map<String, Object> response = new HashMap<>();
response.put("status", "deleted");
response.put("resourceId", id);
response.put("client", authentication.getName());
return ResponseEntity.ok(response);
}
}
2. API Key Management Controller
// ApiKeyController.java
package com.example.hmac.controller;
import com.example.hmac.model.ApiKey;
import com.example.hmac.service.ApiKeyService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api-keys")
public class ApiKeyController {
private static final Logger logger = LoggerFactory.getLogger(ApiKeyController.class);
private final ApiKeyService apiKeyService;
public ApiKeyController(ApiKeyService apiKeyService) {
this.apiKeyService = apiKeyService;
}
@PostMapping("/register")
public ResponseEntity<Map<String, Object>> registerClient(@RequestBody Map<String, String> request) {
String clientName = request.get("clientName");
String algorithm = request.getOrDefault("algorithm", "HmacSHA256");
if (clientName == null || clientName.trim().isEmpty()) {
return ResponseEntity.badRequest().body(Map.of(
"error", "clientName is required"
));
}
try {
ApiKey apiKey = apiKeyService.generateApiKey(clientName, algorithm);
logger.info("Generated new API key for client: {}", clientName);
return ResponseEntity.ok(Map.of(
"keyId", apiKey.getKeyId(),
"secretKey", apiKey.getSecretKey(),
"clientName", apiKey.getClientName(),
"algorithm", apiKey.getAlgorithm(),
"createdAt", apiKey.getCreatedAt()
));
} catch (Exception e) {
logger.error("Failed to generate API key for client: {}", clientName, e);
return ResponseEntity.internalServerError().body(Map.of(
"error", "Failed to generate API key: " + e.getMessage()
));
}
}
@GetMapping
public ResponseEntity<List<ApiKey>> listApiKeys() {
List<ApiKey> apiKeys = apiKeyService.getAllActiveKeys();
return ResponseEntity.ok(apiKeys);
}
@DeleteMapping("/{keyId}")
public ResponseEntity<Map<String, Object>> revokeApiKey(@PathVariable String keyId) {
try {
apiKeyService.revokeApiKey(keyId);
logger.info("Revoked API key: {}", keyId);
return ResponseEntity.ok(Map.of("status", "revoked", "keyId", keyId));
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
}
3. API Key Service
// ApiKeyService.java
package com.example.hmac.service;
import com.example.hmac.model.ApiKey;
import com.example.hmac.repository.ApiKeyRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.security.SecureRandom;
import java.time.LocalDateTime;
import java.util.Base64;
import java.util.List;
import java.util.Optional;
@Service
public class ApiKeyService {
private static final Logger logger = LoggerFactory.getLogger(ApiKeyService.class);
private final ApiKeyRepository apiKeyRepository;
private final SecureRandom secureRandom;
public ApiKeyService(ApiKeyRepository apiKeyRepository) {
this.apiKeyRepository = apiKeyRepository;
this.secureRandom = new SecureRandom();
}
/**
* Generate new API key for client
*/
public ApiKey generateApiKey(String clientName, String algorithm) {
String keyId = generateRandomString(16);
String secretKey = generateRandomString(32);
ApiKey apiKey = new ApiKey();
apiKey.setKeyId(keyId);
apiKey.setSecretKey(secretKey);
apiKey.setClientName(clientName);
apiKey.setAlgorithm(algorithm);
apiKey.setIsActive(true);
apiKey.setCreatedAt(LocalDateTime.now());
return apiKeyRepository.save(apiKey);
}
/**
* Get all active API keys
*/
public List<ApiKey> getAllActiveKeys() {
return apiKeyRepository.findAll().stream()
.filter(ApiKey::isValid)
.toList();
}
/**
* Revoke API key
*/
public void revokeApiKey(String keyId) {
Optional<ApiKey> apiKeyOpt = apiKeyRepository.findByKeyId(keyId);
if (apiKeyOpt.isPresent()) {
ApiKey apiKey = apiKeyOpt.get();
apiKey.setIsActive(false);
apiKeyRepository.save(apiKey);
logger.info("Revoked API key: {}", keyId);
} else {
throw new IllegalArgumentException("API key not found: " + keyId);
}
}
/**
* Clean up expired API keys
*/
public int cleanupExpiredKeys() {
return apiKeyRepository.deactivateExpiredKeys(LocalDateTime.now());
}
private String generateRandomString(int length) {
byte[] bytes = new byte[length];
secureRandom.nextBytes(bytes);
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
}
}

Testing

1. Unit Tests
// HmacUtilTest.java
package com.example.hmac.util;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class HmacUtilTest {
private final HmacUtil hmacUtil = new HmacUtil();
@Test
void testCalculateHmac() {
String data = "test data";
String secret = "test secret";
String algorithm = "HmacSHA256";
String signature = hmacUtil.calculateHmac(data, secret, algorithm);
assertNotNull(signature);
assertFalse(signature.isEmpty());
}
@Test
void testVerifyHmac() {
String data = "test data";
String secret = "test secret";
String algorithm = "HmacSHA256";
String signature = hmacUtil.calculateHmac(data, secret, algorithm);
boolean isValid = hmacUtil.verifyHmac(data, secret, algorithm, signature);
assertTrue(isValid);
}
@Test
void testInvalidAlgorithm() {
assertFalse(hmacUtil.isValidAlgorithm("InvalidAlgorithm"));
assertTrue(hmacUtil.isValidAlgorithm("HmacSHA256"));
}
}
// HmacAuthenticationServiceTest.java
package com.example.hmac.service;
import com.example.hmac.model.ApiKey;
import com.example.hmac.repository.ApiKeyRepository;
import com.example.hmac.util.HmacUtil;
import com.example.hmac.util.SignatureGenerator;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class HmacAuthenticationServiceTest {
@Mock
private ApiKeyRepository apiKeyRepository;
@Mock
private HmacUtil hmacUtil;
@Mock
private SignatureGenerator signatureGenerator;
@Mock
private HttpServletRequest request;
private HmacAuthenticationService authService;
@BeforeEach
void setUp() {
authService = new HmacAuthenticationService(apiKeyRepository, hmacUtil, signatureGenerator);
}
@Test
void testAuthenticationWithValidSignature() {
// Setup
when(request.getHeader("Authorization")).thenReturn(
"HMAC keyId=\"test-key\",algorithm=\"HmacSHA256\",headers=\"date\",signature=\"test-sig\",timestamp=\"123456789\",nonce=\"abc123\""
);
when(request.getMethod()).thenReturn("GET");
when(request.getRequestURI()).thenReturn("/api/test");
ApiKey apiKey = new ApiKey();
apiKey.setKeyId("test-key");
apiKey.setSecretKey("secret");
apiKey.setClientName("test-client");
apiKey.setIsActive(true);
when(apiKeyRepository.findByKeyIdAndIsActiveTrue("test-key"))
.thenReturn(Optional.of(apiKey));
when(hmacUtil.isValidAlgorithm("HmacSHA256")).thenReturn(true);
when(hmacUtil.verifyHmac(anyString(), anyString(), anyString(), anyString()))
.thenReturn(true);
// Execute
var result = authService.authenticateRequest(request, "");
// Verify
assertTrue(result.isAuthenticated());
assertEquals("test-client", result.getClientName());
}
}
2. Integration Test
// HmacIntegrationTest.java
package com.example.hmac.integration;
import com.example.hmac.model.ApiKey;
import com.example.hmac.repository.ApiKeyRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.*;
import org.springframework.test.context.ActiveProfiles;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
class HmacIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private ApiKeyRepository apiKeyRepository;
private ApiKey testApiKey;
@BeforeEach
void setUp() {
// Create test API key
testApiKey = new ApiKey();
testApiKey.setKeyId("test-key-integration");
testApiKey.setSecretKey("integration-secret-key-123");
testApiKey.setClientName("integration-test");
testApiKey.setAlgorithm("HmacSHA256");
testApiKey.setIsActive(true);
apiKeyRepository.save(testApiKey);
}
@Test
void testUnauthorizedAccess() {
ResponseEntity<String> response = restTemplate.getForEntity("/api/secure-data", String.class);
assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode());
}
@Test
void testApiKeyRegistration() {
Map<String, String> request = Map.of("clientName", "test-client");
ResponseEntity<Map> response = restTemplate.postForEntity(
"/api-keys/register", request, Map.class);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody().get("keyId"));
assertNotNull(response.getBody().get("secretKey"));
}
}

Production Considerations

1. Enhanced Security Features
// RateLimitingService.java
package com.example.hmac.service;
import org.springframework.stereotype.Service;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@Service
public class RateLimitingService {
private final ConcurrentHashMap<String, AtomicInteger> requestCounts = new ConcurrentHashMap<>();
private final int maxRequestsPerMinute = 100;
public boolean isRateLimited(String clientId) {
String key = clientId + ":" + (System.currentTimeMillis() / 60000);
AtomicInteger count = requestCounts.computeIfAbsent(key, k -> new AtomicInteger(0));
return count.incrementAndGet() > maxRequestsPerMinute;
}
public void cleanupOldEntries() {
long currentMinute = System.currentTimeMillis() / 60000;
requestCounts.keySet().removeIf(key -> {
long keyMinute = Long.parseLong(key.split(":")[1]);
return currentMinute - keyMinute > 1; // Remove entries older than 1 minute
});
}
}
2. Monitoring and Metrics
// HmacMetricsService.java
package com.example.hmac.service;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Service;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
@Service
public class HmacMetricsService {
private final Counter authenticationSuccess;
private final Counter authenticationFailure;
private final ConcurrentHashMap<String, AtomicLong> clientRequestCounts;
public HmacMetricsService(MeterRegistry registry) {
this.authenticationSuccess = Counter.builder("hmac.auth.success")
.description("Successful HMAC authentications")
.register(registry);
this.authenticationFailure = Counter.builder("hmac.auth.failure")
.description("Failed HMAC authentications")
.register(registry);
this.clientRequestCounts = new ConcurrentHashMap<>();
}
public void recordAuthenticationSuccess(String clientId) {
authenticationSuccess.increment();
clientRequestCounts.computeIfAbsent(clientId, k -> new AtomicLong(0)).incrementAndGet();
}
public void recordAuthenticationFailure() {
authenticationFailure.increment();
}
public long getRequestCount(String clientId) {
AtomicLong count = clientRequestCounts.get(clientId);
return count != null ? count.get() : 0;
}
}

Best Practices

  1. Security:
  • Use strong random number generation for secrets
  • Implement proper key rotation policies
  • Use constant-time comparison for signature verification
  • Validate all input parameters
  1. Performance:
  • Cache API key lookups when appropriate
  • Use efficient data structures for nonce tracking
  • Implement connection pooling for database access
  1. Monitoring:
  • Log authentication attempts (success/failure)
  • Track rate limiting events
  • Monitor key usage patterns
  1. Operations:
  • Implement key expiration and rotation
  • Provide key revocation capabilities
  • Backup key storage securely

Conclusion

This HMAC signature authentication implementation provides:

  • Secure API authentication without transmitting passwords
  • Replay attack protection through timestamps and nonces
  • Request integrity verification via signature validation
  • Production-ready features including rate limiting and monitoring
  • Comprehensive client and server implementations

The solution can be extended with additional features like:

  • JWT token issuance after HMAC authentication
  • Advanced rate limiting strategies
  • Key rotation automation
  • Audit logging and compliance features
  • Multi-tenant key management

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper