How can prevent Vaadin redirecting from all pages from BasicAuth to /login?

Hi everyone, I have many different api endpoints, I also have a swagger that I want to access via BasicAuth, how can I prevent Vaadin redirecting from all pages from BasicAuth to /backoffice/login. Thanks in advance!

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends VaadinWebSecurity {

    private final JwtAuthenticationFilter jwtAuthenticationFilter;
    private final DatabaseUserDetailsService databaseUserDetailsService;
    private final PasswordEncoder bcryptPasswordEncoder;
    private final CustomVaadinAuthenticationProvider customVaadinAuthenticationProvider;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // Authentication provider
        configureAuthenticationProvider(http.getSharedObject(AuthenticationManagerBuilder.class));
      
        setLoginView(http, "/login", "/login");
        super.configure(http);
    }

    /**
     * Swagger filter via Basicauth
     */
    @Bean
    @Order(1)
    public SecurityFilterChain swaggerSecurityFilterChain(HttpSecurity http) throws Exception {
        http
                .securityMatcher("/swagger-ui/**", "/v3/api-docs/**")
                .csrf(CsrfConfigurer::disable)
                .csrf(csrf -> csrf.ignoringRequestMatchers(AntPathRequestMatcher.antMatcher("/swagger-ui/**"),
                        AntPathRequestMatcher.antMatcher("/v3/api-docs/**")))
                .authorizeHttpRequests(auth -> auth
                        // Require authentication for all Swagger requests
                        .anyRequest().hasAnyAuthority(RoleName.ROLE_ADMIN.name(), RoleName.ROLE_EDITOR.name()))
                // Turn on the BasicAuth
                .httpBasic(Customizer.withDefaults())
                // Turn off the session
                .sessionManagement(session ->
                        session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
        return http.build();
    }

    /**
     * Filter for access to ento -users /**, available only to admin
     */
    @Bean
    @Order(2)
    public SecurityFilterChain adminSecurityFilterChain(HttpSecurity http) throws Exception {
        http
                .cors(customizer->{})
                .securityMatcher("/users/**") 
                .csrf(CsrfConfigurer::disable)
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/users/activate/*", "/users/archive/*")
                        .hasAnyAuthority(RoleName.ROLE_ADMIN.name(), RoleName.ROLE_APPLICATION.name())
                        .requestMatchers("/users/email")
                        .hasAuthority(RoleName.ROLE_DEVELOPER.name())
                        // Only for admins
                        .anyRequest().hasAuthority(RoleName.ROLE_ADMIN.name()))
                //  Turn on the BasicAuth
                .httpBasic(Customizer.withDefaults())
                // JWT support
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                // Turn off the session
                .sessionManagement(session -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS));
        return http.build();
    }

    /**
     * Vaadin admin configuration
     */
    @Bean
    @Order(3)
    public SecurityFilterChain vaadinSecurityFilterChain(HttpSecurity http) throws Exception {
        http
                .securityMatcher("/", "/backoffice/**")
                .csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/backoffice/login/**").permitAll())
                .formLogin(form -> form
                        .loginPage("/backoffice/login").permitAll()
                        .loginProcessingUrl("/backoffice/login")
                        .defaultSuccessUrl("/backoffice/", true))
                .sessionManagement(session -> session
                        .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED));
        return http.build();
    }

    protected void configureAuthenticationProvider(AuthenticationManagerBuilder auth) {
        auth.authenticationProvider(customVaadinAuthenticationProvider);
    }
 
    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(databaseUserDetailsService);
        provider.setPasswordEncoder(bcryptPasswordEncoder);
        return provider;
    }
}

You can exclude URLs

1 Like

I don’t understand the reason behind your vaadinSecurityFilterChain bean. It looks like to me that in this way you are creating an additional filter chain that is not related to the one provided by VaadinWebSecurity.

If your goal is to customize the Vaadin filter chain, I think you should instead override the VaadinWebSecurity.filterChain method and add the same @Bean(name = "VaadinSecurityFilterChainBean") annotation on the overriding method.

2 Likes

Thanks, I would like to disallow automatic redirects other than from /backoffice/ to /backoffice/login. Do you know of any examples of how I could do this?

Sorry, I don’t understand what redirects are you talking about.
Do you mean you are redirected to /backoffice/login when accessing, for example /users/something, instead of having basic authentication triggered?

Yes. I have many filterChains with different authentication and I don’t want to merge them into one configuration. And I want to remove the redirect from all closed pages to /backoffice/login

Could you please explain what do you mean by “closed pages”?
I am assuming the URIs protected by non-Vaadin security filter chain, are not Vaadin views.

Did you already try the previous suggestion of overriding VaadinWebSecurity.filterChain method?

By “closed pages,” I meant pages that are restricted to unauthenticated users. I wrote a small filterChain with @Bean(name = "VaadinSecurityFilterChainBean"), but it doesn’t work as expected. If a user is on a page that does not start with /backoffice/, there should be no automatic redirect to /backoffice/login. However, the user is still being redirected there.

Could you please suggest how I can modify the filterChain to prevent the automatic redirect to /backoffice/login for unauthenticated users? I tried adding http.authorizeHttpRequests(auth -> auth.requestMatchers("/**").permitAll()), but it doesn’t work properly.

    @Bean(name = "VaadinSecurityFilterChainBean")
    @Order(4)
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return super.filterChain(http);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
      configureAuthenticationProvider(http.getSharedObject(AuthenticationManagerBuilder.class));

        http
                .securityMatcher("/", "/backoffice/**")
                .csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/backoffice/login/**").permitAll())
                .formLogin(form -> form
                        .loginPage("/backoffice/login").permitAll()
                        .loginProcessingUrl("/backoffice/login")
                        .defaultSuccessUrl("/backoffice/", true));

        super.configure(http);
        setLoginView(http, "/login", "/login");
    }

Can you provide additional information about your application?

Is Vaadin Servlet mapped to root (/*, that is the default) or do you have a custom mapping /backoffice/*?
Are resources under /users/** Vaadin views or different technology?

setLoginView(http, “/login”, “/login”);

Login view setting seems incorrect; shouldn’t it be /backoffice/login? Does this path match with the path of @Route annotation on the View? Or is the login page not a Vaadin view?

I tested your configuration locally, only changing the setLoginView part, and if I do a request to /users/something I can see the basic auth login dialog pop up, no redirects to the login view. In my example, /users/something is served by a @RestController

I have a large multi-module application with a huge number of API endpoints spread across different filterChain configurations. I was asked to integrate an admin panel with URLs starting with /backoffice/. I configured the settings in the application file:

vaadin:
  whitelisted-packages: com/virit/vaadin_admin/views 
  urlMapping: /backoffice/*

Also I wrote a provider that interacts with the database to assign roles:

@Component
@RequiredArgsConstructor
public class CustomVaadinAuthenticationProvider implements AuthenticationProvider {
    private final UserService userService;
    private final PasswordEncoder bcryptPasswordEncoder;
    private final LdapService ldapService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String login = authentication.getName();
        String password = authentication.getCredentials().toString();

        Optional<User> optionalUser = userService.findByEmail(login);
        if (optionalUser.isEmpty()) {
            throw new BadCredentialsException("User not found: " + login);
        }

        Optional<User> user = optionalUser
                .filter(u -> u.getType() == UserType.CORE)
                .filter(u -> bcryptPasswordEncoder.matches(password, u.getPassword()));

        if (user.isEmpty()) {
            throw new BadCredentialsException("Password is incorrect: " + login);
        }
        UserResponseDto userResponse = userService.getUserResponseDtoByEmail(user.get().getEmail());

        // Создание пустого списка ролей
        List<GrantedAuthority> authorities = new ArrayList<>();

        // Создание списка ролей текущего пользователя
        List<RoleName> userRoles = userResponse.getRoles();
        if (userRoles.isEmpty()) {
            throw new BadCredentialsException("User role not found");
        }

        // Добавляем роля
        for (RoleName userRole: userRoles) {
            authorities.add(
                    new SimpleGrantedAuthority(userRole.toString())
            );
        }

        // Возвращаем аутентификацию с данными
        return new UsernamePasswordAuthenticationToken(login, password, authorities);
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

And set up the security configuration:

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends VaadinWebSecurity {

    private final JwtAuthenticationFilter jwtAuthenticationFilter;
    private final DatabaseUserDetailsService databaseUserDetailsService;
    private final PasswordEncoder bcryptPasswordEncoder;
    private final CustomVaadinAuthenticationProvider customVaadinAuthenticationProvider;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
configureAuthenticationProvider(http.getSharedObject(AuthenticationManagerBuilder.class));
        
        setLoginView(http, "/login", "/login");
        super.configure(http);
    }

    @Bean
    @Order(1)
    public SecurityFilterChain swaggerSecurityFilterChain(HttpSecurity http) throws Exception {
        http
                .securityMatcher("/swagger-ui/**", "/v3/api-docs/**")
                .csrf(CsrfConfigurer::disable)
                .authorizeHttpRequests(auth -> auth
                        .anyRequest().hasAnyAuthority(RoleName.ROLE_ADMIN.name(), RoleName.ROLE_EDITOR.name()))
                .httpBasic(Customizer.withDefaults())
                .sessionManagement(session ->
                        session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
        return http.build();
    }

    @Bean
    @Order(2)
    public SecurityFilterChain adminSecurityFilterChain(HttpSecurity http) throws Exception {
        http
                .cors(customizer->{})
                .securityMatcher("/users/**", "/whitelist/**", "/auth/register-application") 
                .csrf(CsrfConfigurer::disable)
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/users/activate/*", "/users/archive/*")
                        .hasAnyAuthority(RoleName.ROLE_ADMIN.name(), RoleName.ROLE_APPLICATION.name())
                        .requestMatchers("/users/email")
                        .hasAuthority(RoleName.ROLE_DEVELOPER.name())
                        .anyRequest().hasAuthority(RoleName.ROLE_ADMIN.name()))
                .httpBasic(Customizer.withDefaults())
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                .sessionManagement(session -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS));
        return http.build();
    }

    @Bean
    @Order(3)
    public SecurityFilterChain vaadinSecurityFilterChain(HttpSecurity http) throws Exception {
        http
                .securityMatcher("/", "/backoffice/**")
                .csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/backoffice/login/**").permitAll())
                .formLogin(form -> form
                        .loginPage("/backoffice/login").permitAll()
                        .loginProcessingUrl("/backoffice/login")
                        .defaultSuccessUrl("/backoffice/", true));
        return http.build();
    }

    protected void configureAuthenticationProvider(AuthenticationManagerBuilder auth) {
        auth.authenticationProvider(customVaadinAuthenticationProvider);
    }


    @Bean
    public OpenApiCustomizer removeEndpointController() {
        return openApi -> openApi.getPaths().keySet()
                .removeIf(path -> path.startsWith("/connect/"));
    }

    @Bean
    public GroupedOpenApi publicApi(OpenApiCustomizer customizer) {
        return GroupedOpenApi.builder()
                .group("public")
                .pathsToMatch("/**")
                .addOpenApiCustomizer(customizer)
                .build();
    }


    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(databaseUserDetailsService);
        provider.setPasswordEncoder(bcryptPasswordEncoder);
        return provider;
    }
}

I just want the Vaadin admin panel to work only under /backoffice/ and not redirect unauthenticated users from other pages to itself.

I’m sorry, but I cannot reproduce the problem.
In your configuration, I can see only a couple of things that can be modified:

  • do not add another filter chain called vaadinSecurityFilterChain. As reported previously, override the filterChain method of VaadinWebSecurity if you want to apply the @Order annotation(and add the same@Bean annotation as the overridden method)
  • setLoginView can use the login view class
  • securityMatcher("/", "/backoffice/**"): since you have a custom mapping for VaadinServlet make sure to use an AntPathRequestMatcher instead of an MVC request matcher (http.securityMatcher(new AntPathRequestMatcher("/backoffice/**")). My opinion is that / should not be added.

I don’t know how the JwtAuthenticationFilter filter works. Make sure it is the one that’s doing the redirecting.

I suggest you to set TRACE log level for Spring Security and analyze the logs to understand what’s going on (logging.level.org.springframework.security.web = trace)

1 Like