feat(04-02): wire Spring Security filter chain with JWT auth, bootstrap registration, and refresh endpoint

- JwtAuthenticationFilter extracts JWT from Authorization header or query param, validates via JwtService
- SecurityConfig creates stateless SecurityFilterChain with public/protected endpoint split
- AgentRegistrationController requires bootstrap token, returns accessToken + refreshToken + serverPublicKey
- New POST /agents/{id}/refresh endpoint issues new access JWT from valid refresh token

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-11 20:13:53 +01:00
parent b3b4e62d34
commit 387e2e66b2
3 changed files with 208 additions and 5 deletions

View File

@@ -0,0 +1,72 @@
package com.cameleer3.server.app.security;
import com.cameleer3.server.core.agent.AgentRegistryService;
import com.cameleer3.server.core.security.JwtService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.List;
/**
* JWT authentication filter that extracts and validates JWT tokens from
* the {@code Authorization: Bearer} header or the {@code token} query parameter.
* <p>
* Not annotated {@code @Component} -- constructed explicitly in {@link SecurityConfig}
* to avoid double filter registration.
*/
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private static final Logger log = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
private static final String BEARER_PREFIX = "Bearer ";
private final JwtService jwtService;
private final AgentRegistryService agentRegistryService;
public JwtAuthenticationFilter(JwtService jwtService, AgentRegistryService agentRegistryService) {
this.jwtService = jwtService;
this.agentRegistryService = agentRegistryService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String token = extractToken(request);
if (token != null) {
try {
String agentId = jwtService.validateAndExtractAgentId(token);
if (agentRegistryService.findById(agentId) != null) {
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(agentId, null, List.of());
SecurityContextHolder.getContext().setAuthentication(auth);
} else {
log.debug("JWT valid but agent not found: {}", agentId);
}
} catch (Exception e) {
log.debug("JWT validation failed: {}", e.getMessage());
}
}
chain.doFilter(request, response);
}
private String extractToken(HttpServletRequest request) {
// Check Authorization header first
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith(BEARER_PREFIX)) {
return authHeader.substring(BEARER_PREFIX.length());
}
// Fall back to query parameter (for SSE EventSource API)
return request.getParameter("token");
}
}

View File

@@ -0,0 +1,53 @@
package com.cameleer3.server.app.security;
import com.cameleer3.server.core.agent.AgentRegistryService;
import com.cameleer3.server.core.security.JwtService;
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.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* Spring Security configuration for JWT-based stateless authentication.
* <p>
* Public endpoints: health, agent registration, refresh, API docs, Swagger UI.
* All other endpoints require a valid JWT access token.
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http,
JwtService jwtService,
AgentRegistryService registryService) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/api/v1/health",
"/api/v1/agents/register",
"/api/v1/agents/*/refresh",
"/api/v1/api-docs/**",
"/api/v1/swagger-ui/**",
"/swagger-ui/**",
"/v3/api-docs/**",
"/swagger-ui.html"
).permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(
new JwtAuthenticationFilter(jwtService, registryService),
UsernamePasswordAuthenticationFilter.class
);
return http.build();
}
}