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
- Security:
- Use strong random number generation for secrets
- Implement proper key rotation policies
- Use constant-time comparison for signature verification
- Validate all input parameters
- Performance:
- Cache API key lookups when appropriate
- Use efficient data structures for nonce tracking
- Implement connection pooling for database access
- Monitoring:
- Log authentication attempts (success/failure)
- Track rate limiting events
- Monitor key usage patterns
- 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