Documentation

Documentation versions (currently viewingVaadin 24.4 (pre))

Role-Based Access Control for Views

How to restrict access for selected Hilla views based on roles defined for the logged-in user.

It’s possible to restrict access for selected Hilla views, based on roles defined for the logged-in user. This article explains how to do this.

To follow the examples here, you’ll need a Hilla application with authentication enabled. The Authentication With Spring Security page will help you to get started.

Define Roles with Spring Security

Roles are a set of string attributes representing the authorities that are assigned to a user. In Spring Security, the user details used for authentication also specify roles.

Typically, roles are defined in authority strings prefixed with ROLE_. After successful authentication, these are accessible via the GrantedAuthority objects returned by Authentication.getAuthorities(). See the Authentication With Spring Security page for examples of configuration.

Using Roles in TypeScript

A convenient way to use roles for access control in TypeScript views is to add a Hilla endpoint that gets user information, including roles, from Java during authentication. To do this, first define a bean representing information about the user:

public class UserInfo {

    @Nonnull
    private String name;
    @Nonnull
    private Collection<String> authorities;

    public UserInfo(String name, Collection<String> authorities) {
        this.name = name;
        this.authorities = Collections.unmodifiableCollection(authorities);
    }

    public String getName() {
        return name;
    }

    public Collection<String> getAuthorities() {
        return authorities;
    }

}

Next, add the endpoint to get a UserInfo containing authorities for the logged-in user on the client side:

@BrowserCallable
public class UserInfoService {

    @PermitAll
    @Nonnull
    public UserInfo getUserInfo() {
        Authentication auth = SecurityContextHolder.getContext()
                .getAuthentication();

        final List<String> authorities = auth.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.toList());

        return new UserInfo(auth.getName(), authorities);
    }

}

Then, change the authentication implementation in TypeScript to get the user information from the endpoint. Change the auth.ts defined in Authentication With Spring Security as follows:

import { UserInfoService } from 'Frontend/generated/endpoints';
import type UserInfo from 'Frontend/generated/com/vaadin/demo/fusion/security/authentication/UserInfo';

interface Authentication {
  user: UserInfo;
}

let authentication: Authentication | undefined;

export async function login(
  username: string,
  password: string,
  options: LoginOptions = {}
): Promise<LoginResult> {
  return await loginImpl(username, password, {
    ...options,
    async onSuccess() {
      // Get user info from endpoint
      const user = await UserInfoService.getUserInfo();
      authentication = {
        user,
      };
    },
  });
}

Add an isUserInRole() helper, which enables role-based access control checks for the UI.

export function isUserInRole(role: string) {
  if (!authentication) {
    return false;
  }

  return authentication.user.authorities.includes(`ROLE_${role}`);
}

Routes with Access Control

To enable allowed roles to be specified on the view routes, define an extended type ViewRoute, that has a rolesAllowed string, like so:

export type ViewRoute = Route & {
  title?: string;
  children?: ViewRoute[];
  rolesAllowed?: string[];
};

Add a method to check access for the given route by iterating rolesAllowed, using isUserInRole(), as follows:

export function isAuthorizedViewRoute(route: ViewRoute) {
  if (route.rolesAllowed) {
    return route.rolesAllowed.find((role) => isUserInRole(role));
  }

  return true;
}

Then use the method added in the route action to redirect on unauthorized access like this:

export const routes: ViewRoute[] = [
  {
    path: 'protected',
    component: 'protected-view',
    title: 'Protected',
    rolesAllowed: ['ADMIN'],
    action: async (context, commands: Router.Commands) => {
      const route = context.route as ViewRoute;
      if (!isAuthorizedViewRoute(route)) {
        return commands.prevent();
      }
      await import('./protected-view');
      return undefined;
    },
  },
];

Hiding Unauthorized Menu Items

Filter the route list using the isAuthorizedViewRoute() helper defined earlier. Then use the filtered list of routes as menu items:

private get menuRoutes() {
    return routes.filter((route) => route.title).filter(isAuthorizedViewRoute);
  }