Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions plugins/java-spring/skills/java-security/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
---
description: Reviews or implements Spring Security configuration — JWT authentication, OAuth2, method-level security, CORS, and CSRF. Use when user asks to "add authentication", "secure this API", "implement JWT", "configure Spring Security", "add OAuth2 login", "protect endpoints", or "review security config".
argument-hint: "[review | jwt | oauth2 | method-security | cors] [Spring Boot version]"
allowed-tools: Read, Grep, Glob
---

# /java-security — Spring Security Advisor

You are a Spring Security specialist. Review existing security configuration or implement new security features for Spring Boot projects.

> **Quick OWASP vulnerability scan?** Use `/java-security-check` instead.

## Step 1 — Detect project context

1. Check Spring Boot version from `pom.xml` / `build.gradle`:
- Spring Boot 3.x → Spring Security 6.x (`jakarta.*`, `SecurityFilterChain` bean, no `WebSecurityConfigurerAdapter`)
- Spring Boot 2.x → Spring Security 5.x (`javax.*`, `WebSecurityConfigurerAdapter` still works but deprecated)
2. Check if `spring-boot-starter-security` is already on the classpath
3. If reviewing: scan for existing `@Configuration` + `@EnableWebSecurity` classes

## Step 2 — Determine mode from argument

- **`review`** (default if no arg) → audit existing config, go to Step 3
- **`jwt`** → implement stateless JWT authentication, go to Step 4
- **`oauth2`** → configure OAuth2 resource server or login, go to Step 5
- **`method-security`** → add method-level annotations, go to Step 6
- **`cors`** → configure CORS policy, go to Step 7

---

## Step 3 — Review existing security config

Check for these issues and report each with file:line and severity:

**CRITICAL**
- `permitAll()` on sensitive paths (`/admin`, `/actuator`, `/internal`)
- `csrf().disable()` on non-stateless APIs (stateful session apps need CSRF)
- `@CrossOrigin(origins = "*")` in production controllers
- Passwords hashed with MD5, SHA-1, or stored plain

**HIGH**
- `httpBasic()` enabled on production APIs (use JWT or OAuth2)
- Actuator endpoints exposed without authentication (`/actuator/**`)
- Missing `@PreAuthorize` or role checks on admin endpoints
- `antMatchers` / `requestMatchers` ordering issues (broad rules before specific ones)

**MEDIUM**
- No session fixation protection
- Missing security headers (HSTS, X-Frame-Options, X-Content-Type-Options)
- `BCryptPasswordEncoder` strength below 10
- No rate limiting on `/login` endpoint

Use the patterns in `references/patterns.md` to suggest fixes.

---

## Step 4 — Implement JWT authentication

Use the templates in `references/patterns.md` (JWT section). Generate in this order:

1. **Dependencies** — add to `pom.xml` / `build.gradle`:
- Spring Boot 3.x: `spring-boot-starter-oauth2-resource-server` (uses built-in JWT support)
- Spring Boot 2.x: `jjwt-api`, `jjwt-impl`, `jjwt-jackson`

2. **`SecurityConfig.java`** — `SecurityFilterChain` bean:
- Stateless session (`SessionCreationPolicy.STATELESS`)
- Permit `/auth/**`, secure everything else
- JWT decoder / filter setup

3. **`JwtService.java`** — generate and validate tokens:
- Sign with `HS256` (symmetric) for simple cases, `RS256` (asymmetric) for multi-service
- Include: `sub` (userId), `iat`, `exp`, `roles`
- Expiry: 15 min for access token, 7 days for refresh token

4. **`AuthController.java`** — `/auth/login` and `/auth/refresh` endpoints

5. **`AuthService.java`** — authenticate against `UserDetailsService`, issue tokens

6. **Version notes:**
- Spring Boot 3.x: use `spring-security-oauth2-resource-server` JWT decoder — no manual filter needed
- Spring Boot 2.x: implement `OncePerRequestFilter` manually

---

## Step 5 — Configure OAuth2

For **resource server** (API validates tokens from an external IdP):
```yaml
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://your-idp.example.com
```

For **login** (users log in via Google, GitHub, etc.):
```yaml
spring:
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}
```

Remind: never hardcode client secrets — use environment variables.

---

## Step 6 — Method-level security

Enable with `@EnableMethodSecurity` (Spring Security 6) or `@EnableGlobalMethodSecurity` (5):

| Annotation | Use for |
|---|---|
| `@PreAuthorize("hasRole('ADMIN')")` | Role-based access before method runs |
| `@PreAuthorize("hasAuthority('user:write')")` | Fine-grained permission check |
| `@PreAuthorize("#userId == authentication.principal.id")` | Owner-only access |
| `@PostAuthorize("returnObject.userId == authentication.principal.id")` | Filter after return |
| `@Secured("ROLE_ADMIN")` | Simple role check (legacy) |

Generate `@PreAuthorize` annotations for each controller method based on its sensitivity.

---

## Step 7 — CORS configuration

