Facebook Pixel
Articles

Complete Guide to Spring Security API Key Authentication for REST APIs

A practical, in‑depth tutorial on wiring API key authentication into Spring Security for stateless REST services. It covers custom AuthenticationToken, provider, filter chain, secure key storage, testing, and production hardening.

Written by:
APin

Senior Technology Analyst • Verified Expert

More from this author →
Complete Guide to Spring Security API Key Authentication for REST APIs

A practical, in‑depth tutorial on wiring API key authentication into Spring Security for stateless REST services. It covers custom AuthenticationToken, provider, filter chain, secure key storage, testing, and production hardening.

Why API Keys Over Traditional Session‑Based Security

Spring Security’s out‑of‑the‑box configuration is built around a stateful, form‑login flow. It creates an HttpSession, stores a SecurityContext in that session, and expects the client to return a session cookie on each request. This model works for traditional web UI applications, but it collides with the design goals of a REST API, which is supposed to be stateless: every request must contain all information required for authentication and authorization without relying on server‑side session data.

When a REST endpoint is protected with the default setup, developers encounter several friction points:

  • CSRF protection is automatically enabled, forcing clients to obtain and send a CSRF token that is irrelevant for API calls.
  • Redirects to a login page are generated for unauthenticated requests, breaking the JSON contract expected by API consumers.
  • Session cookies appear in the HTTP contract, requiring clients to manage cookie storage even though they never requested it.

These mismatches make the default flow unsuitable for micro‑services, mobile apps, or third‑party integrations that expect a pure HTTP/JSON interaction. A pragmatic alternative is to replace the heavyweight OAuth or JWT scaffolding with a simple API key. An API key is a secret string sent in a request header (e.g., X‑API‑KEY) and validated on each call, preserving statelessness while leveraging Spring’s extensible filter chain.

Typical implementation steps are:

  1. Create a lightweight AuthenticationToken that holds the raw key (see ApiKeyAuthenticationToken in the evidence).
  2. Implement an AuthenticationProvider that looks up the hashed key in a repository and, on a match, returns an authenticated token with the user’s authorities.
  3. Register a OncePerRequestFilter that extracts the header, builds an unauthenticated token, and delegates to the provider.

Key operational considerations include:

  • Never store raw keys; hash them (e.g., BCrypt) before persisting.
  • Fail fast with 401 Unauthorized without indicating whether the key was missing or invalid to avoid enumeration attacks.
  • Isolate filter, provider, and repository for unit testing, and add rate‑limiting or revocation logic via Spring Actuator for production hardening.

By aligning authentication with the stateless nature of REST, an API key approach eliminates session leakage, simplifies client code, and fits cleanly into Spring Security’s architecture without the overhead of full OAuth flows.

Creating a Lightweight AuthenticationToken

In Spring Security the Authentication interface represents the security context of a request. A custom token is required when the default username‑password or JWT mechanisms do not match the API contract. The ApiKeyAuthenticationToken is a lightweight implementation that carries only the raw API key and, once validated, the associated UserDetails and granted authorities.

The class defines two distinct constructors that encode the token’s lifecycle:

  • Unauthenticated constructor – invoked by the filter after extracting the key from a request header. It stores the raw key, leaves the principal null, and calls setAuthenticated(false). This signals to the AuthenticationProvider that the token still requires verification.
  • Authenticated constructor – used by the provider after a successful lookup. It receives a populated UserDetails instance, the same key (or a masked version), and a collection of GrantedAuthority. The call to setAuthenticated(true) marks the token as trusted, allowing downstream security checks to rely on its authorities.
public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken {
    private final String apiKey;
    private final UserDetails principal;

    // Unauthenticated instance
    public ApiKeyAuthenticationToken(String apiKey) {
        super(null);
        this.apiKey = apiKey;
        this.principal = null;
        setAuthenticated(false);
    }

    // Authenticated instance
    public ApiKeyAuthenticationToken(UserDetails principal,
                                    String apiKey,
                                    Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.apiKey = apiKey;
        this.principal = principal;
        setAuthenticated(true);
    }

    @Override
    public Object getCredentials() { return apiKey; }

    @Override
    public Object getPrincipal() { return principal; }
}

Because the filter creates the unauthenticated token and passes it to an AuthenticationProvider, the token becomes the cornerstone of the authentication flow: the provider validates the key (typically by comparing a hashed value stored in a database), constructs the authenticated token, and returns it to the security context. Subsequent authorization decisions consult the token’s authorities, keeping controller code free of security concerns.

Practical guidelines for using this token include:

  • Never persist raw keys; hash them with a strong algorithm such as BCrypt before storage.
  • Fail fast with a 401 Unauthorized response without indicating whether the key was missing or invalid, mitigating enumeration attacks.
  • Isolate the filter, provider, and repository in unit tests to verify edge cases (e.g., malformed headers, revoked keys) without starting a full server.

Configuring the Security Filter Chain and Provider

