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
- Security - Proper key storage and validation
- Access Control - Fine-grained permissions
- Auditability - Complete usage tracking
- Rate Limiting - Prevent abuse and ensure fairness
- Key Rotation - Automatic expiration and renewal
- 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.