# Security - Spring Security 6 ## Security Configuration ```java @Configuration @EnableWebSecurity @EnableMethodSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf .ignoringRequestMatchers("/api/auth/**") .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) ) .cors(cors -> cors.configurationSource(corsConfigurationSource())) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/**", "/actuator/health").permitAll() .requestMatchers("/api/admin/**").hasRole("ADMIN") .requestMatchers("/api/users/**").hasAnyRole("USER", "ADMIN") .anyRequest().authenticated() ) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .exceptionHandling(ex -> ex .authenticationEntryPoint(authenticationEntryPoint()) .accessDeniedHandler(accessDeniedHandler()) ) .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(List.of("http://localhost:3000")); configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); configuration.setAllowedHeaders(List.of("*")); configuration.setAllowCredentials(true); configuration.setMaxAge(3600L); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; } @Bean public AuthenticationManager authenticationManager( AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(12); } } ``` ## JWT Authentication Filter ```java @Component @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtService jwtService; private final UserDetailsService userDetailsService; @Override protected void doFilterInternal( @NonNull HttpServletRequest request, @NonNull HttpServletRequest response, @NonNull FilterChain filterChain) throws ServletException, IOException { final String authHeader = request.getHeader("Authorization"); final String jwt; final String username; if (authHeader == null || !authHeader.startsWith("Bearer ")) { filterChain.doFilter(request, response); return; } jwt = authHeader.substring(7); try { username = jwtService.extractUsername(jwt); if (username != null && SecurityContextHolder.getContext() .getAuthentication() == null) { UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (jwtService.isTokenValid(jwt, userDetails)) { UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities() ); authToken.setDetails( new WebAuthenticationDetailsSource().buildDetails(request) ); SecurityContextHolder.getContext().setAuthentication(authToken); } } } catch (JwtException e) { log.error("JWT validation failed", e); } filterChain.doFilter(request, response); } } ``` ## JWT Service ```java @Service public class JwtService { @Value("${jwt.secret}") private String secretKey; @Value("${jwt.expiration}") private long jwtExpiration; @Value("${jwt.refresh-expiration}") private long refreshExpiration; public String extractUsername(String token) { return extractClaim(token, Claims::getSubject); } public T extractClaim(String token, Function claimsResolver) { final Claims claims = extractAllClaims(token); return claimsResolver.apply(claims); } public String generateToken(UserDetails userDetails) { Map extraClaims = new HashMap<>(); extraClaims.put("roles", userDetails.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList())); return generateToken(extraClaims, userDetails); } public String generateToken( Map extraClaims, UserDetails userDetails) { return buildToken(extraClaims, userDetails, jwtExpiration); } public String generateRefreshToken(UserDetails userDetails) { return buildToken(new HashMap<>(), userDetails, refreshExpiration); } private String buildToken( Map extraClaims, UserDetails userDetails, long expiration) { return Jwts .builder() .setClaims(extraClaims) .setSubject(userDetails.getUsername()) .setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + expiration)) .signWith(getSignInKey(), SignatureAlgorithm.HS256) .compact(); } public boolean isTokenValid(String token, UserDetails userDetails) { final String username = extractUsername(token); return username.equals(userDetails.getUsername()) && !isTokenExpired(token); } private boolean isTokenExpired(String token) { return extractExpiration(token).before(new Date()); } private Date extractExpiration(String token) { return extractClaim(token, Claims::getExpiration); } private Claims extractAllClaims(String token) { return Jwts .parserBuilder() .setSigningKey(getSignInKey()) .build() .parseClaimsJws(token) .getBody(); } private Key getSignInKey() { byte[] keyBytes = Decoders.BASE64.decode(secretKey); return Keys.hmacShaKeyFor(keyBytes); } } ``` ## UserDetailsService Implementation ```java @Service @RequiredArgsConstructor public class CustomUserDetailsService implements UserDetailsService { private final UserRepository userRepository; @Override @Transactional(readOnly = true) public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByEmailWithRoles(username) .orElseThrow(() -> new UsernameNotFoundException( "User not found with email: " + username)); return org.springframework.security.core.userdetails.User .builder() .username(user.getEmail()) .password(user.getPassword()) .authorities(user.getRoles().stream() .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName())) .collect(Collectors.toList())) .accountExpired(false) .accountLocked(!user.getActive()) .credentialsExpired(false) .disabled(!user.getActive()) .build(); } } ``` ## Authentication Controller ```java @RestController @RequestMapping("/api/auth") @RequiredArgsConstructor public class AuthenticationController { private final AuthenticationService authenticationService; @PostMapping("/register") public ResponseEntity register( @Valid @RequestBody RegisterRequest request) { AuthenticationResponse response = authenticationService.register(request); return ResponseEntity.status(HttpStatus.CREATED).body(response); } @PostMapping("/login") public ResponseEntity login( @Valid @RequestBody LoginRequest request) { AuthenticationResponse response = authenticationService.login(request); return ResponseEntity.ok(response); } @PostMapping("/refresh") public ResponseEntity refreshToken( @RequestBody RefreshTokenRequest request) { AuthenticationResponse response = authenticationService.refreshToken(request); return ResponseEntity.ok(response); } @PostMapping("/logout") @PreAuthorize("isAuthenticated()") public ResponseEntity logout() { SecurityContextHolder.clearContext(); return ResponseEntity.noContent().build(); } } ``` ## Authentication Service ```java @Service @RequiredArgsConstructor @Transactional public class AuthenticationService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; private final JwtService jwtService; private final AuthenticationManager authenticationManager; public AuthenticationResponse register(RegisterRequest request) { if (userRepository.existsByEmail(request.email())) { throw new DuplicateResourceException("Email already registered"); } User user = User.builder() .email(request.email()) .password(passwordEncoder.encode(request.password())) .username(request.username()) .active(true) .roles(Set.of(Role.builder().name("USER").build())) .build(); user = userRepository.save(user); String accessToken = jwtService.generateToken(convertToUserDetails(user)); String refreshToken = jwtService.generateRefreshToken(convertToUserDetails(user)); return new AuthenticationResponse(accessToken, refreshToken); } public AuthenticationResponse login(LoginRequest request) { authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( request.email(), request.password() ) ); User user = userRepository.findByEmail(request.email()) .orElseThrow(() -> new UsernameNotFoundException("User not found")); String accessToken = jwtService.generateToken(convertToUserDetails(user)); String refreshToken = jwtService.generateRefreshToken(convertToUserDetails(user)); return new AuthenticationResponse(accessToken, refreshToken); } public AuthenticationResponse refreshToken(RefreshTokenRequest request) { String username = jwtService.extractUsername(request.refreshToken()); User user = userRepository.findByEmail(username) .orElseThrow(() -> new UsernameNotFoundException("User not found")); UserDetails userDetails = convertToUserDetails(user); if (!jwtService.isTokenValid(request.refreshToken(), userDetails)) { throw new InvalidTokenException("Invalid refresh token"); } String accessToken = jwtService.generateToken(userDetails); return new AuthenticationResponse(accessToken, request.refreshToken()); } private UserDetails convertToUserDetails(User user) { return org.springframework.security.core.userdetails.User .builder() .username(user.getEmail()) .password(user.getPassword()) .authorities(user.getRoles().stream() .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName())) .collect(Collectors.toList())) .build(); } } ``` ## Method Security ```java @Service @RequiredArgsConstructor public class UserService { private final UserRepository userRepository; @PreAuthorize("hasRole('ADMIN')") public List getAllUsers() { return userRepository.findAll(); } @PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id") public User getUserById(Long userId) { return userRepository.findById(userId) .orElseThrow(() -> new ResourceNotFoundException("User not found")); } @PreAuthorize("isAuthenticated()") @PostAuthorize("returnObject.email == authentication.principal.username") public User updateProfile(Long userId, UserUpdateRequest request) { User user = getUserById(userId); // Update logic return userRepository.save(user); } @Secured({"ROLE_ADMIN", "ROLE_MANAGER"}) public void deleteUser(Long userId) { userRepository.deleteById(userId); } } ``` ## OAuth2 Resource Server (JWT) ```java @Configuration @EnableWebSecurity public class OAuth2ResourceServerConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/public/**").permitAll() .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt .jwtAuthenticationConverter(jwtAuthenticationConverter()) ) ); return http.build(); } @Bean public JwtDecoder jwtDecoder() { return JwtDecoders.fromIssuerLocation("https://auth.example.com"); } @Bean public JwtAuthenticationConverter jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); grantedAuthoritiesConverter.setAuthoritiesClaimName("roles"); grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_"); JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter( grantedAuthoritiesConverter); return jwtAuthenticationConverter; } } ``` ## Quick Reference | Annotation | Purpose | |------------|---------| | `@EnableWebSecurity` | Enables Spring Security | | `@EnableMethodSecurity` | Enables method-level security annotations | | `@PreAuthorize` | Checks authorization before method execution | | `@PostAuthorize` | Checks authorization after method execution | | `@Secured` | Role-based method security | | `@WithMockUser` | Mock authenticated user in tests | | `@AuthenticationPrincipal` | Inject current user in controller | ## Security Best Practices - Always use HTTPS in production - Store JWT secret in environment variables - Use strong password encoding (BCrypt with strength 12+) - Implement token refresh mechanism - Add rate limiting to authentication endpoints - Validate all user inputs - Log security events - Keep dependencies updated - Use CSRF protection for state-changing operations - Implement proper session timeout