Getting started with User Manager
This guide explains the minimal steps to integrate User Manager in a new application.
Prepare the Application
A new application created on start.vaadin.com can be used as a starting point for this tutorial. Open the site, select an empty project, choose Spring Boot as the framework, and download the generated archive. Extract it, import it into your IDE of choice, and verify the application starts correctly by running the main class.
Add the AppJars Repository
AppJars artifacts are published to the public AppJars Maven repository. Add it to your pom.xml so that Maven can resolve the AppJars dependencies:
<repositories>
<repository>
<id>appjars</id>
<name>AppJars Public Repository</name>
<url>https://maven.appjars.com</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
Add the Dependencies
Given that it is a monolithic application, the following dependencies representing the three layers of the appjar must be added:
<dependency>
<groupId>com.appjars</groupId>
<artifactId>appjars-user-manager-flow</artifactId>
</dependency>
<dependency>
<groupId>com.appjars</groupId>
<artifactId>appjars-user-manager-data-impl</artifactId>
</dependency>
<dependency>
<groupId>com.appjars</groupId>
<artifactId>appjars-user-manager-business-impl</artifactId>
</dependency>
After adding them, build the application to confirm that the dependencies are resolved correctly.
Modifying the Main Application Class
The following annotations must be added to the main application class:
@EnableVaadin({"com.appjars.usermanager.flow", "com.example.application"})
@ComponentScan(basePackageClasses = {UserManagerAutoConfiguration.class, AppJarsAutoConfiguration.class, Application.class})
- @EnableVaadin: Instructs Vaadin to process the specified packages for views and components. The first entry covers the User Manager package; the second covers the application's own views.
- @ComponentScan: Instructs Spring to load the beans provided by the appjar as well as the beans of the application itself.
The imports for the classes referenced in @ComponentScan are:
com.appjars.usermanager.UserManagerAutoConfigurationcom.appjars.AppJarsAutoConfiguration
If the application already contains JPA entities, add an @EntityScan annotation that includes both the application's entity packages and the User Manager package class:
@EntityScan(basePackageClasses = {UserManagerModule.class, YourEntity.class})
The dataSourceScriptDatabaseInitializer bean can be removed from the application if present, as User Manager handles database initialisation.
Then inject the RouteConfigurer provided by User Manager and configure the router layout in a @PostConstruct method so that the appjar views share the same layout as the rest of the application:
@Autowired
@Qualifier("UserManagerRouteConfigurer")
RouteConfigurer routeConfigurer;
@PostConstruct
public void configure() {
routeConfigurer.setViewsRouterLayout(MainLayout.class);
}
The import for RouteConfigurer is com.appjars.usermanager.flow.util.RouteConfigurer.
Configure Spring Security
Modify the SecurityConfiguration class to integrate with User Manager. User Manager provides its own PasswordEncoder, UserDetailsService, and a RememberMeServicesProvider bean, so those no longer need to be declared in the application.
Replace the contents of SecurityConfiguration with the following:
@EnableWebSecurity
@Configuration
public class SecurityConfiguration extends VaadinWebSecurity {
@Value("${com.appjars.usermanager.encoding.secret.key:1234567890}")
private String secretKey;
private final RememberMeServicesProvider rememberMeServicesProvider;
@Autowired
public SecurityConfiguration(RememberMeServicesProvider rememberMeServicesProvider) {
this.rememberMeServicesProvider = rememberMeServicesProvider;
}
@Bean
static NavigationAccessControlConfigurer navigationAccessControlConfigurer() {
return new NavigationAccessControlConfigurer()
.withAvailableNavigationAccessCheckers(
checker -> checker instanceof UserManagerNavigationAccessChecker);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize
.requestMatchers(new AntPathRequestMatcher("/images/*.png")).permitAll());
http.authorizeHttpRequests(authorize -> authorize
.requestMatchers(new AntPathRequestMatcher("/line-awesome/**/*.svg")).permitAll());
http.formLogin(formLogin -> formLogin.failureHandler(authenticationFailureHandler()));
http.rememberMe(rememberMe -> rememberMe
.rememberMeServices(rememberMeServicesProvider.getRememberMeServices())
.tokenValiditySeconds(7200));
super.configure(http);
setLoginView(http, LoginView.class, "/");
}
public AuthenticationFailureHandler authenticationFailureHandler() {
Map<String, String> exceptionMappings = new HashMap<>();
exceptionMappings.put(DisabledException.class.getCanonicalName(), "/login?error=disabled");
exceptionMappings.put(BadCredentialsException.class.getCanonicalName(), "/login?error=badcredentials");
ExceptionMappingAuthenticationFailureHandler result =
new ExceptionMappingAuthenticationFailureHandler();
result.setExceptionMappings(exceptionMappings);
result.setDefaultFailureUrl("/login?error");
return result;
}
}
Key points about this configuration:
- NavigationAccessControlConfigurer: Required for User Manager's runtime access rules to take effect. This bean registers
UserManagerNavigationAccessChecker, which evaluates the rules configured at runtime against incoming navigation requests. - RememberMeServicesProvider: Provided by User Manager. It wires together the
UserDetailsServiceandPersistentLoginServiceneeded for persistent login support. - authenticationFailureHandler: Maps specific Spring Security exceptions to login error URLs so the login view can display appropriate messages for disabled accounts and bad credentials.
Use User Manager Services and Views
User Manager provides its own user model and services. Update the application to use them instead of the generated ones.
Replace the AuthenticatedUser class with the following implementation that uses UserService and UserDto:
@Component
public class AuthenticatedUser {
private final UserService userService;
private final AuthenticationContext authenticationContext;
public AuthenticatedUser(AuthenticationContext authenticationContext, UserService userService) {
this.userService = userService;
this.authenticationContext = authenticationContext;
}
@Transactional
public Optional<UserDto> get() {
return authenticationContext.getAuthenticatedUser(UserDetails.class)
.map(userDetails -> userService.findByUsername(userDetails.getUsername()))
.orElse(Optional.empty());
}
public void logout() {
authenticationContext.logout();
}
}
The imports are:
com.appjars.usermanager.model.UserDtocom.appjars.usermanager.service.UserService
Because authenticatedUser.get() now returns Optional<UserDto>, update the createFooter() method in MainLayout accordingly:
private Footer createFooter() {
Footer layout = new Footer();
Optional<UserDto> maybeUser = authenticatedUser.get();
if (maybeUser.isPresent()) {
UserDto user = maybeUser.get();
Avatar avatar = new Avatar(user.getUsername());
avatar.setThemeName("xsmall");
avatar.getElement().setAttribute("tabindex", "-1");
MenuBar userMenu = new MenuBar();
userMenu.setThemeName("tertiary-inline contrast");
MenuItem userName = userMenu.addItem("");
Div div = new Div();
div.add(avatar);
div.add(user.getUsername());
div.add(new Icon("lumo", "dropdown"));
div.getElement().getStyle().set("display", "flex");
div.getElement().getStyle().set("align-items", "center");
div.getElement().getStyle().set("gap", "var(--lumo-space-s)");
userName.add(div);
userName.getSubMenu().addItem("Sign out", e -> authenticatedUser.logout());
layout.add(userMenu);
} else {
Anchor loginLink = new Anchor("login", "Sign in");
layout.add(loginLink);
}
return layout;
}
Add the User Manager views to the navigation menu. Insert the following snippet at the end of createNavigation() in MainLayout, before returning nav:
if (accessChecker.hasAccess(UsersListView.class)) {
SideNavItem navSec = new SideNavItem("Security");
navSec.setPrefixComponent(LineAwesomeIcon.SHIELD_ALT_SOLID.create());
if (accessChecker.hasAccess(UsersListView.class)) {
navSec.addItem(new SideNavItem("Users", UsersListView.class, LineAwesomeIcon.USER.create()));
}
if (accessChecker.hasAccess(AuthoritiesView.class)) {
navSec.addItem(new SideNavItem("Roles", AuthoritiesView.class, LineAwesomeIcon.THEATER_MASKS_SOLID.create()));
}
if (accessChecker.hasAccess(GroupsListView.class)) {
navSec.addItem(new SideNavItem("Groups", GroupsListView.class, LineAwesomeIcon.USERS_SOLID.create()));
}
if (accessChecker.hasAccess(RulesView.class)) {
navSec.addItem(new SideNavItem("Rules", RulesView.class, LineAwesomeIcon.BALANCE_SCALE_SOLID.create()));
}
if (accessChecker.hasAccess(ViewsView.class)) {
navSec.addItem(new SideNavItem("Views", ViewsView.class, LineAwesomeIcon.EYE_SOLID.create()));
}
if (accessChecker.hasAccess(ProfileView.class)) {
navSec.addItem(new SideNavItem("My Profile", ProfileView.class, LineAwesomeIcon.USER_CIRCLE.create()));
}
nav.addItem(navSec);
}
The imports for the view classes are:
com.appjars.usermanager.flow.view.UsersListViewcom.appjars.usermanager.flow.view.AuthoritiesViewcom.appjars.usermanager.flow.view.GroupsListViewcom.appjars.usermanager.flow.view.RulesViewcom.appjars.usermanager.flow.view.ViewsViewcom.appjars.usermanager.flow.view.ProfileView
Finally, simplify the LoginView so it extends the view provided by User Manager:
@AnonymousAllowed
@Route("login")
public class LoginView extends UserManagerLoginView {}
The import for UserManagerLoginView is com.appjars.usermanager.flow.view.UserManagerLoginView.
Remove Unneeded Classes
The following classes generated by the starter can be removed, as User Manager provides equivalents:
com.example.application.data.entity.Usercom.example.application.security.UserDetailsServiceImplcom.example.application.data.service.UserRepositorycom.example.application.data.service.UserServicecom.example.application.data.Role
After removing them, resolve any resulting import errors in the classes that referenced them.
The starter also generates an initial SQL script at src/main/resources/data.sql. This file can be removed as User Manager handles the creation and initial population of the database tables.
Configure Application Properties
Add the following properties to application.properties:
spring.jpa.hibernate.ddl-auto=update
spring.jpa.generate-ddl=true
spring.aop.proxy-target-class=false
vaadin.i18n.provider=com.appjars.utils.i18n.AppjarsI18nProvider
The spring.aop.proxy-target-class=false property is required for the appjar to function correctly.
Finally, add com.flowingcode and com.appjars to the list of whitelisted packages:
vaadin.allowed-packages = com.vaadin,org.vaadin,dev.hilla,com.example.application,com.flowingcode,com.appjars
Testing the Application
Start the application by running the main Spring Boot class. User Manager creates and populates the database tables on first run. After the application starts, log in using the username admin and password admin.
The Security menu group will be visible in the navigation, providing access to user, role, group, rule, and view management. Navigate to My Profile to verify that the authenticated user's details are displayed correctly.
Optional: Enabling External Authentication Providers
External authentication providers (Google, GitHub, or any provider written against the User Manager SPI) are optional. The steps below are only required when the application needs to offer OAuth2 or OpenID Connect sign-in alongside, or instead of, the regular username and password form.
Add the OAuth2 Client Dependency
External providers are wired through Spring Security's OAuth2 client. Add the starter to the application's POM:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
When this dependency is not present, all external-provider beans declared by User Manager remain dormant and the rest of the appjar continues to function with regular authentication only.
Wire the External Auth Configurer
User Manager exposes an ExternalAuthSecurityConfigurer bean that registers the OAuth2 login filters, the database-backed client registration repository, and the success/failure handlers required by the appjar. The bean is autowired as an optional dependency, so the security configuration compiles and runs whether or not the OAuth2 client dependency is present.
Update SecurityConfiguration to call the configurer before super.configure(http):
@Autowired(required = false)
private ExternalAuthSecurityConfigurer externalAuthSecurityConfigurer;
@Override
protected void configure(HttpSecurity http) throws Exception {
// ... existing http.authorizeHttpRequests / http.formLogin / http.rememberMe calls ...
if (externalAuthSecurityConfigurer != null) {
externalAuthSecurityConfigurer.configure(http);
}
super.configure(http);
setLoginView(http, LoginView.class, "/");
}
The import is com.appjars.usermanager.flow.security.ExternalAuthSecurityConfigurer.
Set the External URL
OAuth2 providers require the application's redirect URI to be registered in their developer console. The redirect URI shown in the administration view is built from the com.appjars.usermanager.external-url property:
com.appjars.usermanager.external-url=https://app.example.com
For local development the default http://localhost:8080 is sufficient. In production this must be the public-facing base URL of the application; the value is also used to build the registration-link URLs sent over email.
Configure the Client Secret Encryption Key
Each provider's client secret is stored encrypted in the database. User Manager encrypts and decrypts these values with an AES-256 key supplied through the com.appjars.usermanager.external-auth.secret.key property:
com.appjars.usermanager.external-auth.secret.key=<base64-encoded 32-byte key>
The value must be a base64-encoded 32-byte key — that is, exactly 44 base64 characters that decode to 32 raw bytes. An arbitrary passphrase will not work; it must decode to precisely 32 bytes. Generate a suitable key with:
openssl rand -base64 32
The property has no default. It is only required when external authentication is used and is validated the first time a client secret is encrypted or decrypted, so an application that does not use external providers can start without it. When the key is missing or blank, registering a provider fails with an error indicating the property is not configured.
Warning
Keep this key stable and secret. Changing it after providers have been saved makes the existing encrypted client secrets undecryptable, and those providers must be re-entered.
Configure Providers at Runtime
With the configurer wired and the application restarted, administrators can register one or more providers through the Authentication Providers view under the Security menu group. The view accepts the client ID and client secret obtained from each provider's developer console, along with the OAuth scopes and an optional default group for users created by the provider. See Authentication Providers in the user guide for the full administration flow.
Optional: Disable Regular Authentication
If the application should rely exclusively on external providers, the Regular entry in the Authentication Providers view can be disabled. User Manager automatically hides the username and password fields on the login and registration pages, and gates registration links, password reset links, and the change password view behind a friendly notification.
Warning
At least one provider must remain enabled at all times. The administration view refuses to disable or delete the last enabled provider.