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?
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