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.

Java Architecture Diary
Java Architecture Diary
Java Architecture Diary
Secure Your Spring Boot 4 Apps with One Annotation: MFA Made Easy

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 factor

Full 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.

MFA flow diagram
MFA flow diagram
JavaSpring BootsecurityMFAMulti-Factor Authentication
Java Architecture Diary
Written by

Java Architecture Diary

Committed to sharing original, high‑quality technical articles; no fluff or promotional content.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.