Spring Security Authentication
Introduction
Authentication is the process of verifying that a user is who they claim to be. In web applications, this typically involves validating credentials like usernames and passwords. Spring Security provides a robust and flexible authentication framework that can be integrated into any Spring-based application.
In this guide, we'll explore how authentication works in Spring Security, different authentication mechanisms, and implement practical examples that you can use in your own applications.
Understanding Authentication in Spring Security
Authentication in Spring Security is built around a few core components:
- Authentication - An interface representing an authentication request or an authenticated principal
- AuthenticationManager - The API responsible for processing authentication requests
- AuthenticationProvider - Performs the actual authentication logic
- UserDetailsService - Provides user information needed for authentication
Let's break down how these components work together:
- A user submits credentials (via form login, JWT token, etc.)
- Spring Security creates an
Authentication
object (typicallyUsernamePasswordAuthenticationToken
) - The request is passed to the
AuthenticationManager
- The
AuthenticationManager
delegates to appropriateAuthenticationProvider(s)
- If authentication succeeds, a fully populated
Authentication
object is created and stored in theSecurityContext
Basic Authentication Configuration
Let's start with a simple example of configuring form-based authentication in a Spring Boot application.
Step 1: Add Spring Security Dependencies
In your pom.xml
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Or in build.gradle
:
implementation 'org.springframework.boot:spring-boot-starter-security'
Step 2: Basic Security Configuration
Here's a simple security configuration:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/", "/home", "/public/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.logout(logout -> logout
.permitAll()
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
This configuration:
- Allows public access to specific URLs (
/
,/home
,/public/**
) - Requires authentication for all other requests
- Sets up a custom login page at
/login
- Configures logout functionality
- Sets up BCrypt password encoding
User Management
Spring Security needs to authenticate users against a user store. Let's implement a simple user service.
In-Memory Authentication
For development and testing, you can use in-memory authentication:
@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.builder()
.username("user")
.password(passwordEncoder().encode("password"))
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password(passwordEncoder().encode("admin"))
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
Database Authentication
For production applications, you'll typically want to authenticate users from a database:
@Service
public class DatabaseUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public DatabaseUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
user.isEnabled(),
true, true, true,
getAuthorities(user.getRoles())
);
}
private Collection<? extends GrantedAuthority> getAuthorities(Set<Role> roles) {
return roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
.collect(Collectors.toList());
}
}
With the entity classes:
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password;
private boolean enabled = true;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<Role> roles = new HashSet<>();
// getters and setters
}
@Entity
@Table(name = "roles")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String name;
// getters and setters
}
Authentication Flow Example
Let's create a practical example showing the authentication flow in a Spring MVC application.
Controller for Login
@Controller
public class AuthController {
@GetMapping("/login")
public String loginPage() {
return "login";
}
@GetMapping("/profile")
public String profilePage(Model model, Principal principal) {
model.addAttribute("username", principal.getName());
return "profile";
}
@GetMapping("/admin")
@PreAuthorize("hasRole('ADMIN')")
public String adminPage() {
return "admin";
}
}
Login Page Template (Thymeleaf example)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Login</title>
</head>
<body>
<h1>Login</h1>
<div th:if="${param.error}">
Invalid username or password.
</div>
<div th:if="${param.logout}">
You have been logged out.
</div>
<form th:action="@{/login}" method="post">
<div>
<label for="username">Username:</label>
<input type="text" id="username" name="username" required>
</div>
<div>
<label for="password">Password:</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit">Login</button>
</form>
<p>New user? <a th:href="@{/register}">Register here</a></p>
</body>
</html>
Advanced Authentication Techniques
Remember Me Authentication
The "Remember Me" feature allows users to remain logged in across browser restarts:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// other configurations
.rememberMe(remember -> remember
.key("uniqueAndSecretKey")
.tokenValiditySeconds(86400) // 1 day
);
return http.build();
}
Then add to your login form:
<div>
<label>
<input type="checkbox" name="remember-me"> Remember me
</label>
</div>
OAuth2 and Social Login
Spring Security provides excellent support for OAuth2 and social login:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
Configure in application.properties
:
spring.security.oauth2.client.registration.google.client-id=your-client-id
spring.security.oauth2.client.registration.google.client-secret=your-client-secret
spring.security.oauth2.client.registration.github.client-id=your-github-client-id
spring.security.oauth2.client.registration.github.client-secret=your-github-client-secret
Update the security configuration:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// existing config
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.defaultSuccessUrl("/profile")
);
return http.build();
}
Add to login page:
<div>
<h2>Social Login</h2>
<a th:href="@{/oauth2/authorization/google}">Login with Google</a>
<a th:href="@{/oauth2/authorization/github}">Login with GitHub</a>
</div>
JWT Authentication
JWT (JSON Web Tokens) is popular for stateless authentication in REST APIs. Here's how to implement it:
Add dependency:
<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>
Create JWT utilities:
@Component
public class JwtTokenUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
if (authorities != null) {
claims.put("authorities", authorities.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()));
}
return createToken(claims, userDetails.getUsername());
}
private String createToken(Map<String, Object> claims, String subject) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration * 1000);
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(Keys.hmacShaKeyFor(secret.getBytes()), SignatureAlgorithm.HS512)
.compact();
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
private <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(secret.getBytes()))
.build()
.parseClaimsJws(token)
.getBody();
}
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
}
Create JWT Filter:
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final UserDetailsService userDetailsService;
private final JwtTokenUtil jwtTokenUtil;
public JwtAuthenticationFilter(UserDetailsService userDetailsService, JwtTokenUtil jwtTokenUtil) {
this.userDetailsService = userDetailsService;
this.jwtTokenUtil = jwtTokenUtil;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
try {
username = jwtTokenUtil.extractUsername(jwt);
} catch (Exception e) {
// Invalid token
}
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
chain.doFilter(request, response);
}
}
Configure JWT security:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final UserDetailsService userDetailsService;
private final JwtTokenUtil jwtTokenUtil;
public SecurityConfig(UserDetailsService userDetailsService, JwtTokenUtil jwtTokenUtil) {
this.userDetailsService = userDetailsService;
this.jwtTokenUtil = jwtTokenUtil;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
)
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.addFilterBefore(new JwtAuthenticationFilter(userDetailsService, jwtTokenUtil),
UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Create authentication controller:
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final AuthenticationManager authenticationManager;
private final UserDetailsService userDetailsService;
private final JwtTokenUtil jwtTokenUtil;
public AuthController(AuthenticationManager authenticationManager,
UserDetailsService userDetailsService,
JwtTokenUtil jwtTokenUtil) {
this.authenticationManager = authenticationManager;
this.userDetailsService = userDetailsService;
this.jwtTokenUtil = jwtTokenUtil;
}
@PostMapping("/login")
public ResponseEntity<?> createAuthenticationToken(@RequestBody LoginRequest loginRequest) {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())
);
} catch (BadCredentialsException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid username or password");
}
final UserDetails userDetails = userDetailsService.loadUserByUsername(loginRequest.getUsername());
final String jwt = jwtTokenUtil.generateToken(userDetails);
return ResponseEntity.ok(new JwtResponse(jwt));
}
// Login request and response DTOs
public static class LoginRequest {
private String username;
private String password;
// getters and setters
}
public static class JwtResponse {
private String token;
public JwtResponse(String token) {
this.token = token;
}
// getter
}
}
Handling Authentication Events
Spring Security provides events that can help you track authentication success, failure, and other security events:
@Component
public class AuthenticationEventListener {
private static final Logger logger = LoggerFactory.getLogger(AuthenticationEventListener.class);
@EventListener
public void onSuccess(AuthenticationSuccessEvent event) {
logger.info("Successful authentication for user: {}",
event.getAuthentication().getName());
}
@EventListener
public void onFailure(AbstractAuthenticationFailureEvent event) {
logger.warn("Failed authentication attempt: {}",
event.getAuthentication().getName());
logger.warn("Failure reason: {}", event.getException().getMessage());
}
@EventListener
public void onLogout(LogoutSuccessEvent event) {
logger.info("User logged out: {}",
event.getAuthentication().getName());
}
}
Authentication Testing
Testing authentication is crucial. Here's how to test security with Spring Boot:
@SpringBootTest
@AutoConfigureMockMvc
class SecurityTests {
@Autowired
private MockMvc mockMvc;
@Test
void accessUnsecuredEndpointShouldBeAllowed() throws Exception {
mockMvc.perform(get("/public/info"))
.andExpect(status().isOk());
}
@Test
void accessSecuredEndpointWithoutAuthShouldBeDenied() throws Exception {
mockMvc.perform(get("/api/user/profile"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser(username = "testuser", roles = {"USER"})
void authenticatedUserCanAccessUserEndpoints() throws Exception {
mockMvc.perform(get("/api/user/profile"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(username = "testuser", roles = {"USER"})
void regularUserCannotAccessAdminEndpoints() throws Exception {
mockMvc.perform(get("/api/admin/dashboard"))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser(username = "admin", roles = {"ADMIN"})
void adminCanAccessAdminEndpoints() throws Exception {
mockMvc.perform(get("/api/admin/dashboard"))
.andExpect(status().isOk());
}
}
Summary
Spring Security provides a powerful and flexible authentication framework for securing your applications. In this guide, we've covered:
- Basic concepts of Spring Security authentication
- Form-based authentication configuration
- Managing users with in-memory and database providers
- Advanced techniques like Remember Me and OAuth2/Social login
- JWT-based authentication for REST APIs
- Handling authentication events
- Testing security configurations
By implementing the examples provided, you can secure your Spring applications effectively while maintaining flexibility and user-friendliness.
Additional Resources
To deepen your understanding of Spring Security Authentication:
- Official Spring Security Documentation
- Spring Security Architecture
- Spring Boot OAuth2 Tutorial
- JWT with Spring Security
Exercises
- Implement a custom
AuthenticationProvider
that authenticates users against an external API. - Create a registration form that allows new users to sign up and store their information in a database.
- Add two-factor authentication using email or SMS verification.
- Configure LDAP authentication for an enterprise application.
- Implement a password reset functionality with emailed reset tokens.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)