To implement API key authentication in Spring Security, one must transition from session-based flows to a stateless architecture. A robust approach involves creating a custom AuthenticationToken, a OncePerRequestFilter for interception, and a dedicated AuthenticationProvider for validation. This separation of concerns ensures that authentication logic remains decoupled from business controllers.

The flow functions as follows:

  • Token Definition: Create a class extending AbstractAuthenticationToken to hold the raw API key. Initialize it as unauthenticated until the provider verifies the credentials.
  • Filter Interception: Implement OncePerRequestFilter to extract the API key from the request header. Upon extraction, instantiate the unauthenticated token and pass it to the AuthenticationManager.
  • Provider Validation: Define an AuthenticationProvider that implements the authenticate method. This provider performs the lookup against your data store, hashes the incoming key (e.g., using BCrypt), and returns a fully populated AuthenticationToken containing user authorities if the match is successful.

Consider the following implementation pattern for the authentication token:

public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken {
    private final String apiKey;
    private final UserDetails principal;

    public ApiKeyAuthenticationToken(String apiKey) {
        super(null);
        this.apiKey = apiKey;
        this.principal = null;
        setAuthenticated(false);
    }
    // Constructor for authenticated state...
}

When configuring these components, prioritize security hardening by following these practices:

  • Fail Silently: Always return a 401 Unauthorized status for invalid or missing keys to prevent user enumeration attacks.
  • Secure Storage: Never store raw API keys. Persist only the hashes in your repository and compare the hashed input during the provider's validation phase.
  • Isolation: Test the filter, provider, and repository layers in isolation to verify edge cases without requiring a full server context.

Securely Storing and Managing API Keys

When an API key is used for service‑to‑service authentication, the key must never be persisted in clear text. Spring Data JPA can store a hashed representation of the key, while the application logic handles verification, rotation, and multi‑key support per client.

Entity design

  • @Entity class ApiKey contains id, clientId, hashedKey, createdAt, expiresAt, and active flags.
  • The hashedKey column stores the result of BCryptPasswordEncoder.encode(rawKey), which includes a salt and a work factor.
  • Multiple rows can reference the same clientId, enabling a client to rotate keys without downtime.

Persisting a new key

@Service
public class ApiKeyService {
    private final ApiKeyRepository repo;
    private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();

    public ApiKey createKey(String clientId) {
        String raw = UUID.randomUUID().toString().replace("-", "");
        ApiKey entity = new ApiKey();
        entity.setClientId(clientId);
        entity.setHashedKey(encoder.encode(raw));
        entity.setCreatedAt(Instant.now());
        entity.setActive(true);
        repo.save(entity);
        // Return the raw key only once, e.g., via a secure channel
        return raw;
    }
}

Authentication flow

  • A custom OncePerRequestFilter extracts the key from a header (e.g., X‑API‑KEY).
  • The filter builds an unauthenticated ApiKeyAuthenticationToken and delegates to an AuthenticationProvider.
  • The provider loads all active ApiKey rows for the supplied clientId and uses BCryptPasswordEncoder.matches(raw, hashed) to verify.
  • On success, an authenticated token with the client’s authorities is returned; on failure, a 401 response is sent without indicating whether the key was missing or invalid (preventing enumeration attacks).

Rotation strategy

  • Generate a new key and store it as a separate row while keeping the old key active.
  • Update client configuration to use the new key; the old key remains valid for a grace period.
  • After the grace period, mark the old row active = false and optionally delete it.

Implementing these practices satisfies requirements of security frameworks such as SOC 2, ISO 27001, NIST SP 800‑63B, and OWASP ASVS, which all mandate protection of secrets at rest and controlled key lifecycle management. By hashing keys with BCrypt, avoiding raw storage, and supporting seamless rotation, the system reduces the risk of credential leakage and supports robust auditability.

Testing the Authentication Components

When an API‑key authentication flow is built with Spring Security, the filter, AuthenticationProvider, and controller should each be testable in isolation. Isolating these components avoids the overhead of starting an embedded servlet container and makes the test suite fast enough for continuous integration pipelines.

Unit‑testing the filter

The custom OncePerRequestFilter extracts the header, creates an ApiKeyAuthenticationToken, and forwards it to the authentication manager. A unit test can verify this behavior by supplying a mock HttpServletRequest and a stubbed AuthenticationManager.

@Test
void filterShouldCreateUnauthenticatedToken() throws Exception {
    MockHttpServletRequest request = new MockHttpServletRequest();
    request.addHeader("X‑API‑KEY", "test-key");
    MockHttpServletResponse response = new MockHttpServletResponse();

    AuthenticationManager manager = mock(AuthenticationManager.class);
    when(manager.authenticate(any())).thenReturn(
        new ApiKeyAuthenticationToken(userDetails, "test-key", List.of()));

    ApiKeyFilter filter = new ApiKeyFilter(manager);
    filter.doFilterInternal(request, response, mock(FilterChain.class));

    verify(manager).authenticate(argThat(
        token -> token instanceof ApiKeyAuthenticationToken &&
                 "test-key".equals(token.getCredentials())));
}
  • Missing header → expect 401 without revealing the cause.
  • Malformed key (e.g., empty string) → same fast‑fail response.
  • Exception from the manager → ensure the filter propagates a proper AuthenticationException.

