spring-securityvaadinkeycloakvaadin23

Vaadin 23 Home page with anonymous access


I have the following routes and views defined:

    private void createDrawer() {

        Tabs tabs = new Tabs();
        tabs.add(
                createTab(VaadinIcon.HOME, "Home", HomeView.class),
                createTab(VaadinIcon.DASHBOARD, "Dashboard", DashboardView.class),
                createTab(VaadinIcon.INBOX, "Inbox", InboxView.class),
                createTab(VaadinIcon.WORKPLACE, "Jobs", JobsView.class),
                createTab(VaadinIcon.USER, "Candidates", CandidatesView.class),
                createTab(VaadinIcon.GAVEL, "Administration", AdministrationView.class)
        );
        tabs.setOrientation(Tabs.Orientation.VERTICAL);

        addToDrawer(tabs);
    }

    private Tab createTab(VaadinIcon viewIcon, String viewName, Class<? extends Component> navigationTarget) {
        Icon icon = viewIcon.create();
        icon.getStyle()
                .set("box-sizing", "border-box")
                .set("margin-inline-end", "var(--lumo-space-m)")
                .set("margin-inline-start", "var(--lumo-space-xs)")
                .set("padding", "var(--lumo-space-xs)");

        RouterLink link = new RouterLink();
        link.add(icon, new Span(viewName));
        link.setRoute(navigationTarget);
        link.setTabIndex(-1);

        return new Tab(link);
    }

Each view defines the following annotations, for example

@Scope("prototype")
@Component
@Route(value = "", layout = MainLayout.class)
@PageTitle("Home")
@PermitAll
public class HomeView extends VerticalLayout {

Because of @PermitAll it requires from user to be authenticated. I configured Spring Security with Keycloak 18 and everything works fine. When I try to access a secure page, for example http://localhost:8080/ - the system automatically redirects me to Keycloak login page.

Now, I'd like to allow anonymous access for the Home page (HomeView) of my application. For that purpose I change @PermitAll to @AnonymousAllowed:

@Scope("prototype")
@Component
@Route(value = "", layout = MainLayout.class)
@PageTitle("Home")
@AnonymousAllowed
public class HomeView extends VerticalLayout {

Now I'm able to access http://localhost:8080/ (HomeView) with anonymous user. That's fine. But when I click on another tab link, which represents for example DashboardView, now it shows me the following message:

Could not navigate to 'dashboard'

Available routes:

    <root>
    administration
    candidates
    dashboard
    inbox
    jobs
    list
    login

This detailed message is only shown when running in development mode. Instead of that I expect that the system will automaticall redirect me to Keycloak login page.

The DashboardView definition:

@Scope("prototype")
@Route(value = "dashboard", layout = MainLayout.class)
@PageTitle("Dashboard")
@PermitAll
public class DashboardView extends VerticalLayout {

What am I doing wrong and how to fix it?

UPDATED

I enabled DEBUG on com.vaadin and now may see the following messages:

2022-07-25 18:35:25.564 DEBUG 18984 --- [nio-8080-exec-8] c.v.flow.spring.SpringViewAccessChecker  : Checking access for view com.decisionwanted.ui.views.dashboard.DashboardView
2022-07-25 18:35:25.564 DEBUG 18984 --- [nio-8080-exec-8] c.v.flow.spring.SpringViewAccessChecker  : Denied access to view com.decisionwanted.ui.views.dashboard.DashboardView
2022-07-25 18:35:25.567 DEBUG 18984 --- [nio-8080-exec-8] c.v.flow.spring.SpringViewAccessChecker  : Checking access for view com.vaadin.flow.router.RouteNotFoundError
2022-07-25 18:35:25.568 DEBUG 18984 --- [nio-8080-exec-8] c.v.flow.spring.SpringViewAccessChecker  : Allowed access to view com.vaadin.flow.router.RouteNotFoundError
2022-07-25 18:35:25.580 DEBUG 18984 --- [nio-8080-exec-8] c.vaadin.flow.router.RouteNotFoundError  : Route is not found

com.vaadin.flow.router.NotFoundException: null
    at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) ~[na:na]
    at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:77) ~[na:na]
    at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) ~[na:na]
    at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499) ~[na:na]
    at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480) ~[na:na]
    at com.vaadin.flow.internal.ReflectTools.createProxyInstance(ReflectTools.java:484) ~[flow-server-23.1.3.jar:23.1.3]
    at com.vaadin.flow.internal.ReflectTools.createInstance(ReflectTools.java:452) ~[flow-server-23.1.3.jar:23.1.3]
    at com.vaadin.flow.router.BeforeEvent.rerouteToError(BeforeEvent.java:766) ~[flow-server-23.1.3.jar:23.1.3]
    at com.vaadin.flow.router.BeforeEvent.rerouteToError(BeforeEvent.java:750) ~[flow-server-23.1.3.jar:23.1.3]
    at com.vaadin.flow.server.auth.ViewAccessChecker.beforeEnter(ViewAccessChecker.java:192) ~[flow-server-23.1.3.jar:23.1.3]

