Implementing API Key Authentication With a Custom Spring Security Filter

API keys are a practical choice for server-to-server integrations, internal tools and controlled public APIs. In a Spring application, a custom security filter can read a key from an HTTP header, validate it and attach an authenticated principal to the security context before request authorisation takes place.

This approach is useful when a full login flow with sessions, cookies or OAuth2 would add unnecessary complexity. It also fits services used by Australian businesses, such as an inventory API connecting systems in Sydney, Melbourne and Brisbane.

A secure implementation needs more than a string comparison. Key storage, rotation, transport encryption, request matching, failure responses and audit logging all affect the quality of the authentication design.

The example below uses Spring Security 6 and a OncePerRequestFilter. It can be adapted for Spring Boot applications that expose REST endpoints, payment integrations or private partner APIs.

Choose A Clear API Key Contract

Define where clients must send the credential. A custom header such as X-API-Key is preferable to a query parameter because URLs can appear in browser history, reverse-proxy logs and monitoring systems.

The filter should return a consistent 401 Unauthorized response when the key is missing or invalid. A valid key proves application identity, but it does not automatically grant every permission. Authorities or scopes should still control access to individual endpoints.

public final class ApiKeyAuthenticationFilter
        extends OncePerRequestFilter {

    private final ApiKeyService apiKeyService;

    public ApiKeyAuthenticationFilter(ApiKeyService apiKeyService) {
        this.apiKeyService = apiKeyService;
    }

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain)
            throws ServletException, IOException {

        String key = request.getHeader("X-API-Key");

        if (key == null || key.isBlank()) {
            filterChain.doFilter(request, response);
            return;
        }

        apiKeyService.findValidKey(key).ifPresentOrElse(
            apiKey -> {
                var authentication =
                    new UsernamePasswordAuthenticationToken(
                        apiKey.clientId(),
                        null,
                        apiKey.authorities()
                    );
                SecurityContextHolder.getContext()
                    .setAuthentication(authentication);

                try {
                    filterChain.doFilter(request, response);
                } catch (IOException | ServletException ex) {
                    throw new RuntimeException(ex);
                }
            },
            () -> sendUnauthorized(response)
        );
    }

    private void sendUnauthorized(HttpServletResponse response)
            throws IOException {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType("application/json");
        response.getWriter().write("{\"error\":\"Invalid API key\"}");
    }
}

Validate Keys Without Exposing Secrets

Never store raw production keys in a database if a digest can be stored instead. Generate a high-entropy random value, show it to the client once, and persist a cryptographic hash with metadata such as client ID, expiry date, status and permitted scopes.

The validation service can hash the supplied value and compare it with the stored digest using a constant-time comparison. A real implementation should also reject revoked or expired keys and avoid logging the incoming credential.

@Service
public class ApiKeyService {

    private final ApiKeyRepository repository;

    public Optional<ApiKeyRecord> findValidKey(String rawKey) {
        return repository.findAllActive().stream()
            .filter(record -> record.expiresAt().isAfter(Instant.now()))
            .filter(record -> MessageDigest.isEqual(
                record.keyHash(),
                sha256(rawKey)))
            .findFirst();
    }

    private byte[] sha256(String value) {
        try {
            return MessageDigest.getInstance("SHA-256")
                .digest(value.getBytes(StandardCharsets.UTF_8));
        } catch (NoSuchAlgorithmException ex) {
            throw new IllegalStateException(ex);
        }
    }
}

Register The Filter In Spring Security

The filter must run before a standard authentication filter that could otherwise process the request first. addFilterBefore is commonly used with UsernamePasswordAuthenticationFilter, although the best position depends on the rest of the security chain.

@Bean
SecurityFilterChain apiSecurity(HttpSecurity http,
                                ApiKeyService apiKeyService)
        throws Exception {
    var apiKeyFilter =
        new ApiKeyAuthenticationFilter(apiKeyService);

    return http
        .csrf(csrf -> csrf.disable())
        .sessionManagement(session ->
            session.sessionCreationPolicy(
                SessionCreationPolicy.STATELESS))
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/actuator/health").permitAll()
            .requestMatchers(HttpMethod.GET, "/api/reports/**")
                .hasAuthority("REPORT_READ")
            .anyRequest().authenticated())
        .addFilterBefore(apiKeyFilter,
            UsernamePasswordAuthenticationFilter.class)
        .build();
}

CSRF protection is usually disabled for a stateless API that does not use browser cookies for authentication. It should not be disabled automatically for a browser-facing application. HTTPS is mandatory, including between an Australian cloud workload and a local office or data centre.

Compare Authentication Design Choices

API keys work well for stable machine identities, while other mechanisms solve different problems. Choosing the simplest suitable option reduces maintenance and limits accidental exposure.

Mechanism Suitable Use Main Strength Important Limitation
API key Partner or internal service access Simple integration Limited user identity
Basic authentication Legacy service connections Easy to implement Credentials need careful rotation
OAuth2 client credentials Multi-service platforms Scopes and token expiry Greater configuration overhead
JWT bearer token Distributed APIs Local token verification Revocation is more complex
Session login Browser applications Strong user interaction model Poor fit for stateless APIs

For a payment gateway callback or warehouse integration, an API key may be sufficient when the receiving service only needs to identify the calling system. For customer-facing applications, OAuth2 or an established identity provider is generally more appropriate.

Limit The Filter’s Request Scope

A filter should not process every endpoint unless that is intentional. Apply it to API paths, or let requests without a key continue so that other authentication mechanisms can handle them.

@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
    return !request.getServletPath().startsWith("/api/");
}

Use a separate security chain when public pages, administrative routes and machine APIs have distinct rules. This prevents an API credential from accidentally becoming valid for browser pages or management endpoints.

Error responses should avoid revealing whether a client ID exists. Rate limiting at the gateway or reverse proxy is also valuable, especially for services exposed to the public internet in a market where cloud deployments may serve users across Perth, Adelaide and regional areas.

Harden Rotation, Monitoring And Compliance

Issue keys with an owner, purpose, creation timestamp and expiry policy. Supporting two active keys briefly allows a client to rotate credentials without downtime. Revocation should take effect quickly, particularly after a suspected leak.

Keep audit events useful but safe: record the client identifier, endpoint, result and timestamp, never the raw key. Monitor repeated failures, unusual locations and sudden traffic changes. Australian organisations handling personal information should also consider the Privacy Act 1988, the Australian Privacy Principles and obligations under the Notifiable Data Breaches scheme.

Test Authentication As Part Of The API

Integration tests should verify missing, malformed, expired, revoked and valid credentials. They should also confirm that a valid key receives only its assigned authorities and cannot access unrelated administrative routes.

Use Spring Security test support or MockMvc to assert both HTTP status and security context behaviour. A valid read-only key should receive 200 for an allowed GET request but 403 for a protected write operation. Test preflight and health endpoints separately when the API is consumed by systems with different networking requirements.

A custom filter remains small, explicit and effective when its responsibilities are limited to extracting credentials, validating them and establishing authentication. The surrounding service design—secure storage, authorisation rules, rotation and observability—determines whether the finished API is genuinely safe to operate.