Spring SecurityAuthenticationAuthorizationJWTOAuth2
Spring Security 6: Complete Authentication and Authorization Guide
January 10, 2025•22 min read•Security
# Spring Security 6: Complete Authentication and Authorization Guide
Spring Security 6 introduces significant changes and improvements, including enhanced OAuth2 support, simplified configuration, and better integration with modern authentication standards. This guide covers everything you need to secure your Spring applications effectively.
## Spring Security 6 Overview
Spring Security 6 brings several major changes:
- Requires Java 17+
- Servlet API 6.0+ support
- Enhanced OAuth2 and OpenID Connect support
- Simplified configuration with lambda DSL
- Improved observability and metrics
- Better integration with Spring Boot 3
## Basic Security Configuration
### Modern Configuration Approach
```java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.ignoringRequestMatchers("/api/public/**")
)
.authorizeHttpRequests(authz -> authz
.requestMatchers("/", "/home", "/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/api/users/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
.failureUrl("/login?error")
.permitAll()
)
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.maximumSessions(1)
.maxSessionsPreventsLogin(false)
)
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}
```
### User Details Service Implementation
```java
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
@Transactional(readOnly = true)
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
return CustomUserPrincipal.builder()
.id(user.getId())
.username(user.getUsername())
.password(user.getPassword())
.email(user.getEmail())
.authorities(mapRolesToAuthorities(user.getRoles()))
.enabled(user.isEnabled())
.accountNonExpired(user.isAccountNonExpired())
.accountNonLocked(user.isAccountNonLocked())
.credentialsNonExpired(user.isCredentialsNonExpired())
.build();
}
private Collection<? extends GrantedAuthority> mapRolesToAuthorities(Set<Role> roles) {
return roles.stream()
.flatMap(role -> {
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName()));
// Add permissions as authorities
role.getPermissions().forEach(permission ->
authorities.add(new SimpleGrantedAuthority(permission.getName())));
return authorities.stream();
})
.collect(Collectors.toList());
}
}
@Builder
@Data
public class CustomUserPrincipal implements UserDetails {
private Long id;
private String username;
private String password;
private String email;
private Collection<? extends GrantedAuthority> authorities;
private boolean enabled;
private boolean accountNonExpired;
private boolean accountNonLocked;
private boolean credentialsNonExpired;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
// Other UserDetails methods...
}
```
## JWT Authentication Implementation
### JWT Configuration
```java
@Component
public class JwtTokenProvider {
private final String jwtSecret;
private final int jwtExpirationMs;
private final Key key;
public JwtTokenProvider(@Value("${app.jwt.secret}") String jwtSecret,
@Value("${app.jwt.expiration}") int jwtExpirationMs) {
this.jwtSecret = jwtSecret;
this.jwtExpirationMs = jwtExpirationMs;
this.key = Keys.hmacShaKeyFor(jwtSecret.getBytes());
}
public String generateToken(Authentication authentication) {
CustomUserPrincipal userPrincipal = (CustomUserPrincipal) authentication.getPrincipal();
Date expiryDate = new Date(System.currentTimeMillis() + jwtExpirationMs);
return Jwts.builder()
.setSubject(userPrincipal.getUsername())
.setIssuedAt(new Date())
.setExpiration(expiryDate)
.claim("userId", userPrincipal.getId())
.claim("authorities", userPrincipal.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()))
.signWith(key, SignatureAlgorithm.HS512)
.compact();
}
public String generateRefreshToken(String username) {
Date expiryDate = new Date(System.currentTimeMillis() + (jwtExpirationMs * 7)); // 7x longer
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(expiryDate)
.claim("type", "refresh")
.signWith(key, SignatureAlgorithm.HS512)
.compact();
}
public String getUsernameFromToken(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
logger.error("Invalid JWT token: {}", e.getMessage());
return false;
}
}
public Collection<? extends GrantedAuthority> getAuthoritiesFromToken(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
@SuppressWarnings("unchecked")
List<String> authorities = claims.get("authorities", List.class);
return authorities.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
}
```
### JWT Authentication Filter
```java
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider tokenProvider;
@Autowired
private CustomUserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = getTokenFromRequest(request);
if (token != null && tokenProvider.validateToken(token)) {
String username = tokenProvider.getUsernameFromToken(token);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (userDetails != null) {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource()
.buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
filterChain.doFilter(request, response);
}
private String getTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
```
### Authentication Controller
```java
@RestController
@RequestMapping("/api/auth")
@Validated
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenProvider tokenProvider;
@Autowired
private UserService userService;
@PostMapping("/login")
public ResponseEntity<JwtAuthenticationResponse> authenticateUser(
@Valid @RequestBody LoginRequest loginRequest) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = tokenProvider.generateToken(authentication);
String refreshToken = tokenProvider.generateRefreshToken(
loginRequest.getUsername());
return ResponseEntity.ok(new JwtAuthenticationResponse(jwt, refreshToken));
}
@PostMapping("/register")
public ResponseEntity<ApiResponse> registerUser(
@Valid @RequestBody SignUpRequest signUpRequest) {
if (userService.existsByUsername(signUpRequest.getUsername())) {
return ResponseEntity.badRequest()
.body(new ApiResponse(false, "Username is already taken!"));
}
if (userService.existsByEmail(signUpRequest.getEmail())) {
return ResponseEntity.badRequest()
.body(new ApiResponse(false, "Email Address already in use!"));
}
User user = User.builder()
.username(signUpRequest.getUsername())
.email(signUpRequest.getEmail())
.password(passwordEncoder.encode(signUpRequest.getPassword()))
.roles(Set.of(roleRepository.findByName("ROLE_USER")
.orElseThrow(() -> new RuntimeException("User Role not set."))))
.build();
User result = userService.save(user);
return ResponseEntity.ok(new ApiResponse(true, "User registered successfully"));
}
@PostMapping("/refresh")
public ResponseEntity<JwtAuthenticationResponse> refreshToken(
@Valid @RequestBody TokenRefreshRequest request) {
String refreshToken = request.getRefreshToken();
if (tokenProvider.validateToken(refreshToken)) {
String username = tokenProvider.getUsernameFromToken(refreshToken);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
Authentication authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
String newToken = tokenProvider.generateToken(authentication);
return ResponseEntity.ok(new JwtAuthenticationResponse(newToken, refreshToken));
} else {
throw new TokenRefreshException(refreshToken, "Refresh token is not valid!");
}
}
}
```
## OAuth2 and OpenID Connect
### OAuth2 Client Configuration
```yaml
# application.yml
spring:
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}
scope:
- openid
- profile
- email
github:
client-id: ${GITHUB_CLIENT_ID}
client-secret: ${GITHUB_CLIENT_SECRET}
scope:
- user:email
- read:user
provider:
google:
authorization-uri: https://accounts.google.com/o/oauth2/v2/auth
token-uri: https://oauth2.googleapis.com/token
user-info-uri: https://www.googleapis.com/oauth2/v2/userinfo
user-name-attribute: email
```
### OAuth2 Security Configuration
```java
@Configuration
public class OAuth2SecurityConfig {
@Autowired
private CustomOAuth2UserService customOAuth2UserService;
@Autowired
private OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler;
@Autowired
private OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler;
@Bean
public SecurityFilterChain oauth2FilterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/", "/error", "/login**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuth2UserService)
)
.successHandler(oAuth2AuthenticationSuccessHandler)
.failureHandler(oAuth2AuthenticationFailureHandler)
)
.build();
}
}
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
@Autowired
private UserRepository userRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oauth2User = super.loadUser(userRequest);
try {
return processOAuth2User(userRequest, oauth2User);
} catch (AuthenticationException ex) {
throw ex;
} catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex.getCause());
}
}
private OAuth2User processOAuth2User(OAuth2UserRequest userRequest, OAuth2User oauth2User) {
OAuth2UserInfo oauth2UserInfo = OAuth2UserInfoFactory
.getOAuth2UserInfo(userRequest.getClientRegistration().getRegistrationId(),
oauth2User.getAttributes());
if (StringUtils.isEmpty(oauth2UserInfo.getEmail())) {
throw new OAuth2AuthenticationProcessingException("Email not found from OAuth2 provider");
}
Optional<User> userOptional = userRepository.findByEmail(oauth2UserInfo.getEmail());
User user;
if (userOptional.isPresent()) {
user = userOptional.get();
if (!user.getProvider().equals(AuthProvider.valueOf(
userRequest.getClientRegistration().getRegistrationId().toUpperCase()))) {
throw new OAuth2AuthenticationProcessingException(
"Looks like you're signed up with " + user.getProvider() +
" account. Please use your " + user.getProvider() + " account to login.");
}
user = updateExistingUser(user, oauth2UserInfo);
} else {
user = registerNewUser(userRequest, oauth2UserInfo);
}
return UserPrincipal.create(user, oauth2User.getAttributes());
}
private User registerNewUser(OAuth2UserRequest userRequest, OAuth2UserInfo oauth2UserInfo) {
User user = User.builder()
.provider(AuthProvider.valueOf(userRequest.getClientRegistration()
.getRegistrationId().toUpperCase()))
.providerId(oauth2UserInfo.getId())
.name(oauth2UserInfo.getName())
.email(oauth2UserInfo.getEmail())
.imageUrl(oauth2UserInfo.getImageUrl())
.build();
return userRepository.save(user);
}
private User updateExistingUser(User existingUser, OAuth2UserInfo oauth2UserInfo) {
existingUser.setName(oauth2UserInfo.getName());
existingUser.setImageUrl(oauth2UserInfo.getImageUrl());
return userRepository.save(existingUser);
}
}
```
## Method-Level Security
### Method Security Configuration
```java
@Configuration
@EnableMethodSecurity(
prePostEnabled = true,
securedEnabled = true,
jsr250Enabled = true
)
public class MethodSecurityConfig {
@Bean
public MethodSecurityExpressionHandler methodSecurityExpressionHandler() {
DefaultMethodSecurityExpressionHandler expressionHandler =
new DefaultMethodSecurityExpressionHandler();
expressionHandler.setPermissionEvaluator(new CustomPermissionEvaluator());
return expressionHandler;
}
}
@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
@Autowired
private UserService userService;
@Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
if (authentication == null || targetDomainObject == null || !(permission instanceof String)) {
return false;
}
String targetType = targetDomainObject.getClass().getSimpleName().toUpperCase();
return hasPrivilege(authentication, targetType, permission.toString().toUpperCase());
}
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId,
String targetType, Object permission) {
if (authentication == null || targetType == null || !(permission instanceof String)) {
return false;
}
return hasPrivilege(authentication, targetType.toUpperCase(), permission.toString().toUpperCase());
}
private boolean hasPrivilege(Authentication authentication, String targetType, String permission) {
CustomUserPrincipal userPrincipal = (CustomUserPrincipal) authentication.getPrincipal();
return userPrincipal.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.anyMatch(authority -> authority.equals(targetType + "_" + permission) ||
authority.equals("ROLE_ADMIN"));
}
}
```
### Method Security Examples
```java
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
// Pre-authorization
@PreAuthorize("hasRole('ADMIN')")
public List<User> getAllUsers() {
return userRepository.findAll();
}
// Post-authorization
@PostAuthorize("returnObject.username == authentication.name or hasRole('ADMIN')")
public User getUserById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found"));
}
// Custom permission evaluation
@PreAuthorize("hasPermission(#user, 'WRITE')")
public User updateUser(User user) {
return userRepository.save(user);
}
// Method-level filtering
@PostFilter("filterObject.department == authentication.principal.department or hasRole('ADMIN')")
public List<User> getUsersByDepartment() {
return userRepository.findAll();
}
// JSR-250 annotations
@RolesAllowed({"ADMIN", "MANAGER"})
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
// Spring Security annotations
@Secured("ROLE_ADMIN")
public void resetPassword(Long userId, String newPassword) {
User user = getUserById(userId);
user.setPassword(passwordEncoder.encode(newPassword));
userRepository.save(user);
}
// Complex expressions
@PreAuthorize("@userService.isOwnerOrAdmin(#userId, authentication.name)")
public void updateUserProfile(Long userId, UserProfileRequest request) {
User user = getUserById(userId);
// Update user profile
userRepository.save(user);
}
public boolean isOwnerOrAdmin(Long userId, String currentUsername) {
User user = getUserById(userId);
return user.getUsername().equals(currentUsername) ||
SecurityContextHolder.getContext().getAuthentication()
.getAuthorities().stream()
.anyMatch(auth -> auth.getAuthority().equals("ROLE_ADMIN"));
}
}
```
## Security Testing
### Security Test Configuration
```java
@SpringBootTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@TestPropertySource(locations = "classpath:application-test.properties")
public class SecurityIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private JwtTokenProvider tokenProvider;
@MockBean
private UserDetailsService userDetailsService;
@Test
@WithMockUser(roles = "USER")
public void testUserAccess() throws Exception {
mockMvc.perform(get("/api/users/profile"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(roles = "ADMIN")
public void testAdminAccess() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isOk());
}
@Test
public void testUnauthorizedAccess() throws Exception {
mockMvc.perform(get("/api/users/profile"))
.andExpect(status().isUnauthorized());
}
@Test
public void testJwtAuthentication() throws Exception {
// Create mock user
CustomUserPrincipal userPrincipal = CustomUserPrincipal.builder()
.username("testuser")
.authorities(List.of(new SimpleGrantedAuthority("ROLE_USER")))
.build();
Authentication authentication = new UsernamePasswordAuthenticationToken(
userPrincipal, null, userPrincipal.getAuthorities());
String token = tokenProvider.generateToken(authentication);
mockMvc.perform(get("/api/users/profile")
.header("Authorization", "Bearer " + token))
.andExpect(status().isOk());
}
@Test
@WithAnonymousUser
public void testPublicEndpoint() throws Exception {
mockMvc.perform(get("/public/info"))
.andExpect(status().isOk());
}
}
// Custom security test annotation
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
public @interface WithMockCustomUser {
String username() default "testuser";
String[] roles() default {"USER"};
String[] authorities() default {};
}
public class WithMockCustomUserSecurityContextFactory
implements WithSecurityContextFactory<WithMockCustomUser> {
@Override
public SecurityContext createSecurityContext(WithMockCustomUser customUser) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
List<GrantedAuthority> authorities = new ArrayList<>();
for (String role : customUser.roles()) {
authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
}
for (String authority : customUser.authorities()) {
authorities.add(new SimpleGrantedAuthority(authority));
}
CustomUserPrincipal principal = CustomUserPrincipal.builder()
.username(customUser.username())
.authorities(authorities)
.build();
Authentication auth = new UsernamePasswordAuthenticationToken(
principal, null, authorities);
context.setAuthentication(auth);
return context;
}
}
```
## Security Best Practices
### Password Security
```java
@Component
public class PasswordPolicyValidator {
private static final int MIN_LENGTH = 8;
private static final String SPECIAL_CHARS = "!@#$%^&*()_+-=[]{}|;:,.<>?";
public ValidationResult validatePassword(String password) {
List<String> errors = new ArrayList<>();
if (password == null || password.length() < MIN_LENGTH) {
errors.add("Password must be at least " + MIN_LENGTH + " characters long");
}
if (!password.matches(".*[A-Z].*")) {
errors.add("Password must contain at least one uppercase letter");
}
if (!password.matches(".*[a-z].*")) {
errors.add("Password must contain at least one lowercase letter");
}
if (!password.matches(".*\d.*")) {
errors.add("Password must contain at least one digit");
}
if (!containsSpecialCharacter(password)) {
errors.add("Password must contain at least one special character");
}
if (isCommonPassword(password)) {
errors.add("Password is too common, please choose a different one");
}
return new ValidationResult(errors.isEmpty(), errors);
}
private boolean containsSpecialCharacter(String password) {
return password.chars()
.anyMatch(ch -> SPECIAL_CHARS.indexOf(ch) >= 0);
}
private boolean isCommonPassword(String password) {
Set<String> commonPasswords = Set.of(
"password", "123456", "password123", "admin", "qwerty"
);
return commonPasswords.contains(password.toLowerCase());
}
}
```
### Rate Limiting
```java
@Component
public class RateLimitingFilter implements Filter {
private final Map<String, List<Long>> requestCounts = new ConcurrentHashMap<>();
private final int maxRequests = 100;
private final long timeWindow = 60000; // 1 minute
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String clientIp = getClientIp(httpRequest);
if (isRateLimited(clientIp)) {
httpResponse.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
httpResponse.getWriter().write("Rate limit exceeded");
return;
}
chain.doFilter(request, response);
}
private boolean isRateLimited(String clientIp) {
long currentTime = System.currentTimeMillis();
requestCounts.compute(clientIp, (key, timestamps) -> {
if (timestamps == null) {
timestamps = new ArrayList<>();
}
// Remove old timestamps
timestamps.removeIf(timestamp -> currentTime - timestamp > timeWindow);
// Add current timestamp
timestamps.add(currentTime);
return timestamps;
});
return requestCounts.get(clientIp).size() > maxRequests;
}
private String getClientIp(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
return request.getRemoteAddr();
}
}
```
Spring Security 6 provides a robust foundation for securing modern Java applications. By combining proper authentication, authorization, and security best practices, you can build secure, scalable applications that protect user data and system resources effectively.