Spring Boot
Spring Security
Multi-Factor Authentication
Java
Authentication

Multi-Factor Authentication with Spring Boot 2 and Spring Security 5

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

Multi-factor authentication in Spring Boot 2 is usually built as a two-step login flow: password first, then a second factor such as a time-based one-time password. Spring Security 5 gives you the authentication hooks, but you still need to decide how to represent the intermediate state between "password accepted" and "fully authenticated."

Model MFA as Two Distinct Steps

A clean mental model is:

  1. verify username and password
  2. if the user has MFA enabled, require an OTP before granting full access

That means a successful password check does not always mean a complete login. Instead, it may mean "first factor passed, continue to OTP verification."

A simple user entity might look like this:

java
1public class AppUser {
2    private Long id;
3    private String username;
4    private String passwordHash;
5    private boolean mfaEnabled;
6    private String totpSecret;
7
8    public boolean isMfaEnabled() {
9        return mfaEnabled;
10    }
11
12    public String getTotpSecret() {
13        return totpSecret;
14    }
15}

The important fields are the MFA flag and the TOTP secret used to validate the second factor.

Authenticate the Password First

Your normal UserDetailsService and password encoder still handle the first factor. The difference is what happens after the password is correct.

java
1@Service
2public class AppUserDetailsService implements UserDetailsService {
3    @Override
4    public UserDetails loadUserByUsername(String username) {
5        return User.withUsername(username)
6                .password("{bcrypt}$2a$10$exampleexampleexampleexampleexampleexampleex")
7                .roles("USER")
8                .build();
9    }
10}

In a real application you would load the user from a database. The key design choice is whether the authentication success handler sends the user directly to the app or redirects to an MFA screen.

Redirect to an MFA Verification Step

A common pattern is to store a temporary marker in the session after password login, then redirect to /mfa.

java
1@Component
2public class MfaLoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
3    @Override
4    public void onAuthenticationSuccess(
5            HttpServletRequest request,
6            HttpServletResponse response,
7            Authentication authentication) throws IOException, ServletException {
8
9        request.getSession().setAttribute("MFA_PENDING_USER", authentication.getName());
10        getRedirectStrategy().sendRedirect(request, response, "/mfa");
11    }
12}

This is easier to reason about than trying to overload one authentication object with both incomplete and complete states.

Verify the TOTP Code

For the second factor, many Spring applications use a TOTP library such as Google Authenticator compatible code generation. The service interface can stay simple.

java
1@Service
2public class TotpService {
3    public boolean verifyCode(String secret, int code) {
4        // Replace with a real TOTP library call.
5        return code == 123456;
6    }
7}

Then a controller can complete the MFA flow:

java
1@Controller
2public class MfaController {
3    private final TotpService totpService;
4
5    public MfaController(TotpService totpService) {
6        this.totpService = totpService;
7    }
8
9    @GetMapping("/mfa")
10    public String mfaPage() {
11        return "mfa";
12    }
13
14    @PostMapping("/mfa")
15    public String verify(
16            @RequestParam int code,
17            HttpServletRequest request) {
18
19        String username = (String) request.getSession().getAttribute("MFA_PENDING_USER");
20        if (username == null) {
21            return "redirect:/login";
22        }
23
24        String secret = "user-secret";
25        if (!totpService.verifyCode(secret, code)) {
26            return "mfa";
27        }
28
29        request.getSession().removeAttribute("MFA_PENDING_USER");
30        request.getSession().setAttribute("MFA_VERIFIED", true);
31        return "redirect:/";
32    }
33}

This example is intentionally simplified, but the flow is the important part: pending state first, verified state second.

Configure Spring Security Around the Flow

Your security configuration should allow access to the login page and MFA page, but require a completed login for protected resources.

java
1@EnableWebSecurity
2public class SecurityConfig extends WebSecurityConfigurerAdapter {
3    private final MfaLoginSuccessHandler successHandler;
4
5    public SecurityConfig(MfaLoginSuccessHandler successHandler) {
6        this.successHandler = successHandler;
7    }
8
9    @Override
10    protected void configure(HttpSecurity http) throws Exception {
11        http
12            .authorizeRequests()
13                .antMatchers("/login", "/mfa").permitAll()
14                .anyRequest().authenticated()
15            .and()
16            .formLogin()
17                .successHandler(successHandler)
18            .and()
19            .logout();
20    }
21}

In a production system, you would also add logic that blocks access to the rest of the application when MFA is still pending.

Do Not Forget Recovery and Secret Storage

A working OTP screen is only the start. A real MFA implementation also needs:

  • encrypted storage for the TOTP secret
  • enrollment flow with QR code provisioning
  • backup or recovery codes
  • rate limiting on OTP attempts
  • audit logging for second-factor failures

Without recovery handling, MFA turns into a support problem as soon as users lose their authenticator app or replace a phone.

Common Pitfalls

The biggest mistake is treating password success as full authentication even for users who should still pass a second factor. Another common issue is storing the TOTP secret in plain text or handling OTP verification without rate limiting. Developers also often forget recovery flows, which makes MFA impossible to support operationally. Finally, the intermediate "MFA pending" state must be handled carefully so partially authenticated users cannot reach protected endpoints.

Summary

  • A practical Spring Boot 2 and Spring Security 5 MFA flow is usually password first, OTP second.
  • Model the post-password state as "MFA pending" rather than fully authenticated.
  • Verify the TOTP code in a dedicated step such as /mfa.
  • Securely store secrets and plan for recovery, rate limiting, and auditing.
  • The security challenge is not only generating OTPs, but also correctly representing incomplete versus complete authentication.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.