Secure Your Spring Boot 4 Apps with One Annotation: MFA Made Easy
This article explains how Spring Boot 4.0’s @EnableMultiFactorAuthentication annotation simplifies the implementation of password‑plus‑one‑time‑token multi‑factor authentication, providing step‑by‑step code examples, custom token services, endpoint‑level MFA configuration, and production‑grade considerations.
Introduction
Passwords alone are no longer sufficient as password‑leak incidents hit historic highs. Multi‑factor authentication (MFA) is the most effective defense, but traditional implementations require extensive boilerplate code. Spring Boot 4.0, built on Spring Security 7, introduces a single annotation that handles MFA automatically.
What Is Multi‑Factor Authentication?
MFA requires users to verify their identity through multiple independent factors. OWASP classifies factors into knowledge (e.g., password, PIN), possession (e.g., phone, email), biometric (e.g., fingerprint), location, and behavior. The most common combination is a password (knowledge) plus a one‑time token (possession).
Spring Boot 4.0’s New Weapon
Spring Boot 4.0 adds the @EnableMultiFactorAuthentication annotation, which performs three core actions:
Factor tracking – automatically records which factors a user has completed.
Smart redirection – redirects users to the appropriate authentication page when a factor is missing.
Authority management – assigns separate authorities for each factor.
Two predefined authorities are provided:
FactorGrantedAuthority.PASSWORD_AUTHORITY // password factor
FactorGrantedAuthority.OTT_AUTHORITY // one‑time‑token factorFull Code Walkthrough
Below is a complete example using the Pig Mall admin backend.
Project Dependencies
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.1</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>Security Configuration
@Configuration
@EnableWebSecurity
@EnableMultiFactorAuthentication(authorities = {
FactorGrantedAuthority.PASSWORD_AUTHORITY,
FactorGrantedAuthority.OTT_AUTHORITY
})
public class PigSecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/", "/ott/sent").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(withDefaults())
.oneTimeTokenLogin(withDefaults())
.build();
}
@Bean
UserDetailsService pigUserDetailsService() {
var lengleng = User.withUsername("lengleng")
.password("{noop}pig123")
.roles("ADMIN", "USER")
.build();
var guest = User.withUsername("guest")
.password("{noop}guest")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(lengleng, guest);
}
}One‑Time Token Service (Custom PIN)
public class PigPinTokenService implements OneTimeTokenService {
private static final int PIN_LENGTH = 5;
private static final int MAX_PIN = 100_000;
private final Map<String, OneTimeToken> tokens = new ConcurrentHashMap<>();
private final SecureRandom random = new SecureRandom();
private Duration expiresIn = Duration.ofMinutes(5);
@Override
public OneTimeToken generate(GenerateOneTimeTokenRequest request) {
String pin = generatePin();
Instant expiresAt = Instant.now().plus(expiresIn);
OneTimeToken token = new DefaultOneTimeToken(pin, request.getUsername(), expiresAt);
tokens.put(pin, token);
cleanExpiredTokens();
return token;
}
@Override
public OneTimeToken consume(OneTimeTokenAuthenticationToken authToken) {
OneTimeToken token = tokens.remove(authToken.getTokenValue());
if (token == null || Instant.now().isAfter(token.getExpiresAt())) {
return null;
}
return token;
}
private String generatePin() {
int pin = random.nextInt(MAX_PIN);
return String.format("%05d", pin);
}
private void cleanExpiredTokens() {
if (tokens.size() < 100) return;
Instant now = Instant.now();
tokens.entrySet().removeIf(e -> now.isAfter(e.getValue().getExpiresAt()));
}
public void setExpiresIn(Duration expiresIn) {
this.expiresIn = expiresIn;
}
}Bean Registration
@Bean
public OneTimeTokenService pigTokenService() {
PigPinTokenService service = new PigPinTokenService();
service.setExpiresIn(Duration.ofMinutes(3));
return service;
}Advanced Usage: Endpoint‑Level MFA
To protect specific endpoints only, use AuthorizationManagerFactories.multiFactor():
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
var mfa = AuthorizationManagerFactories.multiFactor()
.requireFactors(FactorGrantedAuthority.PASSWORD_AUTHORITY, FactorGrantedAuthority.OTT_AUTHORITY)
.build();
return http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/admin/**").access(mfa.hasRole("ADMIN"))
.requestMatchers("/user/**").authenticated()
.anyRequest().permitAll()
)
.formLogin(withDefaults())
.oneTimeTokenLogin(withDefaults())
.build();
}You can also set a validity period for the password factor, e.g., 30 minutes without re‑entering the password:
var passwordIn30m = AuthorizationManagerFactories.multiFactor()
.requireFactor(factor -> factor.passwordAuthority().validDuration(Duration.ofMinutes(30)))
.build();Production‑Ready Considerations
Token storage – In‑memory storage is only for demos; use JdbcOneTimeTokenService or Redis in production to avoid token loss on restart.
Token lifetime – A PIN should be valid for 3‑5 minutes; longer lifetimes increase risk, shorter lifetimes hurt usability.
Delivery channel – Tokens must be sent via email or SMS, not printed to logs.
Debugging – Enable Spring Security debugging during development: @EnableWebSecurity(debug = true) and set logging level to TRACE for org.springframework.security.
Number of factors – Two factors strike the best balance; adding more factors quickly degrades user experience.
Conclusion
The @EnableMultiFactorAuthentication annotation in Spring Boot 4.0 transforms MFA from a labor‑intensive custom implementation into a declarative, production‑ready solution, handling factor tracking, smart redirection, and authority management with just a single line of configuration.
Java Architecture Diary
Committed to sharing original, high‑quality technical articles; no fluff or promotional content.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
