Rename Java packages from com.cameleer3 to com.cameleer, module directories from cameleer3-* to cameleer-*, and all references throughout workflows, Dockerfiles, docs, migrations, and pom.xml. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
31 KiB
Phase 4: Security - Research
Researched: 2026-03-11 Domain: Spring Security JWT authentication, Ed25519 signing, bootstrap token validation Confidence: HIGH
Summary
This phase adds authentication and integrity protection to the Cameleer server. The implementation uses Spring Security 6.4.3 (managed by Spring Boot 3.4.3) with a custom OncePerRequestFilter for JWT validation, JDK 17 built-in Ed25519 for signing SSE payloads, and environment variable-based bootstrap tokens for agent registration. The approach is deliberately simple -- no OAuth2 resource server, no external identity provider, just symmetric HMAC JWTs for access control and Ed25519 signatures for payload integrity.
The existing codebase has clear integration points: AgentRegistrationController.register() already returns serverPublicKey: null as a placeholder, SseConnectionManager.onCommandReady() is the signing hook for SSE events, and WebConfig already defines excluded paths that align with the public endpoint list. Spring Security's SecurityFilterChain replaces the need for hand-rolled authorization logic -- endpoints are protected by default, with explicit permitAll() for health, register, and docs.
Primary recommendation: Use Nimbus JOSE+JWT (transitive via spring-boot-starter-security) with HMAC-SHA256 for JWTs, JDK built-in KeyPairGenerator.getInstance("Ed25519") for signing keypairs, and a single SecurityFilterChain bean with a custom JwtAuthenticationFilter extends OncePerRequestFilter added before UsernamePasswordAuthenticationFilter.
<user_constraints>
User Constraints (from CONTEXT.md)
Locked Decisions
- Single shared token from
CAMELEER_AUTH_TOKENenv var -- no config file fallback - Agent passes bootstrap token via
Authorization: Bearer <token>header onPOST /register - Server returns
401 Unauthorizedwhen token is missing or invalid -- no detail about what's wrong - Server fails fast on startup if
CAMELEER_AUTH_TOKENis not set -- prevents running insecure - Hot rotation via dual-token overlap: support
CAMELEER_AUTH_TOKEN_PREVIOUSenv var, server accepts both during rotation window - Access JWT expires after 1 hour
- Separate refresh token with 7-day expiry, issued alongside access JWT at registration
- Agent calls
POST /api/v1/agents/{id}/refreshwith refresh token to get new access JWT - JWT claims:
sub= agentId, custom claim for group - Registration response includes both access JWT and refresh token (replaces current
serverPublicKey: nullplaceholder with actual public key) - Ephemeral keypair generated fresh each server startup -- no persistence needed
- Agents receive public key during registration; must re-register after server restart to get new key
- Signature included as a
signaturefield in the SSE event data JSON -- agent verifies payload minus signature field - All command types signed (config-update, deep-trace, replay) -- uniform security model
- Public (no JWT):
GET /health,POST /register(uses bootstrap token), OpenAPI/Swagger UI docs - Protected (JWT required): all other endpoints including ingestion (
/data/**), search, agent management, commands - SSE connections authenticated via JWT as query parameter:
/agents/{id}/events?token=<jwt>(EventSource API doesn't support custom headers) - Spring Security filter chain (
spring-boot-starter-security) with customJwtAuthenticationFilter
Claude's Discretion
- JWT signing algorithm (HMAC with server secret vs Ed25519 for JWT too)
- Nimbus JOSE+JWT vs jjwt vs other JWT library
- Ed25519 implementation library (Bouncy Castle vs JDK built-in)
- Spring Security configuration details (SecurityFilterChain bean, permit patterns)
- Refresh token storage mechanism (in-memory map, agent registry, or stateless)
Deferred Ideas (OUT OF SCOPE)
- User/UI authentication -- belongs with web UI in v2
- Role-based access control (admin vs agent vs viewer) -- future phase
- Token revocation list -- evaluate after v1 usage patterns
- Mutual TLS as additional transport security -- infrastructure concern, not application layer
- Key rotation API endpoint -- adds attack surface, stick with restart-based rotation for v1 </user_constraints>
<phase_requirements>
Phase Requirements
| ID | Description | Research Support |
|---|---|---|
| SECU-01 (#23) | All API endpoints (except health and register) require valid JWT Bearer token | Spring Security SecurityFilterChain with permitAll() for public paths, custom JwtAuthenticationFilter for JWT validation |
| SECU-02 (#24) | JWT refresh flow via POST /api/v1/agents/{id}/refresh |
Nimbus JOSE+JWT for JWT creation/validation, stateless refresh tokens with longer expiry |
| SECU-03 (#25) | Server generates Ed25519 keypair; public key delivered at registration | JDK 17 built-in KeyPairGenerator.getInstance("Ed25519"), Base64-encoded public key in registration response |
| SECU-04 (#26) | All config-update and replay SSE payloads signed with Ed25519 private key | JDK 17 Signature.getInstance("Ed25519"), signing hook in SseConnectionManager.onCommandReady() |
| SECU-05 (#27) | Bootstrap token from CAMELEER_AUTH_TOKEN env var validates initial agent registration |
@Value injection with startup validation, checked before registration logic |
| </phase_requirements> |
Standard Stack
Core
| Library | Version | Purpose | Why Standard |
|---|---|---|---|
| spring-boot-starter-security | 3.4.3 (managed) | Security filter chain, authentication framework | Spring Boot's standard security starter; brings Spring Security 6.4.3 |
| nimbus-jose-jwt | 9.37+ (transitive via spring-security-oauth2-jose) | JWT creation, signing, parsing, verification | Spring Security's own JWT library; already in the Spring ecosystem |
| JDK Ed25519 | JDK 17 built-in | Ed25519 keypair generation and signing | Native support since Java 15 via java.security.KeyPairGenerator and java.security.Signature; no external dependency needed |
Supporting
| Library | Version | Purpose | When to Use |
|---|---|---|---|
| spring-boot-starter-test | 3.4.3 (managed) | MockMvc with security context, @WithMockUser support |
Already present; tests gain security testing support automatically |
Alternatives Considered
| Instead of | Could Use | Tradeoff |
|---|---|---|
| Nimbus JOSE+JWT | JJWT (io.jsonwebtoken) | JJWT is simpler API but doesn't support JWE; Nimbus is already a Spring Security transitive dependency so adding it explicitly costs zero |
| JDK Ed25519 | Bouncy Castle | Bouncy Castle adds ~5MB dependency for something JDK 17 does natively; only needed if targeting Java < 15 |
| HMAC-SHA256 for JWT | Ed25519 for JWT too | HMAC is simpler for server-only JWT creation/validation (no key distribution needed); Ed25519 for JWT only matters if a third party validates JWTs |
Discretion Recommendations:
- JWT signing algorithm: Use HMAC-SHA256 (HS256). The server both creates and validates JWTs -- no external party needs to verify them. HMAC is simpler (one shared secret vs keypair), and the 256-bit secret can be generated randomly at startup (ephemeral, like the Ed25519 key). This keeps JWT signing separate from Ed25519 payload signing -- cleaner separation of concerns.
- JWT library: Use Nimbus JOSE+JWT. It is Spring Security's transitive dependency, so it costs nothing extra. Adding
spring-boot-starter-securitybringsspring-security-oauth2-josewhich includes Nimbus. Alternatively, addcom.nimbusds:nimbus-jose-jwtdirectly if not pulling the full OAuth2 stack. - Ed25519 library: Use JDK built-in. Zero external dependencies, native performance, well-tested in JDK 17+.
- Refresh token storage: Use stateless signed refresh tokens (also HMAC-signed JWTs with different claims/expiry). This avoids any in-memory storage for refresh tokens and scales naturally. The refresh token is just a JWT with
type=refresh,sub=agentId, and 7-day expiry. On refresh, validate the refresh JWT, check agent still exists, issue new access JWT.
Installation (add to cameleer-server-app pom.xml):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.47</version>
</dependency>
Note: If spring-boot-starter-security brings Nimbus transitively (via spring-security-oauth2-jose), the explicit Nimbus dependency is optional. However, since we are NOT using Spring Security's OAuth2 resource server (we have a custom filter), adding Nimbus explicitly ensures it is available. Check if the starter alone suffices; if not, add Nimbus directly.
Architecture Patterns
Recommended Project Structure
cameleer-server-core/src/main/java/com/cameleer/server/core/
security/
JwtService.java # Interface: createAccessToken, createRefreshToken, validateToken, extractAgentId
Ed25519SigningService.java # Interface: sign(payload) -> signature, getPublicKeyBase64()
cameleer-server-app/src/main/java/com/cameleer/server/app/
security/
JwtServiceImpl.java # Nimbus JOSE+JWT HMAC implementation
Ed25519SigningServiceImpl.java # JDK Ed25519 keypair + signing implementation
JwtAuthenticationFilter.java # OncePerRequestFilter: extract JWT, validate, set SecurityContext
BootstrapTokenValidator.java # Validates bootstrap token(s) from env vars
SecurityConfig.java # SecurityFilterChain bean, permit patterns
config/
SecurityProperties.java # @ConfigurationProperties for token expiry, etc.
Pattern 1: SecurityFilterChain with Custom JWT Filter
What: Single SecurityFilterChain bean that permits public paths and requires authentication everywhere else, with a custom JwtAuthenticationFilter added before Spring's UsernamePasswordAuthenticationFilter.
When to use: Always -- this is the sole security configuration.
Example:
// Source: Spring Security 6.4 official docs + Spring Boot 3.4 patterns
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http,
JwtAuthenticationFilter jwtFilter) throws Exception {
http
.csrf(csrf -> csrf.disable()) // REST API, no browser forms
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/health").permitAll()
.requestMatchers("/api/v1/agents/register").permitAll()
.requestMatchers("/api/v1/api-docs/**").permitAll()
.requestMatchers("/api/v1/swagger-ui/**").permitAll()
.requestMatchers("/swagger-ui/**").permitAll()
.requestMatchers("/v3/api-docs/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
Pattern 2: JwtAuthenticationFilter (OncePerRequestFilter)
What: Extracts JWT from Authorization: Bearer <token> header (or token query param for SSE), validates it, and sets a Spring Security Authentication object in the SecurityContextHolder.
When to use: Every authenticated request.
Example:
// Custom filter pattern for Spring Security 6.x
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final AgentRegistryService agentRegistry;
@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);
// Verify agent still exists
if (agentRegistry.findById(agentId) != null) {
var auth = new UsernamePasswordAuthenticationToken(
agentId, null, List.of());
SecurityContextHolder.getContext().setAuthentication(auth);
}
} catch (Exception e) {
// Invalid token -- do not set authentication, Spring Security will reject
}
}
chain.doFilter(request, response);
}
private String extractToken(HttpServletRequest request) {
// 1. Check Authorization header
String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
return header.substring(7);
}
// 2. Check query parameter (for SSE EventSource)
return request.getParameter("token");
}
}
Pattern 3: Ed25519 Payload Signing in SSE Delivery
What: Before sending an SSE event, serialize the payload JSON, compute an Ed25519 signature, add the signature field to the JSON, then send.
When to use: Every SSE command delivery (config-update, deep-trace, replay).
Example:
// JDK 17 built-in Ed25519 signing
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("Ed25519");
KeyPair keyPair = keyGen.generateKeyPair();
// Signing
Signature signer = Signature.getInstance("Ed25519");
signer.initSign(keyPair.getPrivate());
signer.update(payloadJson.getBytes(StandardCharsets.UTF_8));
byte[] signatureBytes = signer.sign();
String signatureBase64 = Base64.getEncoder().encodeToString(signatureBytes);
// Public key for registration response
String publicKeyBase64 = Base64.getEncoder().encodeToString(
keyPair.getPublic().getEncoded()); // X.509 SubjectPublicKeyInfo DER encoding
Pattern 4: Bootstrap Token Validation
What: Check Authorization: Bearer <token> on POST /register against CAMELEER_AUTH_TOKEN (and optionally CAMELEER_AUTH_TOKEN_PREVIOUS).
When to use: Only on the registration endpoint.
Example:
// Startup validation in a @Component or @Bean init
@Value("${CAMELEER_AUTH_TOKEN:#{null}}")
private String bootstrapToken;
@PostConstruct
void validateBootstrapToken() {
if (bootstrapToken == null || bootstrapToken.isBlank()) {
throw new IllegalStateException(
"CAMELEER_AUTH_TOKEN environment variable must be set");
}
}
Anti-Patterns to Avoid
- Registering JwtAuthenticationFilter as a @Bean without @Component exclusion: If marked as
@Component, Spring Boot will register it as a global servlet filter AND in the security chain, running it twice. Either do NOT annotate it as@Component(construct it manually in theSecurityConfigbean) or useFilterRegistrationBeanto disable auto-registration. - Checking JWT on every request including permitAll paths: The filter runs on all requests, but should gracefully skip validation for public paths (just call
chain.doFilterif no token present -- Spring Security's authorization rules handle the rest). - Storing refresh tokens in-memory: Unnecessarily complex and lost on restart. Stateless signed refresh tokens are sufficient.
- Using Ed25519 for JWT signing: Adds complexity (key distribution, asymmetric operations) for no benefit when only the server creates and validates JWTs.
Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---|---|---|---|
| JWT creation/validation | Custom token format or Base64 JSON | Nimbus JOSE+JWT SignedJWT + MACSigner/MACVerifier |
Handles algorithm validation, claim parsing, expiry checks, type-safe builders |
| Request authentication | Custom servlet filter checking headers manually | Spring Security SecurityFilterChain + OncePerRequestFilter |
Handles CORS, CSRF disabling, session management, exception handling, path matching |
| Ed25519 signing | Hand-rolled crypto or custom signature format | JDK java.security.Signature + java.security.KeyPairGenerator |
Audited, constant-time, handles DER encoding properly |
| Constant-time token comparison | String.equals() for bootstrap token |
MessageDigest.isEqual() |
Prevents timing attacks on bootstrap token validation |
| Public key encoding | Custom byte formatting | PublicKey.getEncoded() + Base64 |
Standard X.509 SubjectPublicKeyInfo DER format, interoperable with any Ed25519 library |
Key insight: Cryptographic code has an extraordinary surface area for subtle bugs (timing attacks, encoding mismatches, algorithm confusion). Every piece should use battle-tested library methods.
Common Pitfalls
Pitfall 1: Double Filter Registration
What goes wrong: Annotating JwtAuthenticationFilter with @Component causes Spring Boot to auto-register it as a global servlet filter AND Spring Security adds it to the filter chain, resulting in the filter executing twice per request.
Why it happens: Spring Boot auto-detects @Component classes that extend Filter and registers them globally.
How to avoid: Do NOT annotate the filter as @Component. Instead, construct it in SecurityConfig and pass it to addFilterBefore(). If you must use @Component, add a FilterRegistrationBean that disables auto-registration.
Warning signs: Filter logging messages appear twice per request; 401 responses on valid tokens (filter runs before SecurityFilterChain on second pass).
Pitfall 2: Spring Security Blocking Existing Tests
What goes wrong: Adding spring-boot-starter-security immediately makes all endpoints return 401/403 in existing integration tests.
Why it happens: Spring Security's default configuration denies all requests. Existing tests don't include JWT tokens.
How to avoid: Two approaches: (1) Add @WithMockUser or test-specific security configuration for existing tests, or (2) set a test-profile application-test.yml property with a known bootstrap token and have test helpers generate valid JWTs. Prefer option (2) for realistic security testing.
Warning signs: All existing ITs start failing with 401 after adding the security starter.
Pitfall 3: SSE Token in URL Logged/Cached
What goes wrong: JWT passed as query parameter ?token=<jwt> appears in server access logs, proxy logs, and browser history.
Why it happens: Query parameters are part of the URL, which is logged by default.
How to avoid: Use short-lived access JWTs (1 hour is fine). Consider filtering the token parameter from access logs. The EventSource API limitation makes this unavoidable -- document it as a known tradeoff.
Warning signs: JWT tokens visible in plain text in log files.
Pitfall 4: Timing Attack on Bootstrap Token
What goes wrong: Using String.equals() for bootstrap token comparison leaks token length/prefix via timing side-channel.
Why it happens: String.equals() short-circuits on first mismatch.
How to avoid: Use MessageDigest.isEqual(a.getBytes(), b.getBytes()) for constant-time comparison.
Warning signs: None visible in normal operation -- this is a preventive measure.
Pitfall 5: Ed25519 Signature Field Ordering
What goes wrong: Agent cannot verify signature because JSON field ordering differs between signing and verification.
Why it happens: JSON object field order is not guaranteed. If the server signs a different serialization than the agent verifies, signatures won't match.
How to avoid: Sign the JSON payload WITHOUT the signature field (sign the payload as-is before adding the signature). Document clearly: "signature is computed over the data field value of the SSE event, excluding the signature key". Use a canonical approach: sign the payload JSON string, then wrap it in an outer object with data and signature fields.
Warning signs: Signature verification fails intermittently or consistently on the agent side.
Pitfall 6: Forgetting to Exclude Actuator/Springdoc Paths
What goes wrong: Health endpoint returns 401 because the SecurityFilterChain doesn't match the actuator path format.
Why it happens: Actuator's base path is configured as /api/v1 in this project (see management.endpoints.web.base-path), so health is at /api/v1/health. Springdoc paths may also vary depending on configuration.
How to avoid: Ensure requestMatchers covers: /api/v1/health, /api/v1/api-docs/**, /api/v1/swagger-ui/**, /swagger-ui/**, /v3/api-docs/** (springdoc internal redirects).
Warning signs: Health checks fail, Swagger UI returns 401.
Code Examples
JWT Creation with Nimbus JOSE+JWT (HMAC-SHA256)
// Source: https://connect2id.com/products/nimbus-jose-jwt/examples/jwt-with-hmac
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.*;
import com.nimbusds.jwt.*;
import java.util.Date;
// Generate a random 256-bit secret at startup
byte[] secret = new byte[32];
new java.security.SecureRandom().nextBytes(secret);
// Create access token
JWTClaimsSet claims = new JWTClaimsSet.Builder()
.subject(agentId) // sub = agentId
.claim("group", group) // custom claim
.claim("type", "access") // distinguish from refresh
.issueTime(new Date())
.expirationTime(new Date(System.currentTimeMillis() + 3600_000)) // 1 hour
.build();
SignedJWT jwt = new SignedJWT(
new JWSHeader(JWSAlgorithm.HS256),
claims);
jwt.sign(new MACSigner(secret));
String tokenString = jwt.serialize();
// Validate token
SignedJWT parsed = SignedJWT.parse(tokenString);
boolean valid = parsed.verify(new MACVerifier(secret));
// Then check: claims.getExpirationTime().after(new Date())
// Then check: claims.getStringClaim("type").equals("access")
Ed25519 Keypair Generation and Signing (JDK 17)
// Source: https://howtodoinjava.com/java15/java-eddsa-example/
import java.security.*;
import java.util.Base64;
import java.nio.charset.StandardCharsets;
// Generate ephemeral keypair at startup
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("Ed25519");
KeyPair keyPair = keyGen.generateKeyPair();
// Export public key as Base64 (X.509 SubjectPublicKeyInfo DER)
String publicKeyBase64 = Base64.getEncoder().encodeToString(
keyPair.getPublic().getEncoded());
// Sign a payload
Signature signer = Signature.getInstance("Ed25519");
signer.initSign(keyPair.getPrivate());
signer.update(payloadJson.getBytes(StandardCharsets.UTF_8));
byte[] sig = signer.sign();
String signatureBase64 = Base64.getEncoder().encodeToString(sig);
SecurityFilterChain Configuration
// Source: Spring Security 6.4 reference docs
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http,
JwtService jwtService,
AgentRegistryService registry) throws Exception {
JwtAuthenticationFilter jwtFilter = new JwtAuthenticationFilter(jwtService, registry);
http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/api/v1/health",
"/api/v1/agents/register",
"/api/v1/api-docs/**",
"/api/v1/swagger-ui/**",
"/swagger-ui/**",
"/v3/api-docs/**"
).permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
Bootstrap Token Validation with Constant-Time Comparison
import java.security.MessageDigest;
import java.nio.charset.StandardCharsets;
public boolean validateBootstrapToken(String provided) {
byte[] providedBytes = provided.getBytes(StandardCharsets.UTF_8);
byte[] expectedBytes = bootstrapToken.getBytes(StandardCharsets.UTF_8);
boolean match = MessageDigest.isEqual(providedBytes, expectedBytes);
if (!match && previousBootstrapToken != null) {
byte[] previousBytes = previousBootstrapToken.getBytes(StandardCharsets.UTF_8);
match = MessageDigest.isEqual(providedBytes, previousBytes);
}
return match;
}
State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|---|---|---|---|
WebSecurityConfigurerAdapter |
SecurityFilterChain bean |
Spring Security 5.7 / Spring Boot 3.0 | Must use lambda-style HttpSecurity configuration |
antMatchers() |
requestMatchers() |
Spring Security 6.0 | Method name changed; old code won't compile |
| Ed25519 via Bouncy Castle | JDK built-in Ed25519 | Java 15 (JEP 339) | No external dependency needed for EdDSA |
| Session-based auth | Stateless JWT | Architectural pattern | SessionCreationPolicy.STATELESS mandatory for REST APIs |
Deprecated/outdated:
WebSecurityConfigurerAdapter: Removed in Spring Security 6.0. UseSecurityFilterChainbean instead.antMatchers()/mvcMatchers(): Replaced byrequestMatchers()in Spring Security 6.0.authorizeRequests(): Replaced byauthorizeHttpRequests()in Spring Security 6.0.
Open Questions
-
Nimbus JOSE+JWT transitive availability
- What we know:
spring-boot-starter-securitybrings Spring Security 6.4.3. Ifspring-security-oauth2-joseis on the classpath, Nimbus is available transitively. - What's unclear: Whether the base
spring-boot-starter-security(without OAuth2 resource server) includes Nimbus. - Recommendation: Add
com.nimbusds:nimbus-jose-jwtexplicitly as a dependency. This costs nothing if already transitive and ensures availability if not. Version 9.47 is current and compatible.
- What we know:
-
Existing test adaptation scope
- What we know: 21 existing integration tests use
TestRestTemplatewithout any auth headers. All will fail when security is enabled. - What's unclear: Exact effort to adapt all tests.
- Recommendation: Create a test utility class that generates valid test JWTs and bootstrap tokens. Set
CAMELEER_AUTH_TOKEN=test-tokeninapplication-test.yml. Add JWT header to all test HTTP calls via a shared helper method.
- What we know: 21 existing integration tests use
Validation Architecture
Test Framework
| Property | Value |
|---|---|
| Framework | JUnit 5 + Spring Boot Test (spring-boot-starter-test) |
| Config file | cameleer-server-app/src/test/resources/application-test.yml |
| Quick run command | mvn test -pl cameleer-server-app -Dtest=Security*Test -Dsurefire.reuseForks=false |
| Full suite command | mvn clean verify |
Phase Requirements to Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|---|---|---|---|---|
| SECU-01 | Protected endpoints reject requests without JWT; public endpoints accessible | integration | mvn test -pl cameleer-server-app -Dtest=SecurityFilterIT -Dsurefire.reuseForks=false |
No -- Wave 0 |
| SECU-02 | Refresh endpoint issues new access JWT from valid refresh token | integration | mvn test -pl cameleer-server-app -Dtest=JwtRefreshIT -Dsurefire.reuseForks=false |
No -- Wave 0 |
| SECU-03 | Ed25519 keypair generated at startup; public key in registration response | integration | mvn test -pl cameleer-server-app -Dtest=RegistrationSecurityIT -Dsurefire.reuseForks=false |
No -- Wave 0 |
| SECU-04 | SSE payloads carry valid Ed25519 signature | integration | mvn test -pl cameleer-server-app -Dtest=SseSigningIT -Dsurefire.reuseForks=false |
No -- Wave 0 |
| SECU-05 | Bootstrap token required for registration; rejects invalid/missing tokens | integration | mvn test -pl cameleer-server-app -Dtest=BootstrapTokenIT -Dsurefire.reuseForks=false |
No -- Wave 0 |
| N/A | JWT creation, validation, expiry logic | unit | mvn test -pl cameleer-server-app -Dtest=JwtServiceTest -Dsurefire.reuseForks=false |
No -- Wave 0 |
| N/A | Ed25519 signing and verification roundtrip | unit | mvn test -pl cameleer-server-app -Dtest=Ed25519SigningServiceTest -Dsurefire.reuseForks=false |
No -- Wave 0 |
Sampling Rate
- Per task commit:
mvn test -pl cameleer-server-app -Dsurefire.reuseForks=false - Per wave merge:
mvn clean verify - Phase gate: Full suite green before
/gsd:verify-work
Wave 0 Gaps
SecurityFilterIT.java-- covers SECU-01 (protected/public endpoint access)JwtRefreshIT.java-- covers SECU-02 (refresh flow)RegistrationSecurityIT.java-- covers SECU-03 + SECU-05 (bootstrap token + public key)SseSigningIT.java-- covers SECU-04 (Ed25519 SSE signing)BootstrapTokenIT.java-- covers SECU-05 (bootstrap token validation)JwtServiceTest.java-- unit test for JWT creation/validationEd25519SigningServiceTest.java-- unit test for Ed25519 signing roundtrip- Update
application-test.ymlwithCAMELEER_AUTH_TOKEN: test-tokenand security-related test config - Update ALL existing ITs to include JWT auth headers (21 test files affected)
Sources
Primary (HIGH confidence)
- Spring Security 6.4 Official Docs - SecurityFilterChain configuration, filter ordering
- Spring Security OAuth2 Resource Server JWT - JWT handling patterns
- Nimbus JOSE+JWT Official Site - Library capabilities, HMAC examples
- Nimbus JOSE+JWT HMAC Examples - JWT creation/verification code
- Java EdDSA (Ed25519) - HowToDoInJava - JDK built-in Ed25519 API
- JDK 17 X509EncodedKeySpec - Public key encoding format
- Spring Boot 3.4.3 BOM - Confirms Spring Security 6.4.3 managed version
Secondary (MEDIUM confidence)
- Baeldung Custom Filter - Custom filter registration patterns, double-registration pitfall
- Bootiful Spring Boot 3.4: Security - Spring Boot 3.4 security features overview
- Bootify REST API with JWT - JWT filter pattern validation
Tertiary (LOW confidence)
- None -- all findings verified against primary sources
Metadata
Confidence breakdown:
- Standard stack: HIGH - Spring Security 6.4.3 confirmed managed by Spring Boot 3.4.3, Nimbus well-documented, JDK Ed25519 verified for Java 17
- Architecture: HIGH - SecurityFilterChain pattern is the documented standard for Spring Security 6.x, existing codebase has clear integration points
- Pitfalls: HIGH - Double filter registration and test breakage are well-documented issues with Spring Security adoption; Ed25519 signing concerns are from domain knowledge
Research date: 2026-03-11 Valid until: 2026-04-11 (stable -- Spring Boot 3.4.x LTS, JDK 17 LTS)