3 plans in 2 waves covering all 5 SECU requirements: - Plan 01 (W1): Security service foundation (JWT, Ed25519, bootstrap token) - Plan 02 (W2): Spring Security filter chain, endpoint protection, test adaptation - Plan 03 (W2): SSE payload signing with Ed25519 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
18 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 04-security | 02 | execute | 2 |
|
|
true |
|
|
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.
<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>
@.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@cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/AgentRegistrationController.java @cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/AgentSseController.java @cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/WebConfig.java @cameleer3-server-app/src/test/java/com/cameleer3/server/app/AbstractClickHouseIT.java @cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/AgentRegistrationControllerIT.java
From core/security/JwtService.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:
public interface Ed25519SigningService {
String sign(String payload);
String getPublicKeyBase64();
}
From app/security/BootstrapTokenValidator.java:
public class BootstrapTokenValidator {
public boolean validate(String provided);
}
From app/security/SecurityProperties.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:
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);
}
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.
cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn clean compile -pl cameleer3-server-app
- 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
Task 2: Security integration tests + existing test adaptation
cameleer3-server-app/src/test/java/com/cameleer3/server/app/TestSecurityHelper.java,
cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/TestSecurityConfig.java,
cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/SecurityFilterIT.java,
cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/JwtRefreshIT.java,
cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/RegistrationSecurityIT.java,
cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/BootstrapTokenIT.java,
cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/AgentRegistrationControllerIT.java,
cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/ExecutionControllerIT.java,
cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/DiagramControllerIT.java,
cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/MetricsControllerIT.java,
cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/BackpressureIT.java,
cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/DiagramRenderControllerIT.java,
cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/DetailControllerIT.java,
cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/SearchControllerIT.java,
cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/AgentCommandControllerIT.java,
cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/AgentSseControllerIT.java,
cameleer3-server-app/src/test/java/com/cameleer3/server/app/storage/DiagramLinkingIT.java,
cameleer3-server-app/src/test/java/com/cameleer3/server/app/storage/IngestionSchemaIT.java,
cameleer3-server-app/src/test/java/com/cameleer3/server/app/interceptor/ProtocolVersionIT.java,
cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/OpenApiIT.java,
cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/ForwardCompatIT.java,
cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/HealthControllerIT.java
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
cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn clean verify
- 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
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.
<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>