```java
// Preferred: global CORS via SecurityFilterChain (Spring Security 6)
http.cors(cors -> cors.configurationSource(corsConfigurationSource()));

@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("https://app.example.com")); // never "*" in prod
config.setAllowedMethods(List.of("GET","POST","PUT","DELETE","OPTIONS"));
config.setAllowedHeaders(List.of("Authorization","Content-Type"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
```

Flag `@CrossOrigin(origins = "*")` on controllers — replace with global config.

---

## Step 8 — Post-implementation checklist

- [ ] Secret keys come from env vars, not hardcoded in code or `application.yml`
- [ ] JWT expiry is set (access ≤ 15 min, refresh ≤ 7 days)
- [ ] Actuator endpoints secured or restricted to internal network
- [ ] `/auth/login` endpoint is rate-limited (suggest Bucket4j or Spring's built-in)
- [ ] Run `/java-security-check` to verify no OWASP issues remain

## Next Steps

- Full OWASP scan → `/java-security-check`
- Deep security audit → `java-security-reviewer` agent
- Generate tests for auth flows → `/java-test`
215 changes: 215 additions & 0 deletions plugins/java-spring/skills/java-security/references/patterns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
# Spring Security Reference Patterns

---

## JWT — Spring Boot 3.x (OAuth2 Resource Server, built-in decoder)

### pom.xml dependency
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
```

### SecurityConfig.java
```java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable) // stateless — no CSRF needed
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**", "/actuator/health").permitAll()
.requestMatchers("/actuator/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
.build();
}

@Bean
JwtDecoder jwtDecoder(@Value("${app.jwt.secret}") String secret) {
SecretKeySpec key = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
return NimbusJwtDecoder.withSecretKey(key).build();
}

@Bean
JwtEncoder jwtEncoder(@Value("${app.jwt.secret}") String secret) {
SecretKeySpec key = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
JWKSource<SecurityContext> jwks = new ImmutableSecret<>(key);
return new NimbusJwtEncoder(jwks);
}
}
```

### JwtService.java (Spring Boot 3.x)
```java
@Service
@RequiredArgsConstructor
public class JwtService {

private final JwtEncoder encoder;
private static final Duration ACCESS_TOKEN_EXPIRY = Duration.ofMinutes(15);
private static final Duration REFRESH_TOKEN_EXPIRY = Duration.ofDays(7);

public String generateAccessToken(UserDetails user) {
return encode(user.getUsername(), user.getAuthorities(), ACCESS_TOKEN_EXPIRY);
}

public String generateRefreshToken(UserDetails user) {
return encode(user.getUsername(), List.of(), REFRESH_TOKEN_EXPIRY);
}

private String encode(String subject, Collection<? extends GrantedAuthority> authorities, Duration ttl) {
Instant now = Instant.now();
String roles = authorities.stream()
.map(GrantedAuthority::getAuthority).collect(Collectors.joining(" "));
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("self")
.issuedAt(now)
.expiresAt(now.plus(ttl))
.subject(subject)
.claim("roles", roles)
.build();
return encoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
}
}
```

---

## JWT — Spring Boot 2.x (manual OncePerRequestFilter)

### pom.xml dependencies
```xml
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
```

### JwtAuthFilter.java (Spring Boot 2.x)
```java
@Component
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {

private final JwtService jwtService;
private final UserDetailsService userDetailsService;

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
chain.doFilter(request, response);
return;
}
String token = authHeader.substring(7);
String username = jwtService.extractUsername(token);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails user = userDetailsService.loadUserByUsername(username);
if (jwtService.isTokenValid(token, user)) {
var auth = new UsernamePasswordAuthenticationToken(
user, null, user.getAuthorities());
auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
chain.doFilter(request, response);
}
}
```

---

## AuthController.java (both versions)

```java
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {

private final AuthService authService;

@PostMapping("/login")
public ResponseEntity<TokenResponse> login(@Valid @RequestBody LoginRequest request) {
return ResponseEntity.ok(authService.authenticate(request));
}

@PostMapping("/refresh")
public ResponseEntity<TokenResponse> refresh(@Valid @RequestBody RefreshRequest request) {
return ResponseEntity.ok(authService.refresh(request.refreshToken()));
}
}

public record LoginRequest(@NotBlank String username, @NotBlank String password) {}
public record RefreshRequest(@NotBlank String refreshToken) {}
public record TokenResponse(String accessToken, String refreshToken, long expiresIn) {}
```

---

## Method Security examples

```java
@RestController
@RequestMapping("/api/orders")
public class OrderController {

// Any authenticated user
@GetMapping("/{id}")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<OrderResponse> getOrder(@PathVariable Long id) { ... }

// Only the order owner or admins
@GetMapping("/{id}/details")
@PreAuthorize("#id == authentication.principal.id or hasRole('ADMIN')")
public ResponseEntity<OrderDetails> getDetails(@PathVariable Long id) { ... }

// Fine-grained permission
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('order:delete')")
public ResponseEntity<Void> delete(@PathVariable Long id) { ... }
}
```

---

## application.yml — security properties

```yaml
app:
jwt:
secret: ${JWT_SECRET} # min 32 chars, set via env var — never hardcode
access-token-expiry: 15m
refresh-token-expiry: 7d

spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: ${JWT_ISSUER_URI:} # leave blank for symmetric key setup
```
Loading