Unit‑testing the provider

The provider validates the raw key against a persisted hash (e.g., BCrypt). Mock the repository that stores hashed keys and assert both success and failure paths.

@Test
void providerAuthenticatesValidKey() {
    ApiKeyRepository repo = mock(ApiKeyRepository.class);
    when(repo.findHashByKey("valid-key"))
        .thenReturn(Optional.of(bCryptEncoder.encode("valid-key")));

    ApiKeyAuthenticationProvider provider = new ApiKeyAuthenticationProvider(repo);
    Authentication result = provider.authenticate(
        new ApiKeyAuthenticationToken("valid-key"));

    assertTrue(result.isAuthenticated());
    assertEquals("valid-key", result.getCredentials());
}
  • Invalid key → provider returns null or throws BadCredentialsException.
  • Key not found → same outcome, preventing enumeration.
  • Repository throws → verify that the exception is wrapped as an AuthenticationServiceException.

Integration‑style test for the controller

Using @WebMvcTest loads only the MVC layer and the filter chain, not the full server. Inject a mock provider to control authentication outcomes.

@WebMvcTest(secured = true)
class ApiControllerTest {
    @Autowired MockMvc mvc;
    @MockBean ApiKeyAuthenticationProvider provider;

    @Test
    void requestWithValidKeyIsAuthorized() throws Exception {
        when(provider.authenticate(any()))
            .thenReturn(new ApiKeyAuthenticationToken(user, "key", List.of()));

        mvc.perform(get("/api/resource")
                .header("X‑API‑KEY", "key"))
            .andExpect(status().isOk());
    }

    @Test
    void requestWithMissingKeyIsRejected() throws Exception {
        mvc.perform(get("/api/resource"))
            .andExpect(status().isUnauthorized());
    }
}

These tests cover edge cases such as missing headers, malformed keys, and repository failures while keeping the runtime footprint minimal.

Production Hardening: Rate Limiting, Revocation, and Monitoring

API keys are a lightweight alternative to OAuth or JWT for protecting stateless REST endpoints, but they introduce operational risks that must be mitigated before a service is considered production‑ready. The first step is to understand the common failure modes and then apply concrete controls such as rate limiting, revocation, and observability.

Typical pitfalls

  • Logging raw keys – Frameworks like Spring Boot often log request headers at DEBUG level. If the Authorization or custom API‑key header is written verbatim, the secret is exposed in log aggregation systems.
  • Unlimited request volume – Without throttling a compromised key can be used to launch denial‑of‑service attacks against downstream services.
  • Stale or leaked keys – Keys that are not revoked after employee turnover or credential rotation remain valid, violating controls required by SOC 2, ISO 27001, and NIST SP 800‑57.
  • Insufficient visibility – Without metrics, anomalous usage patterns (e.g., a sudden spike from a single client) go unnoticed, breaching OWASP API Security Top 10 recommendations for monitoring.

Production‑hardening techniques

  • Rate limiting – Implement a OncePerRequestFilter that consults a token bucket stored in Redis or an in‑memory cache. Example:
    if (!bucket.tryConsume(1)) {
        response.sendError(HttpServletResponse.SC_TOO_MANY_REQUESTS);
        return;
    }
  • Revocation – Store a revoked flag alongside the hashed key in the database. The AuthenticationProvider checks this flag and returns 401 for revoked entries. Rotate keys by generating a new secret, hashing it (e.g., BCrypt), and updating the record while preserving the old hash for a grace period.
  • Monitoring with Spring Actuator – Expose custom metrics such as apiKey.requests and apiKey.errors. Register a MeterBinder that increments counters per client identifier, then query via /actuator/metrics/apiKey.requests. Combine with /actuator/loggers to ensure key‑header logging is disabled at runtime.

By eliminating raw‑key logs, enforcing per‑client throttling, providing a revocation workflow, and instrumenting usage through Actuator, an API‑key implementation aligns with the access‑control and audit‑logging requirements of SOC 2, ISO 27001, and NIST, while satisfying OWASP’s guidance for secure API design.

APPWORKS ENGINEERING

Looking for Custom Software or AI Solutions?

Appworks Technologies designs, builds, and scales production enterprise platforms, microservices, and AI agent workflows tailored to your business goals.

Editorial Policy & Research Methodology

Our findings are based on rigorous internal research, verified industry benchmarks, and direct technical implementation experience from our enterprise client projects. All statistics and technical claims are reviewed by senior engineers before publication to ensure accuracy, transparency, and helpfulness for our readers.

Have an Idea? we offer services in Lucknow, Bangalore, Delhi NCR and other locations