ApiKeyAuthenticationFilter.java

package fr.avenirsesr.portfolio.common.security.infrastructure.filter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

@Slf4j
public class ApiKeyAuthenticationFilter extends OncePerRequestFilter {

  private static final String API_KEY_HEADER = "X-API-Key";

  private final String expectedApiKey;

  private final String permitAllPathsString;

  private List<String> permitAllPathsList;

  public ApiKeyAuthenticationFilter(String expectedApiKey, String permitAllPathsString) {
    this.expectedApiKey = expectedApiKey;
    this.permitAllPathsString = permitAllPathsString;
  }

  @Override
  protected boolean shouldNotFilter(@NonNull HttpServletRequest request) {
    if (permitAllPathsList == null && permitAllPathsString != null) {
      permitAllPathsList =
          Arrays.stream(permitAllPathsString.split(","))
              .map(path -> path.trim().replace("/**", ""))
              .toList();
    }

    if (permitAllPathsList == null) {
      return false;
    }

    String path = request.getRequestURI();
    return permitAllPathsList.stream().anyMatch(path::startsWith);
  }

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

    if (isExternalRequest(request)) {
      log.trace("External request detected; continuing filter chain");
      filterChain.doFilter(request, response);
      return;
    }

    String providedApiKey = request.getHeader(API_KEY_HEADER);

    if (providedApiKey == null || providedApiKey.isBlank()) {
      log.warn("API Key authentication failed: missing {} header", API_KEY_HEADER);
      response.setStatus(HttpStatus.UNAUTHORIZED.value());
      response.getWriter().write("Missing API Key");
      return;
    }

    if (expectedApiKey == null || !expectedApiKey.equals(providedApiKey)) {
      log.warn("API Key authentication failed: invalid API key provided");
      response.setStatus(HttpStatus.UNAUTHORIZED.value());
      response.getWriter().write("Invalid API Key");
      return;
    }

    // Marks the request as authenticated so downstream filters can skip authentication.
    var auth = new UsernamePasswordAuthenticationToken("internal-service", null, List.of());
    SecurityContextHolder.getContext().setAuthentication(auth);

    log.debug("API Key authentication successful");
    filterChain.doFilter(request, response);
  }

  private boolean isExternalRequest(HttpServletRequest request) {
    String forwardedFor = request.getHeader("X-Forwarded-For");
    if (forwardedFor == null) return false;
    return !forwardedFor.startsWith("10.")
        && !forwardedFor.startsWith("172.")
        && !forwardedFor.startsWith("192.168.");
  }
}