API Key Authentication for Internal APIs
Learn how to implement API Key Authentication in Spring Boot for internal APIs, service-to-service communication, webhook APIs, partner APIs, custom filters, header validation, database-backed API keys, and production best practices.
API Key Authentication for Internal APIs
Introduction
API Key Authentication is one of the simplest ways to protect internal APIs, partner APIs, webhook endpoints, and service-to-service communication.
An API key is a secret value sent by the client with every request.
Example:
X-API-KEY: 8f3a7b9c-example-secret-key
The server checks whether the API key is valid.
If valid, the request is allowed.
If missing or invalid, the request is rejected.
1. What is API Key Authentication?
API Key Authentication means the client must send a secret key with every request.
flowchart LR
Client["Client / Internal Service"]
--> Request["HTTP Request with API Key"]
--> Server["Spring Boot API"]
--> Validate["Validate API Key"]
--> Controller["Protected Controller"]
The API key usually comes in an HTTP header.
Common header names:
X-API-KEY
x-api-key
Authorization: ApiKey <token>
2. When Should You Use API Keys?
API keys are useful for:
- Internal APIs
- Partner APIs
- Webhook APIs
- Backend-to-backend communication
- Batch jobs
- Scheduled jobs
- Lightweight service authentication
mindmap
root((API Key Use Cases))
Internal APIs
Partner APIs
Webhooks
Batch Jobs
Backend Services
Automation Scripts
3. When Not to Use API Keys?
API keys are not the best option for user login.
For user authentication, use:
- Session login
- JWT
- OAuth2
- OpenID Connect
API keys identify an application or service, not a human user.
flowchart TB
UserLogin["User Login"]
--> JWT["JWT / OAuth2 / OIDC"]
ServiceAccess["Service Access"]
--> APIKey["API Key"]
4. Real-World Example
Imagine an internal notification service.
Only trusted backend services should call it.
Order Service → Notification Service
Payment Service → Notification Service
Claim Service → Notification Service
Each service sends an API key.
flowchart TB
Order["Order Service"]
Payment["Payment Service"]
Claim["Claim Service"]
Order --> Notification["Notification API"]
Payment --> Notification
Claim --> Notification
Notification --> Validate["Validate API Key"]
5. API Key Request Flow
sequenceDiagram
participant Client
participant ApiKeyFilter
participant ApiKeyValidator
participant Controller
Client->>ApiKeyFilter: Request with X-API-KEY
ApiKeyFilter->>ApiKeyValidator: Validate API Key
alt Valid Key
ApiKeyValidator-->>ApiKeyFilter: Valid
ApiKeyFilter->>Controller: Continue Request
Controller-->>Client: Response
else Invalid Key
ApiKeyFilter-->>Client: 401 Unauthorized
end
6. Project Structure
api-key-auth-demo
└── src
└── main
└── java
└── com.codewithvenu.apikey
├── config
│ ├── ApiKeyAuthFilter.java
│ └── SecurityConfig.java
├── controller
│ └── InternalApiController.java
├── service
│ └── ApiKeyValidatorService.java
└── ApiKeyAuthDemoApplication.java
7. Maven Dependencies
<dependencies>
<!-- Spring Web for REST APIs -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Security for filter chain support -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
8. application.yml
For a simple first implementation, store the API key in configuration.
server:
port: 8080
security:
api-key:
header-name: X-API-KEY
value: ${INTERNAL_API_KEY}
Use environment variable:
export INTERNAL_API_KEY="dev-secret-api-key-123"
Never hardcode production API keys in GitHub.
9. Step 1: Create API Key Validator Service
package com.codewithvenu.apikey.service;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@Service
public class ApiKeyValidatorService {
@Value("${security.api-key.value}")
private String configuredApiKey;
public boolean isValid(String apiKey) {
if (apiKey == null || apiKey.isBlank()) {
return false;
}
return configuredApiKey.equals(apiKey);
}
}
Code Explanation
@Service
Registers this class as a Spring service.
@Value("${security.api-key.value}")
Reads API key value from application.yml.
isValid
Checks whether the incoming API key matches configured API key.
10. Step 2: Create Custom API Key Filter
package com.codewithvenu.apikey.config;
import com.codewithvenu.apikey.service.ApiKeyValidatorService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class ApiKeyAuthFilter extends OncePerRequestFilter {
private final ApiKeyValidatorService validatorService;
@Value("${security.api-key.header-name}")
private String apiKeyHeaderName;
public ApiKeyAuthFilter(ApiKeyValidatorService validatorService) {
this.validatorService = validatorService;
}
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String apiKey = request.getHeader(apiKeyHeaderName);
if (!validatorService.isValid(apiKey)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Invalid or missing API key");
return;
}
filterChain.doFilter(request, response);
}
}
Code Explanation
OncePerRequestFilter
Ensures this filter runs once per request.
request.getHeader(apiKeyHeaderName)
Reads the API key from HTTP header.
validatorService.isValid(apiKey)
Validates API key.
SC_UNAUTHORIZED
Returns HTTP 401 if key is missing or invalid.
filterChain.doFilter
Continues request processing if key is valid.
11. Step 3: Create Security Configuration
package com.codewithvenu.apikey.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.*;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
public class SecurityConfig {
private final ApiKeyAuthFilter apiKeyAuthFilter;
public SecurityConfig(ApiKeyAuthFilter apiKeyAuthFilter) {
this.apiKeyAuthFilter = apiKeyAuthFilter;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(
apiKeyAuthFilter,
UsernamePasswordAuthenticationFilter.class
);
return http.build();
}
}
Code Explanation
csrf.disable()
Disables CSRF for REST API example.
/public/**
Allows public endpoints.
anyRequest().authenticated()
Requires authentication for all other endpoints.
addFilterBefore
Runs API key filter before Spring username/password authentication filter.
12. Problem with Previous Filter
The previous filter validates API key for every request.
But /public/** should not require API key.
To fix this, skip public URLs inside the filter.
13. Improved API Key Filter
package com.codewithvenu.apikey.config;
import com.codewithvenu.apikey.service.ApiKeyValidatorService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class ApiKeyAuthFilter extends OncePerRequestFilter {
private final ApiKeyValidatorService validatorService;
@Value("${security.api-key.header-name}")
private String apiKeyHeaderName;
public ApiKeyAuthFilter(ApiKeyValidatorService validatorService) {
this.validatorService = validatorService;
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String path = request.getServletPath();
return path.startsWith("/public");
}
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String apiKey = request.getHeader(apiKeyHeaderName);
if (!validatorService.isValid(apiKey)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write("""
{"error":"Invalid or missing API key"}
""");
return;
}
filterChain.doFilter(request, response);
}
}
14. Step 4: Create Controller
package com.codewithvenu.apikey.controller;
import org.springframework.web.bind.annotation.*;
@RestController
public class InternalApiController {
@GetMapping("/public/health")
public String health() {
return "Application is running";
}
@GetMapping("/internal/orders")
public String orders() {
return "Internal order data";
}
@PostMapping("/internal/notifications")
public String sendNotification() {
return "Notification sent successfully";
}
}
15. Test Public API
curl http://localhost:8080/public/health
Expected:
Application is running
16. Test Protected API Without API Key
curl http://localhost:8080/internal/orders
Expected:
{"error":"Invalid or missing API key"}
17. Test Protected API With API Key
curl http://localhost:8080/internal/orders \
-H "X-API-KEY: dev-secret-api-key-123"
Expected:
Internal order data
18. API Key Flow Summary
flowchart TB
Request["HTTP Request"]
--> PathCheck{"Public URL?"}
PathCheck -- Yes --> Controller["Controller"]
PathCheck -- No --> HeaderCheck{"X-API-KEY Present?"}
HeaderCheck -- No --> Unauthorized["401 Unauthorized"]
HeaderCheck -- Yes --> Validate{"Valid API Key?"}
Validate -- No --> Unauthorized
Validate -- Yes --> Controller
19. Better Approach: Database-Backed API Keys
Configuration-based API keys work for simple internal systems.
For real production systems, store API keys in database.
Benefits:
- Multiple clients
- Expiry date
- Revocation
- Owner tracking
- Usage limits
- Audit logs
flowchart TB
Client["Client Service"]
--> API["Spring Boot API"]
API --> DB["API Keys Table"]
DB --> Valid["Valid / Expired / Revoked"]
Valid --> Controller["Controller"]
20. API Key Entity
package com.codewithvenu.apikey.entity;
import jakarta.persistence.*;
import java.time.Instant;
@Entity
@Table(name = "api_keys")
public class ApiKey {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable=false, unique=true, length=128)
private String keyHash;
@Column(nullable=false)
private String clientName;
@Column(nullable=false)
private boolean active;
private Instant expiresAt;
protected ApiKey() {
}
public ApiKey(String keyHash, String clientName, boolean active, Instant expiresAt) {
this.keyHash = keyHash;
this.clientName = clientName;
this.active = active;
this.expiresAt = expiresAt;
}
public String getKeyHash() {
return keyHash;
}
public String getClientName() {
return clientName;
}
public boolean isActive() {
return active;
}
public Instant getExpiresAt() {
return expiresAt;
}
}
Important:
Store API key hash, not raw API key.
21. API Key Repository
package com.codewithvenu.apikey.repository;
import com.codewithvenu.apikey.entity.ApiKey;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface ApiKeyRepository extends JpaRepository<ApiKey, Long> {
Optional<ApiKey> findByKeyHash(String keyHash);
}
22. Hash API Key Before Storing
package com.codewithvenu.apikey.service;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HexFormat;
@Service
public class ApiKeyHashService {
public String hash(String apiKey) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hashBytes = digest.digest(
apiKey.getBytes(StandardCharsets.UTF_8)
);
return HexFormat.of().formatHex(hashBytes);
} catch (Exception exception) {
throw new RuntimeException("Failed to hash API key", exception);
}
}
}
Why hash API keys?
If database leaks, raw API keys are not exposed.
23. Database-Backed Validator
package com.codewithvenu.apikey.service;
import com.codewithvenu.apikey.entity.ApiKey;
import com.codewithvenu.apikey.repository.ApiKeyRepository;
import org.springframework.stereotype.Service;
import java.time.Instant;
@Service
public class DatabaseApiKeyValidatorService {
private final ApiKeyRepository repository;
private final ApiKeyHashService hashService;
public DatabaseApiKeyValidatorService(ApiKeyRepository repository,
ApiKeyHashService hashService) {
this.repository = repository;
this.hashService = hashService;
}
public boolean isValid(String rawApiKey) {
if (rawApiKey == null || rawApiKey.isBlank()) {
return false;
}
String keyHash = hashService.hash(rawApiKey);
return repository.findByKeyHash(keyHash)
.filter(ApiKey::isActive)
.filter(key -> key.getExpiresAt() == null ||
key.getExpiresAt().isAfter(Instant.now()))
.isPresent();
}
}
24. Database API Key Validation Flow
sequenceDiagram
participant Client
participant Filter
participant HashService
participant Database
participant Controller
Client->>Filter: Request with X-API-KEY
Filter->>HashService: Hash raw API key
HashService-->>Filter: SHA-256 hash
Filter->>Database: Find key hash
Database-->>Filter: API key record
Filter->>Filter: Check active and expiry
Filter->>Controller: Continue request
25. Generate API Key
Use a strong random API key.
package com.codewithvenu.apikey.service;
import org.springframework.stereotype.Service;
import java.security.SecureRandom;
import java.util.Base64;
@Service
public class ApiKeyGeneratorService {
private final SecureRandom secureRandom = new SecureRandom();
public String generateApiKey() {
byte[] randomBytes = new byte[32];
secureRandom.nextBytes(randomBytes);
return Base64.getUrlEncoder()
.withoutPadding()
.encodeToString(randomBytes);
}
}
26. API Key Rotation
API keys should be rotated periodically.
flowchart TB
OldKey["Old API Key"]
--> NewKey["Generate New API Key"]
--> Deploy["Update Client"]
--> DisableOld["Disable Old API Key"]
Rotation reduces damage if an old key leaks.
27. API Key vs JWT
| API Key | JWT |
|---|---|
| Identifies application/service | Identifies user/session |
| Usually long-lived | Usually short-lived |
| Simple validation | Token claims and signature |
| Good for internal APIs | Good for user authentication |
| Usually stored server-side | Usually stateless |
28. API Key vs OAuth2 Client Credentials
| API Key | OAuth2 Client Credentials |
|---|---|
| Simple | More secure and standardized |
| Easy to implement | Requires authorization server |
| Manual rotation | Token-based expiry |
| Good for simple internal systems | Better for enterprise service-to-service security |
29. Production Best Practices
- Use HTTPS only
- Never log API keys
- Store only hashed API keys
- Rotate keys regularly
- Add expiry date
- Add active/revoked flag
- Track client name
- Use rate limiting
- Use IP allowlisting where possible
- Use different keys per client
- Monitor API key usage
- Prefer OAuth2 client credentials for large enterprise systems
30. Common Mistakes
| Mistake | Risk |
|---|---|
| Hardcoding API key | Secret leakage |
| Sending API key in query params | Key appears in logs |
| No expiry | Unlimited access |
| Same key for all clients | Cannot isolate incidents |
| Logging headers | Key leakage |
| No rotation | Long-term compromise |
| Storing raw key in DB | DB breach exposes keys |
31. Interview Questions
Q1. What is API Key Authentication?
API Key Authentication validates requests using a secret key sent by the client.
Q2. Where should API key be sent?
Usually in an HTTP header such as X-API-KEY.
Q3. Should API keys be used for user login?
No. API keys are better for application/service authentication.
Q4. Should API keys be stored as plain text?
No. Store hashed API keys.
Q5. Why avoid query parameters for API keys?
Query parameters can appear in logs, browser history, and analytics tools.
Q6. API Key vs JWT?
API key usually identifies an application. JWT usually identifies a user/session.
Q7. What is API key rotation?
Replacing old keys with new keys and disabling old keys.
32. Key Takeaways
- API Key Authentication is simple and useful for internal APIs.
- API keys should be sent in headers.
- Use custom filters in Spring Boot to validate API keys.
- Public URLs should bypass API key validation.
- Production systems should store hashed API keys in database.
- API keys should support expiry, revocation, rotation, and audit tracking.
- Do not use API keys for normal user login.
- For enterprise service-to-service security, OAuth2 Client Credentials or mTLS may be better.
Next Article
➡️ Rate Limiting APIs with Bucket4j and Redis