---
phase: 04-security
plan: 02
type: execute
wave: 2
depends_on: ["04-01"]
files_modified:
  - cameleer-server-app/src/main/java/com/cameleer/server/app/security/JwtAuthenticationFilter.java
  - cameleer-server-app/src/main/java/com/cameleer/server/app/security/SecurityConfig.java
  - cameleer-server-app/src/main/java/com/cameleer/server/app/controller/AgentRegistrationController.java
  - cameleer-server-app/src/main/java/com/cameleer/server/app/controller/AgentSseController.java
  - cameleer-server-app/src/main/java/com/cameleer/server/app/config/WebConfig.java
  - cameleer-server-app/src/test/java/com/cameleer/server/app/security/SecurityFilterIT.java
  - cameleer-server-app/src/test/java/com/cameleer/server/app/security/JwtRefreshIT.java
  - cameleer-server-app/src/test/java/com/cameleer/server/app/security/RegistrationSecurityIT.java
  - cameleer-server-app/src/test/java/com/cameleer/server/app/security/BootstrapTokenIT.java
  - cameleer-server-app/src/test/java/com/cameleer/server/app/TestSecurityHelper.java
  - cameleer-server-app/src/test/java/com/cameleer/server/app/security/TestSecurityConfig.java
autonomous: true
requirements:
  - SECU-01
  - SECU-02
  - SECU-05

must_haves:
  truths:
    - "All API endpoints except health, register, and docs reject requests without valid JWT"
    - "POST /register requires bootstrap token in Authorization header, returns JWT + refresh token + Ed25519 public key"
    - "POST /agents/{id}/refresh accepts refresh token and returns new access JWT"
    - "SSE endpoint accepts JWT via ?token= query parameter"
    - "Health endpoint and Swagger UI remain publicly accessible"
  artifacts:
    - path: "cameleer-server-app/src/main/java/com/cameleer/server/app/security/JwtAuthenticationFilter.java"
      provides: "OncePerRequestFilter extracting JWT from header or query param"
    - path: "cameleer-server-app/src/main/java/com/cameleer/server/app/security/SecurityConfig.java"
      provides: "SecurityFilterChain with permitAll for public paths, authenticated for rest"
    - path: "cameleer-server-app/src/main/java/com/cameleer/server/app/controller/AgentRegistrationController.java"
      provides: "Updated register endpoint with bootstrap token validation, JWT issuance, public key"
  key_links:
    - from: "JwtAuthenticationFilter"
      to: "JwtService.validateAndExtractAgentId"
      via: "Filter delegates JWT validation to service"
      pattern: "jwtService\\.validateAndExtractAgentId"
    - from: "SecurityConfig"
      to: "JwtAuthenticationFilter"
      via: "addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)"
      pattern: "addFilterBefore"
    - from: "AgentRegistrationController.register"
      to: "BootstrapTokenValidator.validate"
      via: "Validates bootstrap token before processing registration"
      pattern: "bootstrapTokenValidator\\.validate"
    - from: "AgentRegistrationController.register"
      to: "JwtService.createAccessToken + createRefreshToken"
      via: "Issues tokens in registration response"
      pattern: "jwtService\\.create(Access|Refresh)Token"
---

<objective>
Wire Spring Security into the application: JWT authentication filter, SecurityFilterChain configuration, bootstrap token validation on registration, JWT issuance in registration response, refresh endpoint, and SSE query-parameter authentication. Update existing tests to work with security enabled.

Purpose: Protects all endpoints with JWT authentication while keeping public endpoints accessible and providing the full agent registration-to-authentication flow.
Output: Working security filter chain with protected/public endpoints, registration returns JWT + public key, refresh flow works, all tests pass.
</objective>

<execution_context>
@C:/Users/Hendrik/.claude/get-shit-done/workflows/execute-plan.md
@C:/Users/Hendrik/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/04-security/04-CONTEXT.md
@.planning/phases/04-security/04-RESEARCH.md
@.planning/phases/04-security/04-VALIDATION.md
@.planning/phases/04-security/04-01-SUMMARY.md

@cameleer-server-app/src/main/java/com/cameleer/server/app/controller/AgentRegistrationController.java
@cameleer-server-app/src/main/java/com/cameleer/server/app/controller/AgentSseController.java
@cameleer-server-app/src/main/java/com/cameleer/server/app/config/WebConfig.java
@cameleer-server-app/src/test/java/com/cameleer/server/app/AbstractClickHouseIT.java
@cameleer-server-app/src/test/java/com/cameleer/server/app/controller/AgentRegistrationControllerIT.java

