Login with 2FA in Spring boot 3 + Spring security 6

Login with 2FA in Spring boot 3 + Spring security 6

In today’s digital landscape, ensuring the security of user accounts is paramount. One effective way to enhance security is through the implementation of Two-Factor Authentication (2FA). In this article, we will explore how to integrate 2FA into a Spring Boot application, using Spring Security.

Prerequisites

Before diving into the implementation, make sure to set up a Spring Boot project with the necessary dependencies. Include spring-boot-starter-web and spring-boot-starter-security for basic web and security functionality. Additionally, add the googleauth dependency for Google Authenticator support.

Step 1: Create a Custom User Details Class (AuthUser)

Define a custom user details class (AuthUser) that implements the User interface. This class will represent the user details retrieved from the database.

public class AuthUser extends User {

    private String id;
    private String displayName;
    private boolean isVerify2Fa;
    private boolean isUsing2FA;
    private boolean isLogin2FA;

    public AuthUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, authorities);
    }

    public boolean isVerify2Fa() {
        return isVerify2Fa;
    }

    public void setVerify2Fa(boolean verify2Fa) {
        isVerify2Fa = verify2Fa;
    }

    public boolean isUsing2FA() {
        return isUsing2FA;
    }

    public void setUsing2FA(boolean using2FA) {
        isUsing2FA = using2FA;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public boolean isLogin2FA() {
        return isLogin2FA;
    }

    public void setLogin2FA(boolean login2FA) {
        isLogin2FA = login2FA;
    }

    public String getDisplayName() {
        return displayName;
    }

    public void setDisplayName(String displayName) {
        this.displayName = displayName;
    }
}

Step 2: Create a Custom User Details Service

Create a service class (CustomUserDetailsServiceI) that implements the custom user details service. This class will be responsible for loading user details from the database when users process authenticate on Login Page.

@Service
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    CustomUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User admin = userRepository.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException("Cannot found user"));
        List<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(new SimpleGrantedAuthority("ROLE_" + admin.getRole()));
        AuthUser authUser = new AuthUser(username, admin.getPassword(), authorities);
        authUser.setId(admin.getId().toString());
        authUser.setDisplayName(admin.getFirstName());

        return authUser;
    }
}

Step 3: Configure Security with 2FA in Spring Security 6

In the security configuration class (SecurityConfig), define the security settings, including access rules and the authentication provider for 2FA.

@Configuration
@EnableWebSecurity
public class SpringSecurityConfig {

    @Autowired
    private CustomUserDetailsService customUserDetailsService;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(authz -> authz
            .requestMatchers("/login", "/error").permitAll()
            .anyRequest().authenticated()
        );

        http.formLogin(authz -> authz
            .loginPage("/login").permitAll()
        ).addFilterAfter(new TwoFactorAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

        http.logout(authz -> authz
            .deleteCookies("JSESSIONID")
            .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
        );

        return http.build();
    }

}

Step 4: Create TwoFactorAuthenticationFilter

By workflow, after login success, to enforce OTP verification for certain pages, you can add a custom filter to check if the user has already verified their OTP before accessing protected resources. This can be achieved by creating a filter that intercepts requests and checks the authentication status of the user. If the user has not yet verified their OTP, you can redirect them to the OTP verification page.

public class TwoFactorAuthenticationFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        if (!request.getRequestURI().contains("/verify-otp")
                && authentication != null
                && authentication.isAuthenticated()
                && !hasOtpVerifiedAuthority(authentication)) {
            response.sendRedirect("/verify-otp");
        } else {
            // Continue with the filter chain for other requests
            filterChain.doFilter(request, response);
        }

    }

    private boolean hasOtpVerifiedAuthority(Authentication authentication) {
        // Check if the user has the authority indicating OTP verification
        AuthUser authUser = (AuthUser) authentication.getPrincipal();
        return authUser.isVerify2Fa();
    }
}

Step 5: Create a controller to handle login and OTP verification.

From this example, I will hard code to authenticate OTP, you can create a service or use a 3rd party for this authentication.

@Controller
@RequestMapping
public class LoginController {

    @GetMapping("/login")
    public String loginPage(Model model, HttpServletRequest request) {
        CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
        if (csrfToken != null) {
            model.addAttribute("_csrf", csrfToken);
        }
        return "login";
    }

    @GetMapping("/verify-otp")
    public String verifyOtpPage(Model model, HttpServletRequest request) {
        CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
        if (csrfToken != null) {
            model.addAttribute("_csrf", csrfToken);
        }
        return "otp";
    }

    @PostMapping("/verify-otp")
    public String processVerifyOtp(@RequestParam(value = "otp") String otp) {
        if ("123456".equals(otp)) {
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            AuthUser authUser = (AuthUser) authentication.getPrincipal();
            authUser.setVerify2Fa(true);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        return "redirect:/";
    }
}

With this 2FA login mechanism, you can integrate with Google authenticator or SMS OTP depending on the reasonable business

Summary

Implementing Two-Factor Authentication in a Spring Boot application provides an additional layer of security. By following the steps outlined in this article, you can create a robust authentication system that combines traditional credentials with a second factor, enhancing the overall security posture of your application. Customize the example according to your specific requirements and explore further options for 2FA mechanisms to suit your application’s needs.

Code demo: Github

Leave a Reply

Your email address will not be published. Required fields are marked *