How to handle Access Denied properly in Vaadin 14 LTS

Viewed 412

I started implementing authentication and authorization for our applications written in Spring Boot (2.2.6.RELEASE) and Vaadin 14 LTS (14.6.1).

I have followed those resources:

I have code for checking whether logged-in user has access rights to specified resources implemented in beforeEnter method. The problem is with invocation of event.rerouteToError(AccessDeniedException.class);. It tries to create an instance of the specified exception with reflection but fails because it does not contain public no-arg constructor.

private void beforeEnter(final BeforeEnterEvent event) {
    if (!AuthView.class.equals(event.getNavigationTarget()) && !AuthUtils.isUserLoggedIn()) {
        event.rerouteTo(AuthView.class);
    }

    if (!AuthUtils.isAccessGranted(event.getNavigationTarget())) {
        event.rerouteToError(AccessDeniedException.class);
    }
}
java.lang.IllegalArgumentException: Unable to create an instance of 'org.springframework.security.access.AccessDeniedException'. Make sure the class has a public no-arg constructor.
    at com.vaadin.flow.internal.ReflectTools.createProxyInstance(ReflectTools.java:519)
    at com.vaadin.flow.internal.ReflectTools.createInstance(ReflectTools.java:451)
    at com.vaadin.flow.router.BeforeEvent.rerouteToError(BeforeEvent.java:720)
    at com.vaadin.flow.router.BeforeEvent.rerouteToError(BeforeEvent.java:704)

What can be the best solution for that case? I am thinking about two possible solutions:

  1. First instantiate AccessDeniedException and then pass it to overloaded method in BeforeEvent: public void rerouteToError(Exception exception, String customMessage) which should skip creating exception object by reflection
  2. Create dedicated ErrorView and use method public void rerouteTo(Class<? extends Component> routeTargetType, RouteParameters parameters) of BeforeEvent

I decided to follow Leif Åstrand's answer. I created custom AccessDeniedException and appropriate error handler. Here is my implementation. Maybe it will be helpful for someone.

public class AccessDeniedException extends RuntimeException {
    private final int code;

    public AccessDeniedException() {
        super("common.error.403.details");
        this.code = HttpServletResponse.SC_FORBIDDEN;
    }

    public int getCode() {
        return code;
    }

}
@Tag(Tag.DIV)
@CssImport(value = "./styles/access-denied-view.css")
@CssImport(value = "./styles/access-denied-box.css", themeFor = "vaadin-details")
public class AccessDeniedExceptionHandler extends VerticalLayout implements HasErrorParameter<AccessDeniedException> {

    private final Details details;

    public AccessDeniedExceptionHandler() {
        setWidthFull();
        setHeight("100vh");
        setPadding(false);
        setDefaultHorizontalComponentAlignment(Alignment.CENTER);
        setJustifyContentMode(JustifyContentMode.CENTER);
        setClassName(ComponentConstants.ACCESS_DENIED_VIEW);

        this.details = new Details();
        this.details.setClassName(ComponentConstants.ACCESS_DENIED_BOX);
        this.details.addThemeVariants(DetailsVariant.REVERSE, DetailsVariant.FILLED);
        this.details.setOpened(true);

        add(this.details);
    }

    @Override
    public final int setErrorParameter(final BeforeEnterEvent event, final ErrorParameter<AccessDeniedException> parameter) {
        final int code = parameter.getException().getCode();

        this.details.setSummaryText(getTranslation("common.error.403.header", code));
        this.details.setContent(new Text(getTranslation(parameter.getException().getMessage())));

        return code;
    }

}
2 Answers

I would recommend creating a custom exception type instead of reusing AccessDeniedException from Spring. In that way, you don't have to deal with the required error message at all.

As you mentioned in your first solution, you could do:

event.rerouteToError(new AccessDeniedException("Navigation target not permitted"), "");
  • or maybe also specify the customMessage if you want. If you see the implementation of the rerouteToError(Class) method, it just passes empty customMessage and creates the Exception - which you could do manually and that's completely acceptable. I recommend this solution.

Another solution could be to subclass AccessDeniedException and use that with reflection:

public class RouteAccessDeniedException extends AccessDeniedException {
    public RouteAccessDeniedException() {
        super("Navigation target not permitted");
    }
}

I don't recommend this solution.

Related