2FA in Spring Security with Vaadin 25

Hello!

we’re currently trying to get a 2FA implemented with Spring Security working with the latest v25 betas. Our implementation is hugely based on this blog post: Implementierung von Zwei-Faktor-Authentifizierung (2FA) in einer Spring Boot Anwendung (german).

Basically the flow is as following (details in the blog post):

  1. Normal username/password auth → UsernamePasswordAuthenticationToken, empty authorities, “2FA_REQUIRED” in auth details
  2. 2FA-Filter which checks the requests if “2FA_REQUIRED” is inside the auth details → Redirects to /verify page
  3. After 2FA verification → Set authorities and redirect to requested page

We’re struggeling how to get the filter chain working together with the Vaadin Login Flow and the new VaadinSecurityConfigurer :

    @Bean // Defines a bean to configure the security filter chain
    public SecurityFilterChain configure(HttpSecurity http) throws Exception {
        http
                // Configure form-based authentication
                .formLogin(form -> form
                                   .loginPage("/login") // Specify the custom login page URL
                                   .loginProcessingUrl("/login") // URL to submit the username and password
                                   .defaultSuccessUrl("/verify", true) // Redirect to OTP verification upon successful login
                                   .permitAll() // Allow all users to access the login page
                          )

                .authorizeHttpRequests(authorize -> authorize
                                               // Require authentication for any other requests not previously specified
                                               .anyRequest().authenticated()
                                      )

                // Register the custom authentication provider with Spring Security
                .authenticationProvider(customAuthProvider)

                // Add the TwoFactorAuthenticationFilter after the UsernamePasswordAuthenticationFilter in the filter chain
                .addFilterAfter(twoFactorFilter, UsernamePasswordAuthenticationFilter.class);

        // Build and return the configured SecurityFilterChain
        return http.build();
    }

Aditionally, is the LoginForm usable for our case? I think the /verify-Redirect may break the vaadin callback redirection to the requested page?

Currently our setup is as following:

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .with(VaadinSecurityConfigurer.vaadin(), configurer -> {
                    configurer.anyRequest(AuthorizeHttpRequestsConfigurer.AuthorizedUrl::authenticated);
                    configurer.loginView(LoginView.class);
                })
                // Configure form-based authentication
                .formLogin(form -> form
                        .successForwardUrl("/verify")
                        .loginPage("/login") // Specify the custom login page URL
                        .loginProcessingUrl("/login") // URL to submit the username and password
                        .defaultSuccessUrl("/verify", true) // Redirect to OTP verification upon successful login
                        .permitAll() // Allow all users to access the login page
                )

                // Register the custom authentication provider with Spring Security
                .authenticationProvider(customAuthProvider)

                // Add the TwoFactorAuthenticationFilter after the UsernamePasswordAuthenticationFilter in the filter chain
                .addFilterAfter(twoFactorFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

Login seems to work in basic features but for the filter, we are struggeling to filter out which requests need to be redirected:

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

        System.out.println("===");
        System.out.println("Request URI: " +  request.getRequestURI());
        System.out.println("Framework internal request: " + requestUtil.isFrameworkInternalRequest(request));
        System.out.println("Endpoint request: " + requestUtil.isEndpointRequest(request));

        if (auth != null
                && "2FA_REQUIRED".equals(auth.getDetails())
                && !("/verify".equals(request.getRequestURI()))
                && !requestUtil.isFrameworkInternalRequest(request)
                && requestUtil.isEndpointRequest(request)
        ) {
            response.sendRedirect("/verify");
            return;
        }
    }

For example the login redirect to the dashboard after login is marked as

Request URI: /
Framework internal request: true
Endpoint request: false

In my thoughts it’s at least should be an endpoint request but no internal request?
DashboardView is

@PermitAll
@Route("")
@PageTitle("Dashboard")
@Menu(order = 0, icon = "vaadin:clipboard-check", title = "Dashboard")
public class DashboardView extends Main {

    public DashboardView() {
        add(new Text(("Dashboard")));
    }

}

Any advices?

Personally, I wouldn’t implement this on my own.
Using Keycloak on-prem or a cloud provider like Zitadel or Okta/Auth0 would be my choice.

1 Like

@SimonMartinelli We’re using Keycloak in most cases. I can’t go into detail but due to external constraints it’s not feasible in this scenario.

If you can’t use Keycloak… the easiest solution might be to just ditch the spring security way and use a “simple” modal dialog or a Vaadin Route that is always shown after login and enforces that the user supplies the 2FA.

I’m doing something similar if a user is allowed to use my application for multiple tenants - after the initial login he is forced to select the tenant he wanna use.