<interfaces>
<!-- From Plan 01 (will exist after execution): -->

From core/security/JwtService.java:
```java
public interface JwtService {
    String createAccessToken(String agentId, String group);
    String createRefreshToken(String agentId, String group);
    String validateAndExtractAgentId(String token); // access tokens only
    String validateRefreshToken(String token);       // refresh tokens only
}
```

From core/security/Ed25519SigningService.java:
```java
public interface Ed25519SigningService {
    String sign(String payload);
    String getPublicKeyBase64();
}
```

From app/security/BootstrapTokenValidator.java:
```java
public class BootstrapTokenValidator {
    public boolean validate(String provided);
}
```

From app/security/SecurityProperties.java:
```java
@ConfigurationProperties(prefix = "security")
public class SecurityProperties {
    long accessTokenExpiryMs;   // default 3600000
    long refreshTokenExpiryMs;  // default 604800000
    String bootstrapToken;      // from CAMELEER_AUTH_TOKEN env
    String bootstrapTokenPrevious; // from CAMELEER_AUTH_TOKEN_PREVIOUS env, nullable
}
```

From core/agent/AgentRegistryService.java:
```java
public class AgentRegistryService {
    public AgentInfo register(String id, String name, String group, String version, List<String> routeIds, Map<String, Object> capabilities);
    public AgentInfo findById(String id);
}
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: SecurityFilterChain + JwtAuthenticationFilter + registration/refresh integration</name>
  <files>
    cameleer-server-app/src/main/java/com/cameleer/server/app/security/JwtAuthenticationFilter.java,
    cameleer-server-app/src/main/java/com/cameleer/server/app/security/SecurityConfig.java,
    cameleer-server-app/src/main/java/com/cameleer/server/app/controller/AgentRegistrationController.java,
    cameleer-server-app/src/main/java/com/cameleer/server/app/controller/AgentSseController.java,
    cameleer-server-app/src/main/java/com/cameleer/server/app/config/WebConfig.java
  </files>
  <action>
    1. Create `JwtAuthenticationFilter extends OncePerRequestFilter` (NOT annotated @Component -- constructed in SecurityConfig to avoid double registration):
       - Constructor takes `JwtService` and `AgentRegistryService`
       - `doFilterInternal`: extract token via `extractToken(request)`, if token present: call `jwtService.validateAndExtractAgentId(token)`, verify agent exists via `agentRegistry.findById(agentId)`, if valid set `UsernamePasswordAuthenticationToken(agentId, null, List.of())` in `SecurityContextHolder`. If any exception, log debug and do NOT set auth (Spring Security rejects). Always call `chain.doFilter(request, response)`.
       - `extractToken(request)`: first check `Authorization` header for `Bearer ` prefix, then check `request.getParameter("token")` for SSE query param. Return null if neither.

    2. Create `SecurityConfig` as `@Configuration @EnableWebSecurity`:
       - Single `@Bean SecurityFilterChain filterChain(HttpSecurity http, JwtService jwtService, AgentRegistryService registryService)`:
         - `csrf(AbstractHttpConfigurer::disable)` -- REST API, no browser forms
         - `sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))`
         - `authorizeHttpRequests`: permitAll for `/api/v1/health`, `/api/v1/agents/register`, `/api/v1/api-docs/**`, `/api/v1/swagger-ui/**`, `/swagger-ui/**`, `/v3/api-docs/**`, `/swagger-ui.html`. `anyRequest().authenticated()`.
         - `addFilterBefore(new JwtAuthenticationFilter(jwtService, registryService), UsernamePasswordAuthenticationFilter.class)`
       - Also disable default form login and httpBasic: `.formLogin(AbstractHttpConfigurer::disable).httpBasic(AbstractHttpConfigurer::disable)`

    3. Update `AgentRegistrationController.register()`:
       - Add `BootstrapTokenValidator`, `JwtService`, `Ed25519SigningService` as constructor dependencies
       - Before processing registration body, extract bootstrap token from `Authorization: Bearer <token>` header (use `@RequestHeader("Authorization")` or extract from HttpServletRequest). If missing or invalid (`bootstrapTokenValidator.validate()` returns false), return `401 Unauthorized` with no detail body.
       - After successful registration, generate tokens: `jwtService.createAccessToken(agentId, group)` and `jwtService.createRefreshToken(agentId, group)`
       - Update response map: replace `"serverPublicKey", null` with `"serverPublicKey", ed25519SigningService.getPublicKeyBase64()`. Add `"accessToken"` and `"refreshToken"` fields.

    4. Add a new refresh endpoint in `AgentRegistrationController` (or a new controller -- keep it in the same controller since it's agent auth flow):
       - `POST /api/v1/agents/{id}/refresh` with request body containing `{"refreshToken": "..."}`.
       - Validate refresh token via `jwtService.validateRefreshToken(token)`, extract agentId, verify it matches path `{id}`, verify agent exists.
       - Return new access token: `{"accessToken": "..."}`.
       - Return 401 for invalid/expired refresh token, 404 for unknown agent.
       - NOTE: This endpoint must be AUTHENTICATED (requires valid JWT OR the refresh token itself). Per the user decision, the refresh endpoint uses the refresh token for auth, so add `/api/v1/agents/*/refresh` to permitAll in SecurityConfig, and validate the refresh token in the controller itself.

    5. Update `AgentSseController.events()`:
       - The SSE endpoint uses `?token=<jwt>` query parameter. The `JwtAuthenticationFilter` already handles this (extracts from query param). No changes needed to the controller itself -- Spring Security handles auth via the filter.
       - However, verify the SSE endpoint path `/api/v1/agents/{id}/events` is NOT in permitAll (it should require JWT auth).

    6. Update `WebConfig` if needed: The `ProtocolVersionInterceptor` excluded paths should align with Spring Security public paths. The SSE events path is already excluded from protocol version check (Phase 3 decision). Verify no conflicts.
  </action>
  <verify>
    <automated>cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn clean compile -pl cameleer-server-app</automated>
  </verify>
  <done>
    - SecurityConfig creates stateless filter chain with correct public/protected path split
    - JwtAuthenticationFilter extracts JWT from header or query param, validates, sets SecurityContext
    - Registration endpoint requires bootstrap token, returns accessToken + refreshToken + serverPublicKey
    - Refresh endpoint issues new access token from valid refresh token
    - Application compiles with all security wiring
  </done>
</task>

<task type="auto">
  <name>Task 2: Security integration tests + existing test adaptation</name>
  <files>
    cameleer-server-app/src/test/java/com/cameleer/server/app/TestSecurityHelper.java,
    cameleer-server-app/src/test/java/com/cameleer/server/app/security/TestSecurityConfig.java,
    cameleer-server-app/src/test/java/com/cameleer/server/app/security/SecurityFilterIT.java,
    cameleer-server-app/src/test/java/com/cameleer/server/app/security/JwtRefreshIT.java,
    cameleer-server-app/src/test/java/com/cameleer/server/app/security/RegistrationSecurityIT.java,
    cameleer-server-app/src/test/java/com/cameleer/server/app/security/BootstrapTokenIT.java,
    cameleer-server-app/src/test/java/com/cameleer/server/app/controller/AgentRegistrationControllerIT.java,
    cameleer-server-app/src/test/java/com/cameleer/server/app/controller/ExecutionControllerIT.java,
    cameleer-server-app/src/test/java/com/cameleer/server/app/controller/DiagramControllerIT.java,
    cameleer-server-app/src/test/java/com/cameleer/server/app/controller/MetricsControllerIT.java,
    cameleer-server-app/src/test/java/com/cameleer/server/app/controller/BackpressureIT.java,
    cameleer-server-app/src/test/java/com/cameleer/server/app/controller/DiagramRenderControllerIT.java,
    cameleer-server-app/src/test/java/com/cameleer/server/app/controller/DetailControllerIT.java,
    cameleer-server-app/src/test/java/com/cameleer/server/app/controller/SearchControllerIT.java,
    cameleer-server-app/src/test/java/com/cameleer/server/app/controller/AgentCommandControllerIT.java,
    cameleer-server-app/src/test/java/com/cameleer/server/app/controller/AgentSseControllerIT.java,
    cameleer-server-app/src/test/java/com/cameleer/server/app/storage/DiagramLinkingIT.java,
    cameleer-server-app/src/test/java/com/cameleer/server/app/storage/IngestionSchemaIT.java,
    cameleer-server-app/src/test/java/com/cameleer/server/app/interceptor/ProtocolVersionIT.java,
    cameleer-server-app/src/test/java/com/cameleer/server/app/controller/OpenApiIT.java,
    cameleer-server-app/src/test/java/com/cameleer/server/app/controller/ForwardCompatIT.java,
    cameleer-server-app/src/test/java/com/cameleer/server/app/controller/HealthControllerIT.java
  </files>
  <action>
    1. Replace the Plan 01 temporary `TestSecurityConfig` (permit-all) with real security active in tests. Remove the permit-all override so tests run with actual security enforcement.

    2. Create `TestSecurityHelper` utility class in test root:
       - Autowire `JwtService` and `AgentRegistryService`
       - `registerTestAgent(String agentId)`: calls `registryService.register(agentId, "test", "test-group", "1.0", List.of(), Map.of())` and returns `jwtService.createAccessToken(agentId, "test-group")`
       - `authHeaders(String jwt)`: returns HttpHeaders with `Authorization: Bearer <jwt>` and `X-Cameleer-Protocol-Version: 1` and `Content-Type: application/json`
       - `bootstrapHeaders()`: returns HttpHeaders with `Authorization: Bearer test-bootstrap-token` and `X-Cameleer-Protocol-Version: 1` and `Content-Type: application/json`
       - Make it a Spring `@Component` so it can be autowired in test classes

    3. Update ALL existing IT classes (17 files) to use JWT authentication:
       - Autowire `TestSecurityHelper`
       - In `@BeforeEach` or at test start, call `helper.registerTestAgent("test-agent-<testclass>")` to get a JWT
       - Replace all `protocolHeaders()` calls with headers that include the JWT Bearer token
       - For HealthControllerIT and OpenApiIT: verify these still work WITHOUT JWT (they're public endpoints)
       - For AgentRegistrationControllerIT: update `registerAgent()` helper to use bootstrap token header, verify response now includes `accessToken`, `refreshToken`, `serverPublicKey` (non-null)

    4. Create new security-specific integration tests:

       `SecurityFilterIT` (extends AbstractClickHouseIT):
       - Test: GET /api/v1/agents without JWT returns 401 or 403
       - Test: GET /api/v1/agents with valid JWT returns 200
       - Test: GET /api/v1/health without JWT returns 200 (public)
       - Test: POST /api/v1/data/executions without JWT returns 401 or 403
       - Test: Request with expired JWT returns 401 or 403
       - Test: Request with malformed JWT returns 401 or 403

       `BootstrapTokenIT` (extends AbstractClickHouseIT):
       - Test: POST /register without bootstrap token returns 401
       - Test: POST /register with wrong bootstrap token returns 401
       - Test: POST /register with correct bootstrap token returns 200 with tokens
       - Test: POST /register with previous bootstrap token returns 200 (dual-token rotation)

       `RegistrationSecurityIT` (extends AbstractClickHouseIT):
       - Test: Registration response contains non-null `serverPublicKey` (Base64 string)
       - Test: Registration response contains `accessToken` and `refreshToken`
       - Test: Access token from registration can be used to access protected endpoints

       `JwtRefreshIT` (extends AbstractClickHouseIT):
       - Test: POST /agents/{id}/refresh with valid refresh token returns new access token
       - Test: POST /agents/{id}/refresh with expired refresh token returns 401
       - Test: POST /agents/{id}/refresh with access token (wrong type) returns 401
       - Test: POST /agents/{id}/refresh with mismatched agent ID returns 401
       - Test: New access token from refresh can access protected endpoints
  </action>
  <verify>
    <automated>cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn clean verify</automated>
  </verify>
  <done>
    - All 17 existing ITs pass with JWT authentication
    - SecurityFilterIT: protected endpoints reject unauthenticated requests, public endpoints remain open
    - BootstrapTokenIT: registration requires valid bootstrap token, supports dual-token rotation
    - RegistrationSecurityIT: registration returns public key + tokens
    - JwtRefreshIT: refresh flow issues new access tokens, rejects invalid refresh tokens
    - Full `mvn clean verify` is green
  </done>
</task>

</tasks>

<verification>
mvn clean verify
All existing tests pass with JWT auth. New security ITs validate protected/public endpoint split, bootstrap token flow, registration security, and refresh flow.
</verification>

<success_criteria>
- Protected endpoints return 401/403 without JWT, 200 with valid JWT
- Public endpoints (health, register, docs) remain accessible without JWT
- Registration requires bootstrap token, returns accessToken + refreshToken + serverPublicKey
- Refresh endpoint issues new access JWT from valid refresh token
- SSE endpoint accepts JWT via query parameter
- All 17 existing ITs adapted and passing
- 4 new security ITs passing
</success_criteria>

<output>
After completion, create `.planning/phases/04-security/04-02-SUMMARY.md`
</output>
