View Javadoc
1   package com.acumenvelocity.ath.security;
2   
3   import java.io.IOException;
4   import java.util.HashSet;
5   import java.util.Set;
6   
7   import javax.servlet.Filter;
8   import javax.servlet.FilterChain;
9   import javax.servlet.FilterConfig;
10  import javax.servlet.ServletException;
11  import javax.servlet.ServletRequest;
12  import javax.servlet.ServletResponse;
13  import javax.servlet.http.HttpServletRequest;
14  import javax.servlet.http.HttpServletResponse;
15  
16  import com.acumenvelocity.ath.common.Const;
17  import com.acumenvelocity.ath.common.Log;
18  
19  import net.sf.okapi.common.Util;
20  
21  /**
22   * Servlet filter for API key authentication.
23   * Validates X-API-Key header for all requests.
24   */
25  public class ApiKeyAuthFilter implements Filter {
26  
27    private static final String API_KEY_HEADER = "X-API-Key";
28    private static final Set<String> VALID_API_KEYS = new HashSet<>();
29  
30    // Paths that don't require authentication (e.g., health checks, version)
31    private static final Set<String> PUBLIC_PATHS = new HashSet<>();
32  
33    static {
34      // Load API keys from environment or configuration
35      // For production, load from secure configuration service
36      if (!Util.isEmpty(Const.ATH_API_KEY)) {
37        VALID_API_KEYS.add(Const.ATH_API_KEY);
38      }
39  
40      // Configure public paths
41      PUBLIC_PATHS.add("/public");
42      PUBLIC_PATHS.add("/site");
43      PUBLIC_PATHS.add("/api/version");
44      PUBLIC_PATHS.add("/api/openapi.json");
45    }
46  
47    @Override
48    public void init(FilterConfig filterConfig) throws ServletException {
49    }
50  
51    @Override
52    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
53        throws IOException, ServletException {
54  
55      HttpServletRequest httpRequest = (HttpServletRequest) request;
56      HttpServletResponse httpResponse = (HttpServletResponse) response;
57  
58      String path = httpRequest.getRequestURI();
59  
60      // Skip authentication for public paths
61      if (isPublicPath(path)) {
62        chain.doFilter(request, response);
63        return;
64      }
65  
66      // No auth imposed if the key is not specified on the server (e.g. in a local dev environment)
67      if (Util.isEmpty(Const.ATH_API_KEY)) {
68        chain.doFilter(request, response);
69        return;
70      }
71  
72      // Extract API key from header
73      String apiKey = httpRequest.getHeader(API_KEY_HEADER);
74  
75      // Validate API key
76      if (Util.isEmpty(apiKey)) {
77        sendUnauthorizedResponse(httpResponse, "Missing API key");
78        return;
79      }
80  
81      if (!VALID_API_KEYS.contains(apiKey)) {
82        sendUnauthorizedResponse(httpResponse, "Invalid API key");
83        return;
84      }
85  
86      // API key is valid, proceed with request
87      chain.doFilter(request, response);
88    }
89  
90    @Override
91    public void destroy() {
92      // Cleanup if needed
93    }
94  
95    /**
96     * Check if the path is public and doesn't require authentication.
97     */
98    private boolean isPublicPath(String path) {
99      for (String publicPath : PUBLIC_PATHS) {
100       if (path.startsWith(publicPath)) {
101         return true;
102       }
103     }
104 
105     return false;
106   }
107 
108   /**
109    * Send 401 Unauthorized response.
110    */
111   private void sendUnauthorizedResponse(HttpServletResponse response, String message)
112       throws IOException {
113 
114     response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
115     response.setContentType("application/json");
116 
117     response.getWriter().write(
118         Log.format("{\"code\": 401, \"message\": \"{}\"}", message));
119   }
120 }