Docs

Access-Denied & Not-Found Views

Customizing the views shown when navigation to a view is denied or the view doesn’t exist.

When navigation access control denies navigation to a view, Vaadin shows an error view instead of the view. This page describes the default behavior, how to customize the access-denied view, and what a custom not-found view needs in a secured application.

Default Behavior

Users who aren’t authenticated are redirected to the log-in view. For authenticated users without permission, Vaadin uses the default error handler for denied access, RouteAccessDeniedError. It doesn’t render anything itself. It reroutes to the not-found error, so the user sees the same RouteNotFoundError view as for a route that doesn’t exist. As a security precaution, the message therefore doesn’t say whether the navigation target exists. In development mode, the view lists the available routes. In production mode, it shows by default a message that reads, Could not navigate to 'RequestedRouteName'.

Custom Access-Denied View

You can customize what users see when access is denied in three ways:

  • by extending RouteAccessDeniedError;

  • by implementing HasErrorParameter<AccessDeniedException>; or

  • by rerouting to a different error type with the @AccessDeniedErrorRouter annotation.

An error view needs an access annotation of its own, so that navigation access control allows navigation to it. Use @PermitAll or @RolesAllowed. @AnonymousAllowed isn’t recommended, as it exposes information about access restrictions to anonymous users.

The following example extends RouteAccessDeniedError:

Source code
Java
@PermitAll 1
public class CustomAccessDeniedError extends RouteAccessDeniedError {
    @Override
    public int setErrorParameter(BeforeEnterEvent event,
            ErrorParameter<AccessDeniedException> parameter) {
        getElement().setText("Nothing to see here, please move on");
        return HttpStatusCode.FORBIDDEN.getCode();
    }
}
  1. Overrides the @AnonymousAllowed annotation that RouteAccessDeniedError carries, for the reason explained above.

The following example implements HasErrorParameter:

Source code
Java
@Tag(Tag.DIV)
@PermitAll
public static class CustomAccessDeniedError extends Component
        implements HasErrorParameter<AccessDeniedException> {
    @Override
    public int setErrorParameter(BeforeEnterEvent event,
            ErrorParameter<AccessDeniedException> parameter) {
        getElement().setText("Access denied.");
        return HttpStatusCode.FORBIDDEN.getCode();
    }
}

The following example reroutes denied navigation to an administrative view to the not-found view, which is the default view for NotFoundException. Users without access then can’t tell whether the view exists:

Source code
Java
@Route(value = "admin", layout = MainLayout.class)
@PageTitle("Admin View")
@RolesAllowed("ADMIN")
@AccessDeniedErrorRouter(rerouteToError = NotFoundException.class)
public class AdminView extends VerticalLayout {
    // ...
}

Without rerouteToError, the annotation reroutes to the error view for AccessDeniedException. The exception class given in rerouteToError needs a public no-argument constructor.

Custom Not-Found View

A custom not-found view — a class extending RouteNotFoundError, or implementing HasErrorParameter<NotFoundException> — needs an access annotation, so that navigation access control allows it to render. The following example annotates the view with @PermitAll, so that signed-in users see it, and users who aren’t signed in are sent to the log-in view. Use @AnonymousAllowed instead if users who aren’t signed in can navigate in your application, for example between public views. If the view has a parent layout, set with @ParentLayout, the layout needs to allow the same users, or navigation access control blocks the view.

Source code
CustomNotFoundView.java
@PermitAll
public class CustomNotFoundView extends RouteNotFoundError {

    @Override
    public int setErrorParameter(BeforeEnterEvent event,
            ErrorParameter<NotFoundException> parameter) {
        getElement().setText("The page you're looking for doesn't exist.");
        return HttpStatusCode.NOT_FOUND.getCode();
    }
}

With Spring Security

In an application secured with VaadinSecurityConfigurer, the not-found view also needs a relaxed catch-all request rule, so that requests for non-existing pages reach the Vaadin servlet. By default, VaadinSecurityConfigurer denies every request that isn’t explicitly allowed. A URL that doesn’t match any route, entered in the browser’s address bar or opened from a bookmark, is therefore denied by Spring Security before Vaadin can show the not-found view. The annotation doesn’t change this.

The security configuration then relaxes the catch-all rule with anyRequest():

Source code
SecurityConfiguration.java
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.with(VaadinSecurityConfigurer.vaadin(), configurer -> {
        configurer.loginView(LoginView.class)
                .anyRequest(AuthorizedUrl::permitAll); 1
    });
    return http.build();
}
  1. Lets requests that match no other rule reach the Vaadin servlet, which shows the not-found view for them.

This doesn’t expose your views: they’re still protected at the HTTP layer by the request matchers of VaadinSecurityConfigurer, and by navigation access control. However, the catch-all rule also covers every other endpoint of the application, such as REST controllers or Spring Boot Actuator endpoints. Protect those with rules of their own before relaxing the catch-all rule, as rules added to http before the configurer are applied first.

If only signed-in users need to see the not-found view, use AuthorizedUrl::authenticated instead of AuthorizedUrl::permitAll. Users who aren’t signed in are then sent to the log-in view for any unknown URL, and other unmatched endpoints require authentication. See Security Configuration for the other options of anyRequest(), and Handle Errors for a guide to the error views of an application.

Updated –