This is my SecurityConfig:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfiguration extends VaadinWebSecurityConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    final ClientRegistrationRepository clientRegistrationRepository;
    final GrantedAuthoritiesMapper authoritiesMapper;

    SecurityConfiguration(ClientRegistrationRepository clientRegistrationRepository,
                          GrantedAuthoritiesMapper authoritiesMapper) {
        this.clientRegistrationRepository = clientRegistrationRepository;
        this.authoritiesMapper = authoritiesMapper;
        SecurityContextHolder.setStrategyName(VaadinAwareSecurityContextHolderStrategy.class.getName());
    }

    @Bean
    public SessionRepository sessionRepository() {
        return new SessionRepository();
    }
    
    @Bean
    public ServletListenerRegistrationBean<SessionRepositoryListener> sessionRepositoryListener() {
        var bean = new ServletListenerRegistrationBean<SessionRepositoryListener>();
        bean.setListener(new SessionRepositoryListener(sessionRepository()));
        return bean;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        http
                // Enable OAuth2 login
                .oauth2Login(oauth2Login ->
                        oauth2Login
                                .clientRegistrationRepository(clientRegistrationRepository)
                                .userInfoEndpoint(userInfoEndpoint ->
                                        userInfoEndpoint
                                                // Use a custom authorities mapper to get the roles from the identity provider into the Authentication token
                                                .userAuthoritiesMapper(authoritiesMapper)
                                )
                                // Use a Vaadin aware authentication success handler
                                .successHandler(new VaadinSavedRequestAwareAuthenticationSuccessHandler())
                )
                // Configure logout
                .logout(logout ->
                        logout
                                // Enable OIDC logout (requires that we use the 'openid' scope when authenticating)
                                .logoutSuccessHandler(logoutSuccessHandler())
                                // When CSRF is enabled, the logout URL normally requires a POST request with the CSRF
                                // token attached. This makes it difficult to perform a logout from within a Vaadin
                                // application (since Vaadin uses its own CSRF tokens). By changing the logout endpoint
                                // to accept GET requests, we can redirect to the logout URL from within Vaadin.
                                .logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET"))
                );
    }

    private OidcClientInitiatedLogoutSuccessHandler logoutSuccessHandler() {
        var logoutSuccessHandler = new OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository);
        logoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}");
        return logoutSuccessHandler;
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
        // Don't apply security rules on our static pages
        web.ignoring().antMatchers("/session-expired");
    }

    @Bean
    public PolicyFactory htmlSanitizer() {
        // This is the policy we will be using to sanitize HTML input
        return Sanitizers.FORMATTING.and(Sanitizers.BLOCKS).and(Sanitizers.STYLES).and(Sanitizers.LINKS);
    }

}

Why Vaadin doesn't redirect me to Keycloak login page and fails instead of that? How to configure redirection to Keycloak login page in such a case in order to authenticate the user?


Solution

  • After hours of debugging, it looks like I've found a solution to this problem:

    I created my own class (extended SpringViewAccessChecker):

    public class KeycloakSpringViewAccessChecker extends SpringViewAccessChecker {
    
        private AccessAnnotationChecker accessAnnotationChecker;
        private String keycloakAuthorizationUrl;
    
        public KeycloakSpringViewAccessChecker(AccessAnnotationChecker accessAnnotationChecker, String keycloakAuthorizationUrl) {
            super(null);
            this.accessAnnotationChecker = accessAnnotationChecker;
            this.keycloakAuthorizationUrl = keycloakAuthorizationUrl;
        }
    
        @Override
        public void beforeEnter(BeforeEnterEvent beforeEnterEvent) {
            Class<?> targetView = beforeEnterEvent.getNavigationTarget();
            VaadinRequest request = VaadinRequest.getCurrent();
    
            Principal principal = getPrincipal(request);
            Function<String, Boolean> rolesChecker = getRolesChecker(request);
    
            boolean hasAccess = accessAnnotationChecker.hasAccess(targetView, principal, rolesChecker);
    
            if (!hasAccess) {
    
                if (principal == null) {
                    HttpSession session = (request instanceof VaadinServletRequest) ? ((VaadinServletRequest) request).getSession() : null;
                    if (session != null) {
                        session.setAttribute(SESSION_STORED_REDIRECT, "/" + beforeEnterEvent.getLocation().getPathWithQueryParameters());
                    }
                }
    
                beforeEnterEvent.getUI().getPage().setLocation(keycloakAuthorizationUrl);
            }
        }
    
    }
    

    Overrided a bean with my implementation:

    @Bean
    @Primary
    public SpringViewAccessChecker springViewAccessChecker(AccessAnnotationChecker accessAnnotationChecker) {
        return new KeycloakSpringViewAccessChecker(accessAnnotationChecker, "/oauth2/authorization/keycloak");
    }
    

    and looks like it is now working with Keycloak as expected