google-site-verification: google61fe8ba583a51912.html
Comprehensive API Key Management in Java: Secure and Scalable Implementation

Introduction

API keys are essential for controlling access to APIs and services. Proper API key management ensures security, enables usage tracking, and maintains scalability. This guide explores how to implement robust API key management in Java applications, covering generation, validation, security, and lifecycle management.


Article: Building Enterprise-Grade API Key Management in Java

API key management involves creating, storing, validating, and revoking API keys while maintaining security and performance. Java provides excellent tools for building secure, scalable API key management systems.

1. API Key Architecture Overview

Key Components:

  • Key Generation - Secure random key creation
  • Key Storage - Secure storage with encryption
  • Key Validation - Authentication and authorization
  • Key Lifecycle - Creation, rotation, revocation
  • Rate Limiting - Usage control and quotas
  • Audit Logging - Usage tracking and monitoring

Security Considerations:

  • Encryption at rest - Secure key storage
  • HTTPS only - Prevent key interception
  • Key prefixes - Identify key source/type
  • Key expiration - Automatic key rotation
  • Rate limiting - Prevent abuse

2. Maven Dependencies

pom.xml:

<properties> <spring-boot.version>3.1.0</spring-boot.version> <jwt.version>0.11.5</jwt.version> <bouncycastle.version>1.76</bouncycastle.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-security</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> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Database --> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.6.0</version> </dependency> <!-- Security & Crypto --> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcpkix-jdk18on</artifactId> <version>${bouncycastle.version}</version> </dependency> <!-- Rate Limiting --> <dependency> <groupId>com.giffing.bucket4j.spring.boot.starter</groupId> <artifactId>bucket4j-spring-boot-starter</artifactId> <version>0.9.0</version> </dependency> <!-- Cache --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> <version>3.1.8</version> </dependency> <!-- Monitoring --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> <version>${spring-boot.version}</version> </dependency> </dependencies>

3. Application Configuration

application.yml:

spring: datasource: url: jdbc:postgresql://localhost:5432/apikeys username: ${DB_USERNAME:postgres} password: ${DB_PASSWORD:password} jpa: hibernate: ddl-auto: update show-sql: true properties: hibernate: dialect: org.hibernate.dialect.PostgreSQLDialect cache: type: caffeine caffeine: spec: maximumSize=10000,expireAfterWrite=3600s app: api-key: # Key Configuration prefix: "sk_" # Secret Key prefix public-prefix: "pk_" # Public Key prefix length: 32 # Key length in bytes encoding: BASE64 # BASE64 or HEX # Security hash-algorithm: "SHA-256" encryption-algorithm: "AES/GCM/NoPadding" encryption-key: ${API_KEY_ENCRYPTION_KEY:defaultEncryptionKey123} # Validation max-keys-per-user: 10 default-expiry-days: 365 rotation-warning-days: 30 # Rate Limiting rate-limiting: enabled: true default-requests-per-minute: 60 burst-capacity: 100 # Headers header-name: "X-API-Key" user-id-header: "X-API-User-Id" # Bucket4j Rate Limiting bucket4j: enabled: true filters: - cache-name: buckets url: .* rate-limits: - expression: "getHeader('X-API-Key')" bandwidths: - capacity: 100 time: 1 unit: minutes refill-speed: 60 # Management Endpoints management: endpoints: web: exposure: include: health,metrics,apikeys endpoint: apikeys: enabled: true

4. Domain Models

API Key Entity:

package com.myapp.apikey.model; import jakarta.persistence.*; import jakarta.validation.constraints.NotNull; import java.time.LocalDateTime; import java.util.HashSet; import java.util.Set; @Entity @Table(name = "api_keys", indexes = { @Index(name = "idx_api_key_hash", columnList = "keyHash"), @Index(name = "idx_api_key_user", columnList = "userId"), @Index(name = "idx_api_key_expiry", columnList = "expiresAt") }) public class ApiKey { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @NotNull @Column(unique = true) private String keyId; // Unique identifier for the key @NotNull private String keyHash; // Hashed version of the key for validation @NotNull private String userId; // Owner of the API key private String name; // Human-readable name for the key @ElementCollection(fetch = FetchType.EAGER) @CollectionTable(name = "api_key_permissions", joinColumns = @JoinColumn(name = "api_key_id")) @Column(name = "permission") private Set<String> permissions = new HashSet<>(); @Enumerated(EnumType.STRING) private ApiKeyStatus status = ApiKeyStatus.ACTIVE; @Enumerated(EnumType.STRING) private ApiKeyType type = ApiKeyType.SECRET; @Column(name = "created_at") private LocalDateTime createdAt; @Column(name = "expires_at") private LocalDateTime expiresAt; @Column(name = "last_used_at") private LocalDateTime lastUsedAt; private String createdBy; private String description; @Version private Long version; // Constructors public ApiKey() { this.createdAt = LocalDateTime.now(); } public ApiKey(String keyId, String keyHash, String userId, String name) { this(); this.keyId = keyId; this.keyHash = keyHash; this.userId = userId; this.name = name; } // 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 getKeyHash() { return keyHash; } public void setKeyHash(String keyHash) { this.keyHash = keyHash; } public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Set<String> getPermissions() { return permissions; } public void setPermissions(Set<String> permissions) { this.permissions = permissions; } public ApiKeyStatus getStatus() { return status; } public void setStatus(ApiKeyStatus status) { this.status = status; } public ApiKeyType getType() { return type; } public void setType(ApiKeyType type) { this.type = type; } public LocalDateTime getCreatedAt() { return createdAt; } public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } public LocalDateTime getExpiresAt() { return expiresAt; } public void setExpiresAt(LocalDateTime expiresAt) { this.expiresAt = expiresAt; } public LocalDateTime getLastUsedAt() { return lastUsedAt; } public void setLastUsedAt(LocalDateTime lastUsedAt) { this.lastUsedAt = lastUsedAt; } public String getCreatedBy() { return createdBy; } public void setCreatedBy(String createdBy) { this.createdBy = createdBy; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public Long getVersion() { return version; } public void setVersion(Long version) { this.version = version; } // Business methods public boolean isActive() { return status == ApiKeyStatus.ACTIVE && (expiresAt == null || expiresAt.isAfter(LocalDateTime.now())); } public boolean hasPermission(String permission) { return permissions.contains(permission) || permissions.contains("*"); } public boolean isExpired() { return expiresAt != null && expiresAt.isBefore(LocalDateTime.now()); } public boolean needsRotation() { return expiresAt != null && expiresAt.minusDays(30).isBefore(LocalDateTime.now()); } } enum ApiKeyStatus { ACTIVE, INACTIVE, REVOKED, EXPIRED } enum ApiKeyType { SECRET, PUBLIC, READ_ONLY, READ_WRITE, ADMIN }

API Key Usage Log:

package com.myapp.apikey.model; import jakarta.persistence.*; import java.time.LocalDateTime; @Entity @Table(name = "api_key_usage_logs", indexes = { @Index(name = "idx_usage_api_key", columnList = "apiKeyId"), @Index(name = "idx_usage_timestamp", columnList = "timestamp") }) public class ApiKeyUsageLog { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @NotNull private String apiKeyId; private String userId; private String endpoint; private String method; private String ipAddress; private String userAgent; private int statusCode; private long responseTimeMs; private long requestSize; private long responseSize; @Column(columnDefinition = "TEXT") private String requestHeaders; @Column(columnDefinition = "TEXT") private String responseHeaders; private LocalDateTime timestamp; private boolean success; // Constructors, getters, and setters public ApiKeyUsageLog() { this.timestamp = LocalDateTime.now(); } // Getters and setters... public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getApiKeyId() { return apiKeyId; } public void setApiKeyId(String apiKeyId) { this.apiKeyId = apiKeyId; } public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } public String getEndpoint() { return endpoint; } public void setEndpoint(String endpoint) { this.endpoint = endpoint; } public String getMethod() { return method; } public void setMethod(String method) { this.method = method; } public String getIpAddress() { return ipAddress; } public void setIpAddress(String ipAddress) { this.ipAddress = ipAddress; } public String getUserAgent() { return userAgent; } public void setUserAgent(String userAgent) { this.userAgent = userAgent; } public int getStatusCode() { return statusCode; } public void setStatusCode(int statusCode) { this.statusCode = statusCode; } public long getResponseTimeMs() { return responseTimeMs; } public void setResponseTimeMs(long responseTimeMs) { this.responseTimeMs = responseTimeMs; } public long getRequestSize() { return requestSize; } public void setRequestSize(long requestSize) { this.requestSize = requestSize; } public long getResponseSize() { return responseSize; } public void setResponseSize(long responseSize) { this.responseSize = responseSize; } public String getRequestHeaders() { return requestHeaders; } public void setRequestHeaders(String requestHeaders) { this.requestHeaders = requestHeaders; } public String getResponseHeaders() { return responseHeaders; } public void setResponseHeaders(String responseHeaders) { this.responseHeaders = responseHeaders; } public LocalDateTime getTimestamp() { return timestamp; } public void setTimestamp(LocalDateTime timestamp) { this.timestamp = timestamp; } public boolean isSuccess() { return success; } public void setSuccess(boolean success) { this.success = success; } }

5. API Key Generation Service

Key Generation Service:

package com.myapp.apikey.service; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.security.SecureRandom; import java.util.Base64; @Service public class KeyGenerationService { private final SecureRandom secureRandom = new SecureRandom(); @Value("${app.api-key.prefix:sk_}") private String keyPrefix; @Value("${app.api-key.public-prefix:pk_}") private String publicKeyPrefix; @Value("${app.api-key.length:32}") private int keyLength; @Value("${app.api-key.encoding:BASE64}") private String encoding; public GeneratedApiKey generateSecretKey() { return generateKey(keyPrefix, ApiKeyType.SECRET); } public GeneratedApiKey generatePublicKey() { return generateKey(publicKeyPrefix, ApiKeyType.PUBLIC); } public GeneratedApiKey generateKey(ApiKeyType type) { String prefix = type == ApiKeyType.PUBLIC ? publicKeyPrefix : keyPrefix; return generateKey(prefix, type); } private GeneratedApiKey generateKey(String prefix, ApiKeyType type) { String keyId = generateKeyId(); String rawKey = generateRandomBytes(); String fullKey = prefix + encodeKey(rawKey); return new GeneratedApiKey(keyId, fullKey, rawKey, type); } private String generateKeyId() { byte[] bytes = new byte[16]; secureRandom.nextBytes(bytes); return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); } private String generateRandomBytes() { byte[] bytes = new byte[keyLength]; secureRandom.nextBytes(bytes); return Base64.getEncoder().encodeToString(bytes); } private String encodeKey(String rawKey) { if ("HEX".equalsIgnoreCase(encoding)) { return bytesToHex(rawKey.getBytes()); } // Default to BASE64 return Base64.getUrlEncoder().withoutPadding().encodeToString(rawKey.getBytes()); } private String bytesToHex(byte[] bytes) { StringBuilder result = new StringBuilder(); for (byte b : bytes) { result.append(String.format("%02x", b)); } return result.toString(); } public static class GeneratedApiKey { private final String keyId; private final String fullKey; private final String rawKey; private final ApiKeyType type; public GeneratedApiKey(String keyId, String fullKey, String rawKey, ApiKeyType type) { this.keyId = keyId; this.fullKey = fullKey; this.rawKey = rawKey; this.type = type; } // Getters public String getKeyId() { return keyId; } public String getFullKey() { return fullKey; } public String getRawKey() { return rawKey; } public ApiKeyType getType() { return type; } } }

6. API Key Security Service

Security Service:

package com.myapp.apikey.service; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Base64; @Service public class KeySecurityService { @Value("${app.api-key.hash-algorithm:SHA-256}") private String hashAlgorithm; public String hashApiKey(String apiKey) { try { MessageDigest digest = MessageDigest.getInstance(hashAlgorithm); byte[] hash = digest.digest(apiKey.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(hash); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("Hash algorithm not available: " + hashAlgorithm, e); } } public boolean verifyApiKey(String providedKey, String storedHash) { String providedHash = hashApiKey(providedKey); return MessageDigest.isEqual( providedHash.getBytes(StandardCharsets.UTF_8), storedHash.getBytes(StandardCharsets.UTF_8) ); } public String extractKeyIdFromKey(String apiKey) { // For keys like "sk_abc123", extract "abc123" if (apiKey.startsWith("sk_")) { return apiKey.substring(3); } else if (apiKey.startsWith("pk_")) { return apiKey.substring(3); } return apiKey; // Fallback } public ApiKeyType detectKeyType(String apiKey) { if (apiKey.startsWith("sk_")) { return ApiKeyType.SECRET; } else if (apiKey.startsWith("pk_")) { return ApiKeyType.PUBLIC; } return ApiKeyType.SECRET; // Default } }

7. API Key Management Service

Management Service:

package com.myapp.apikey.service; import com.myapp.apikey.model.ApiKey; import com.myapp.apikey.model.ApiKeyStatus; import com.myapp.apikey.model.ApiKeyType; import com.myapp.apikey.model.ApiKeyUsageLog; import com.myapp.apikey.repository.ApiKeyRepository; import com.myapp.apikey.repository.ApiKeyUsageLogRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.List; import java.util.Optional; import java.util.Set; @Service public class ApiKeyManagementService { private static final Logger logger = LoggerFactory.getLogger(ApiKeyManagementService.class); private final ApiKeyRepository apiKeyRepository; private final ApiKeyUsageLogRepository usageLogRepository; private final KeyGenerationService keyGenerationService; private final KeySecurityService keySecurityService; @Value("${app.api-key.max-keys-per-user:10}") private int maxKeysPerUser; @Value("${app.api-key.default-expiry-days:365}") private int defaultExpiryDays; public ApiKeyManagementService(ApiKeyRepository apiKeyRepository, ApiKeyUsageLogRepository usageLogRepository, KeyGenerationService keyGenerationService, KeySecurityService keySecurityService) { this.apiKeyRepository = apiKeyRepository; this.usageLogRepository = usageLogRepository; this.keyGenerationService = keyGenerationService; this.keySecurityService = keySecurityService; } @Transactional public KeyGenerationService.GeneratedApiKey createApiKey(String userId, String name, Set<String> permissions, ApiKeyType type, LocalDateTime expiresAt) { // Check key limit long userKeyCount = apiKeyRepository.countByUserIdAndStatus(userId, ApiKeyStatus.ACTIVE); if (userKeyCount >= maxKeysPerUser) { throw new ApiKeyLimitExceededException( "User " + userId + " has reached the maximum number of API keys: " + maxKeysPerUser); } // Generate new key KeyGenerationService.GeneratedApiKey generatedKey = keyGenerationService.generateKey(type); // Create API key entity ApiKey apiKey = new ApiKey(); apiKey.setKeyId(generatedKey.getKeyId()); apiKey.setKeyHash(keySecurityService.hashApiKey(generatedKey.getRawKey())); apiKey.setUserId(userId); apiKey.setName(name); apiKey.setPermissions(permissions); apiKey.setType(type); apiKey.setStatus(ApiKeyStatus.ACTIVE); apiKey.setCreatedBy(userId); // Set expiration if (expiresAt == null) { expiresAt = LocalDateTime.now().plusDays(defaultExpiryDays); } apiKey.setExpiresAt(expiresAt); // Save to database apiKeyRepository.save(apiKey); logger.info("Created new API key for user: {}, keyId: {}", userId, generatedKey.getKeyId()); return generatedKey; } @Cacheable(value = "apiKeys", key = "#keyId") public Optional<ApiKey> validateApiKey(String apiKey) { String keyId = keySecurityService.extractKeyIdFromKey(apiKey); return apiKeyRepository.findByKeyId(keyId) .filter(storedKey -> keySecurityService.verifyApiKey(apiKey, storedKey.getKeyHash())) .filter(ApiKey::isActive) .map(this::updateLastUsed); } @Transactional public ApiKey updateLastUsed(ApiKey apiKey) { apiKey.setLastUsedAt(LocalDateTime.now()); return apiKeyRepository.save(apiKey); } public List<ApiKey> getUserApiKeys(String userId) { return apiKeyRepository.findByUserIdOrderByCreatedAtDesc(userId); } public List<ApiKey> getActiveUserApiKeys(String userId) { return apiKeyRepository.findByUserIdAndStatusOrderByCreatedAtDesc(userId, ApiKeyStatus.ACTIVE); } @CacheEvict(value = "apiKeys", key = "#keyId") @Transactional public void revokeApiKey(String keyId, String revokedBy) { apiKeyRepository.findByKeyId(keyId).ifPresent(apiKey -> { apiKey.setStatus(ApiKeyStatus.REVOKED); apiKeyRepository.save(apiKey); logger.info("API key revoked: {} by {}", keyId, revokedBy); }); } @CacheEvict(value = "apiKeys", key = "#keyId") @Transactional public void deleteApiKey(String keyId) { apiKeyRepository.deleteByKeyId(keyId); logger.info("API key deleted: {}", keyId); } @Transactional public ApiKey rotateApiKey(String oldKeyId, String userId, String newKeyName) { // Revoke old key revokeApiKey(oldKeyId, userId); // Create new key with same permissions ApiKey oldKey = apiKeyRepository.findByKeyId(oldKeyId) .orElseThrow(() -> new ApiKeyNotFoundException("Key not found: " + oldKeyId)); KeyGenerationService.GeneratedApiKey newKey = createApiKey( userId, newKeyName != null ? newKeyName : oldKey.getName() + " (Rotated)", oldKey.getPermissions(), oldKey.getType(), LocalDateTime.now().plusDays(defaultExpiryDays) ); logger.info("API key rotated: {} -> {}", oldKeyId, newKey.getKeyId()); return apiKeyRepository.findByKeyId(newKey.getKeyId()).orElseThrow(); } public void logUsage(String apiKeyId, String userId, String endpoint, String method, String ipAddress, String userAgent, int statusCode, long responseTimeMs, boolean success) { ApiKeyUsageLog usageLog = new ApiKeyUsageLog(); usageLog.setApiKeyId(apiKeyId); usageLog.setUserId(userId); usageLog.setEndpoint(endpoint); usageLog.setMethod(method); usageLog.setIpAddress(ipAddress); usageLog.setUserAgent(userAgent); usageLog.setStatusCode(statusCode); usageLog.setResponseTimeMs(responseTimeMs); usageLog.setSuccess(success); usageLogRepository.save(usageLog); } @Scheduled(cron = "0 0 2 * * ?") // Daily at 2 AM @Transactional public void expireOldKeys() { LocalDateTime now = LocalDateTime.now(); List<ApiKey> expiredKeys = apiKeyRepository.findByExpiresAtBeforeAndStatus(now, ApiKeyStatus.ACTIVE); for (ApiKey key : expiredKeys) { key.setStatus(ApiKeyStatus.EXPIRED); logger.info("API key expired: {}", key.getKeyId()); } apiKeyRepository.saveAll(expiredKeys); } public List<ApiKey> getKeysNeedingRotation() { LocalDateTime rotationDate = LocalDateTime.now().plusDays(30); return apiKeyRepository.findByExpiresAtBeforeAndStatus(rotationDate, ApiKeyStatus.ACTIVE); } // Custom Exceptions public static class ApiKeyLimitExceededException extends RuntimeException { public ApiKeyLimitExceededException(String message) { super(message); } } public static class ApiKeyNotFoundException extends RuntimeException { public ApiKeyNotFoundException(String message) { super(message); } } }

8. Spring Security Integration

API Key Authentication Filter:

package com.myapp.apikey.security; import com.myapp.apikey.model.ApiKey; import com.myapp.apikey.service.ApiKeyManagementService; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; 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 java.io.IOException; import java.util.Collections; import java.util.Optional; import java.util.stream.Collectors; public class ApiKeyAuthenticationFilter extends OncePerRequestFilter { private static final Logger logger = LoggerFactory.getLogger(ApiKeyAuthenticationFilter.class); private final ApiKeyManagementService apiKeyManagementService; @Value("${app.api-key.header-name:X-API-Key}") private String apiKeyHeader; public ApiKeyAuthenticationFilter(ApiKeyManagementService apiKeyManagementService) { this.apiKeyManagementService = apiKeyManagementService; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String apiKey = extractApiKey(request); if (apiKey != null) { try { Optional<ApiKey> validatedKey = apiKeyManagementService.validateApiKey(apiKey); if (validatedKey.isPresent()) { ApiKey apiKeyEntity = validatedKey.get(); setAuthentication(apiKeyEntity, request); // Log usage logUsage(apiKeyEntity, request, response); } else { logger.warn("Invalid API key attempt from: {}", getClientIp(request)); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid API Key"); return; } } catch (Exception e) { logger.error("API key validation error", e); response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Authentication error"); return; } } filterChain.doFilter(request, response); } private String extractApiKey(HttpServletRequest request) { // Check header first String apiKey = request.getHeader(apiKeyHeader); // Fallback to query parameter if (apiKey == null) { apiKey = request.getParameter("api_key"); } return apiKey; } private void setAuthentication(ApiKey apiKey, HttpServletRequest request) { var authorities = apiKey.getPermissions().stream() .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); var authentication = new UsernamePasswordAuthenticationToken( apiKey.getUserId(), null, authorities ); // Set API key details authentication.setDetails(new ApiKeyAuthenticationDetails(apiKey, getClientIp(request))); SecurityContextHolder.getContext().setAuthentication(authentication); // Set user ID header for downstream use request.setAttribute("X-API-User-Id", apiKey.getUserId()); } private void logUsage(ApiKey apiKey, HttpServletRequest request, HttpServletResponse response) { try { long startTime = (Long) request.getAttribute("requestStartTime"); long responseTime = System.currentTimeMillis() - startTime; apiKeyManagementService.logUsage( apiKey.getKeyId(), apiKey.getUserId(), request.getRequestURI(), request.getMethod(), getClientIp(request), request.getHeader("User-Agent"), response.getStatus(), responseTime, response.getStatus() < 400 ); } catch (Exception e) { logger.error("Failed to log API key usage", e); } } private String getClientIp(HttpServletRequest request) { String xfHeader = request.getHeader("X-Forwarded-For"); if (xfHeader != null) { return xfHeader.split(",")[0]; } return request.getRemoteAddr(); } @Override protected boolean shouldNotFilter(HttpServletRequest request) { String path = request.getRequestURI(); return path.startsWith("/public/") || path.startsWith("/actuator/health") || path.equals("/error"); } } class ApiKeyAuthenticationDetails { private final ApiKey apiKey; private final String clientIp; public ApiKeyAuthenticationDetails(ApiKey apiKey, String clientIp) { this.apiKey = apiKey; this.clientIp = clientIp; } // Getters public ApiKey getApiKey() { return apiKey; } public String getClientIp() { return clientIp; } }

Security Configuration:

package com.myapp.apikey.config; import com.myapp.apikey.security.ApiKeyAuthenticationFilter; import com.myapp.apikey.service.ApiKeyManagementService; 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 { private final ApiKeyManagementService apiKeyManagementService; public SecurityConfig(ApiKeyManagementService apiKeyManagementService) { this.apiKeyManagementService = apiKeyManagementService; } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(authz -> authz .requestMatchers("/public/**", "/actuator/health").permitAll() .requestMatchers("/api/admin/**").hasAuthority("ADMIN") .anyRequest().authenticated() ) .addFilterBefore( new ApiKeyAuthenticationFilter(apiKeyManagementService), UsernamePasswordAuthenticationFilter.class ); return http.build(); } }

9. REST Controllers

API Key Management Controller:

package com.myapp.apikey.controller; import com.myapp.apikey.model.ApiKey; import com.myapp.apikey.model.ApiKeyType; import com.myapp.apikey.service.ApiKeyManagementService; import com.myapp.apikey.service.KeyGenerationService; import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import java.time.LocalDateTime; import java.util.List; import java.util.Set; @RestController @RequestMapping("/api/apikeys") public class ApiKeyController { private final ApiKeyManagementService apiKeyManagementService; public ApiKeyController(ApiKeyManagementService apiKeyManagementService) { this.apiKeyManagementService = apiKeyManagementService; } @PostMapping public ResponseEntity<?> createApiKey(@Valid @RequestBody CreateApiKeyRequest request) { try { var generatedKey = apiKeyManagementService.createApiKey( request.getUserId(), request.getName(), request.getPermissions(), request.getType(), request.getExpiresAt() ); CreateApiKeyResponse response = new CreateApiKeyResponse( generatedKey.getKeyId(), generatedKey.getFullKey(), generatedKey.getType(), "Store this key securely. It won't be shown again." ); return ResponseEntity.ok(response); } catch (ApiKeyManagementService.ApiKeyLimitExceededException e) { return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage())); } } @GetMapping public ResponseEntity<List<ApiKey>> getUserApiKeys(@RequestParam String userId) { List<ApiKey> apiKeys = apiKeyManagementService.getUserApiKeys(userId); return ResponseEntity.ok(apiKeys); } @DeleteMapping("/{keyId}") public ResponseEntity<?> revokeApiKey(@PathVariable String keyId, @RequestParam String revokedBy) { apiKeyManagementService.revokeApiKey(keyId, revokedBy); return ResponseEntity.ok().build(); } @PostMapping("/{keyId}/rotate") public ResponseEntity<ApiKey> rotateApiKey(@PathVariable String keyId, @RequestParam String userId, @RequestParam(required = false) String newName) { ApiKey rotatedKey = apiKeyManagementService.rotateApiKey(keyId, userId, newName); return ResponseEntity.ok(rotatedKey); } @GetMapping("/needing-rotation") @PreAuthorize("hasAuthority('ADMIN')") public ResponseEntity<List<ApiKey>> getKeysNeedingRotation() { List<ApiKey> keys = apiKeyManagementService.getKeysNeedingRotation(); return ResponseEntity.ok(keys); } // Request/Response DTOs public static class CreateApiKeyRequest { private String userId; private String name; private Set<String> permissions; private ApiKeyType type = ApiKeyType.SECRET; private LocalDateTime expiresAt; // Getters and setters public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Set<String> getPermissions() { return permissions; } public void setPermissions(Set<String> permissions) { this.permissions = permissions; } public ApiKeyType getType() { return type; } public void setType(ApiKeyType type) { this.type = type; } public LocalDateTime getExpiresAt() { return expiresAt; } public void setExpiresAt(LocalDateTime expiresAt) { this.expiresAt = expiresAt; } } public static class CreateApiKeyResponse { private final String keyId; private final String apiKey; private final ApiKeyType type; private final String message; public CreateApiKeyResponse(String keyId, String apiKey, ApiKeyType type, String message) { this.keyId = keyId; this.apiKey = apiKey; this.type = type; this.message = message; } // Getters public String getKeyId() { return keyId; } public String getApiKey() { return apiKey; } public ApiKeyType getType() { return type; } public String getMessage() { return message; } } public static class ErrorResponse { private final String error; public ErrorResponse(String error) { this.error = error; } public String getError() { return error; } } }

10. Repository Layer

Repository Interfaces:

package com.myapp.apikey.repository; import com.myapp.apikey.model.ApiKey; import com.myapp.apikey.model.ApiKeyStatus; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @Repository public interface ApiKeyRepository extends JpaRepository<ApiKey, Long> { Optional<ApiKey> findByKeyId(String keyId); List<ApiKey> findByUserIdOrderByCreatedAtDesc(String userId); List<ApiKey> findByUserIdAndStatusOrderByCreatedAtDesc(String userId, ApiKeyStatus status); List<ApiKey> findByExpiresAtBeforeAndStatus(LocalDateTime date, ApiKeyStatus status); long countByUserIdAndStatus(String userId, ApiKeyStatus status); @Modifying @Query("DELETE FROM ApiKey a WHERE a.keyId = ?1") void deleteByKeyId(String keyId); @Query("SELECT a FROM ApiKey a WHERE a.status = 'ACTIVE' AND a.expiresAt < ?1") List<ApiKey> findExpiringSoon(LocalDateTime date); }
package com.myapp.apikey.repository; import com.myapp.apikey.model.ApiKeyUsageLog; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import java.time.LocalDateTime; import java.util.List; @Repository public interface ApiKeyUsageLogRepository extends JpaRepository<ApiKeyUsageLog, Long> { List<ApiKeyUsageLog> findByApiKeyIdOrderByTimestampDesc(String apiKeyId); List<ApiKeyUsageLog> findByUserIdOrderByTimestampDesc(String userId); @Query("SELECT l FROM ApiKeyUsageLog l WHERE l.timestamp BETWEEN ?1 AND ?2") List<ApiKeyUsageLog> findByTimestampBetween(LocalDateTime start, LocalDateTime end); long countByApiKeyIdAndTimestampAfter(String apiKeyId, LocalDateTime timestamp); }

11. Rate Limiting Configuration

Rate Limiting Service:

package com.myapp.apikey.ratelimit; import io.github.bucket4j.Bandwidth; import io.github.bucket4j.Bucket; import io.github.bucket4j.BucketConfiguration; import io.github.bucket4j.Refill; import io.github.bucket4j.distributed.proxy.ProxyManager; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.time.Duration; import java.util.function.Supplier; @Service public class RateLimitingService { private final ProxyManager<String> buckets; @Value("${app.api-key.rate-limiting.default-requests-per-minute:60}") private int defaultRequestsPerMinute; @Value("${app.api-key.rate-limiting.burst-capacity:100}") private int burstCapacity; public RateLimitingService(ProxyManager<String> buckets) { this.buckets = buckets; } public Bucket resolveBucket(String apiKeyId) { Supplier<BucketConfiguration> configSupplier = getConfigSupplierForUser(apiKeyId); return buckets.builder().build(apiKeyId, configSupplier); } public Bucket resolveBucket(String apiKeyId, int requestsPerMinute, int burstCapacity) { Supplier<BucketConfiguration> configSupplier = () -> { Bandwidth limit = Bandwidth.classic(burstCapacity, Refill.greedy(requestsPerMinute, Duration.ofMinutes(1))); return BucketConfiguration.builder() .addLimit(limit) .build(); }; return buckets.builder().build(apiKeyId, configSupplier); } private Supplier<BucketConfiguration> getConfigSupplierForUser(String apiKeyId) { // In a real application, you might fetch limits from database based on API key plan return () -> { Bandwidth limit = Bandwidth.classic(burstCapacity, Refill.greedy(defaultRequestsPerMinute, Duration.ofMinutes(1))); return BucketConfiguration.builder() .addLimit(limit) .build(); }; } public boolean tryConsume(String apiKeyId) { return resolveBucket(apiKeyId).tryConsume(1); } public boolean tryConsume(String apiKeyId, int tokens) { return resolveBucket(apiKeyId).tryConsume(tokens); } }

12. Monitoring and Metrics

API Key Metrics:

package com.myapp.apikey.monitoring; import com.myapp.apikey.repository.ApiKeyRepository; import com.myapp.apikey.repository.ApiKeyUsageLogRepository; import io.micrometer.core.instrument.Gauge; import io.micrometer.core.instrument.MeterRegistry; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.time.LocalDateTime; import java.util.concurrent.atomic.AtomicLong; @Component public class ApiKeyMetrics { private final ApiKeyRepository apiKeyRepository; private final ApiKeyUsageLogRepository usageLogRepository; private final MeterRegistry meterRegistry; private final AtomicLong totalApiKeys = new AtomicLong(); private final AtomicLong activeApiKeys = new AtomicLong(); private final AtomicLong expiredApiKeys = new AtomicLong(); public ApiKeyMetrics(ApiKeyRepository apiKeyRepository, ApiKeyUsageLogRepository usageLogRepository, MeterRegistry meterRegistry) { this.apiKeyRepository = apiKeyRepository; this.usageLogRepository = usageLogRepository; this.meterRegistry = meterRegistry; initializeMetrics(); } private void initializeMetrics() { Gauge.builder("apikeys.total") .description("Total number of API keys") .register(meterRegistry, totalApiKeys); Gauge.builder("apikeys.active") .description("Number of active API keys") .register(meterRegistry, activeApiKeys); Gauge.builder("apikeys.expired") .description("Number of expired API keys") .register(meterRegistry, expiredApiKeys); updateMetrics(); } @Scheduled(fixedRate = 60000) // Every minute public void updateMetrics() { totalApiKeys.set(apiKeyRepository.count()); // You would add more specific counts here } public void recordApiKeyUsage(String apiKeyId, boolean success) { meterRegistry.counter("apikeys.usage", "apikey", apiKeyId, "success", String.valueOf(success) ).increment(); } public void recordRateLimitHit(String apiKeyId) { meterRegistry.counter("apikeys.rate_limit_hits", "apikey", apiKeyId).increment(); } }

13. Database Schema

PostgreSQL Schema:

-- API Keys table CREATE TABLE api_keys ( id BIGSERIAL PRIMARY KEY, key_id VARCHAR(255) UNIQUE NOT NULL, key_hash TEXT NOT NULL, user_id VARCHAR(255) NOT NULL, name VARCHAR(255), status VARCHAR(50) NOT NULL DEFAULT 'ACTIVE', type VARCHAR(50) NOT NULL DEFAULT 'SECRET', created_at TIMESTAMP NOT NULL DEFAULT NOW(), expires_at TIMESTAMP, last_used_at TIMESTAMP, created_by VARCHAR(255), description TEXT, version BIGINT DEFAULT 0 ); -- API Key permissions CREATE TABLE api_key_permissions ( api_key_id BIGINT NOT NULL REFERENCES api_keys(id) ON DELETE CASCADE, permission VARCHAR(255) NOT NULL, PRIMARY KEY (api_key_id, permission) ); -- API Key usage logs CREATE TABLE api_key_usage_logs ( id BIGSERIAL PRIMARY KEY, api_key_id VARCHAR(255) NOT NULL, user_id VARCHAR(255), endpoint VARCHAR(500), method VARCHAR(10), ip_address VARCHAR(45), user_agent TEXT, status_code INTEGER, response_time_ms BIGINT, request_size BIGINT, response_size BIGINT, request_headers TEXT, response_headers TEXT, timestamp TIMESTAMP NOT NULL DEFAULT NOW(), success BOOLEAN DEFAULT true ); -- Indexes for performance CREATE INDEX idx_api_keys_user_id ON api_keys(user_id); CREATE INDEX idx_api_keys_status ON api_keys(status); CREATE INDEX idx_api_keys_expires_at ON api_keys(expires_at); CREATE INDEX idx_usage_logs_api_key_id ON api_key_usage_logs(api_key_id); CREATE INDEX idx_usage_logs_timestamp ON api_key_usage_logs(timestamp); CREATE INDEX idx_usage_logs_user_id ON api_key_usage_logs(user_id);

Benefits of Proper API Key Management

  1. Security - Proper key storage and validation
  2. Access Control - Fine-grained permissions
  3. Auditability - Complete usage tracking
  4. Rate Limiting - Prevent abuse and ensure fairness
  5. Key Rotation - Automatic expiration and renewal
  6. Scalability - Handle large numbers of keys efficiently

Conclusion

Implementing robust API key management in Java provides secure, scalable access control for your APIs. By following the patterns outlined in this guide, you can create a production-ready API key system that includes key generation, validation, rate limiting, and comprehensive monitoring.

The key to successful API key management is:

  • Secure key storage with proper hashing
  • Comprehensive validation including permissions checking
  • Effective rate limiting to prevent abuse
  • Detailed audit logging for security and analytics
  • Regular key rotation to maintain security
  • Performance optimization with caching and efficient queries

Start with basic API key validation and gradually add more advanced features like rate limiting, key rotation, and detailed analytics as your requirements evolve.


Call to Action: Begin by implementing basic API key generation and validation. Test the security aspects thoroughly, then gradually add rate limiting, usage tracking, and key rotation features. Monitor the system performance and security regularly to ensure reliable API key management.

Leave a Reply

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


Macro Nepal Helper