Developer Guide
This section covers the data model, configuration reference, required integration points, and extension points available to developers integrating User Manager into an application.
Data Model
User Manager persists eight core entities and six join tables. A user holds credentials and references to granted authorities and groups. A group is a named collection of authorities. An authority represents a Spring Security GrantedAuthority (role). An access rule defines a route-matching policy that controls which authorities are required or excluded. A view registers a Vaadin route path subject to access rule evaluation. A registration link is a time-limited token used for user registration, password reset, or authority and group invitation. A persistent login stores the Remember Me token for a user session. An auth provider stores the configuration of every authentication mechanism available to the application: the regular username and password form as well as any number of external OAuth2 or OpenID Connect providers.
---
config:
layout: elk
---
erDiagram
um_user {
integer id PK
varchar username
varchar password
boolean enabled
timestamp last_login
}
um_group {
integer id PK
varchar name
}
um_authority {
integer id PK
varchar name
}
um_rule {
integer id PK
integer priority
varchar type
varchar path_regex
varchar description
boolean consider_parameters
}
um_view {
integer id PK
varchar view_path
}
um_persistent_login {
integer id PK
varchar series
varchar token
timestamp last_used
integer user_id FK
}
um_auth_provider {
integer id PK
varchar type
varchar display_name
boolean enabled
boolean allow_signup
varchar client_id
varchar client_secret
varchar scopes
integer default_group_id FK
}
um_link {
integer id PK
varchar code
timestamp expiration
boolean completed
varchar link_type
integer user_id FK
integer group_id FK
integer authority_id FK
}
um_user_authority {
integer user_id FK
integer authority_id FK
}
um_user_group {
integer user_id FK
integer group_id FK
}
um_group_authority {
integer group_id FK
integer authority_id FK
}
um_req_auth {
integer rule_id FK
integer authority_id FK
}
um_excl_auth {
integer rule_id FK
integer authority_id FK
}
um_rule_view {
integer rule_id FK
integer view_id FK
}
um_user ||--o{ um_persistent_login : "has"
um_user ||--o{ um_link : "registration / password reset"
um_group ||--o{ um_link : "group invite"
um_authority ||--o{ um_link : "authority invite"
um_group ||--o{ um_auth_provider : "default group for"
um_user ||--o{ um_user_authority : ""
um_authority ||--o{ um_user_authority : ""
um_user ||--o{ um_user_group : ""
um_group ||--o{ um_user_group : ""
um_group ||--o{ um_group_authority : ""
um_authority ||--o{ um_group_authority : ""
um_rule ||--o{ um_req_auth : "requires"
um_authority ||--o{ um_req_auth : ""
um_rule ||--o{ um_excl_auth : "disallows"
um_authority ||--o{ um_excl_auth : ""
um_rule ||--o{ um_rule_view : "protects"
um_view ||--o{ um_rule_view : ""
Database Schema
User Manager manages fourteen tables. The schema below represents the DDL generated by Hibernate for a standard relational database.
CREATE SEQUENCE um_user_seq
START WITH 1 INCREMENT BY 50;
CREATE SEQUENCE um_group_seq
START WITH 1 INCREMENT BY 50;
CREATE SEQUENCE um_authority_seq
START WITH 1 INCREMENT BY 50;
CREATE SEQUENCE um_rule_seq
START WITH 1 INCREMENT BY 50;
CREATE SEQUENCE um_view_seq
START WITH 1 INCREMENT BY 50;
CREATE SEQUENCE um_link_seq
START WITH 1 INCREMENT BY 50;
CREATE SEQUENCE um_persistent_login_seq
START WITH 1 INCREMENT BY 50;
CREATE SEQUENCE um_auth_provider_seq
START WITH 1 INCREMENT BY 50;
CREATE TABLE um_authority (
id INTEGER NOT NULL,
name VARCHAR(50) NOT NULL,
CONSTRAINT pk_um_authority PRIMARY KEY (id),
CONSTRAINT uq_um_authority_name UNIQUE (name)
);
CREATE TABLE um_group (
id INTEGER NOT NULL,
name VARCHAR(50) NOT NULL,
CONSTRAINT pk_um_group PRIMARY KEY (id),
CONSTRAINT uq_um_group_name UNIQUE (name)
);
CREATE TABLE um_user (
id INTEGER NOT NULL,
username VARCHAR(50) NOT NULL,
password VARCHAR(60),
enabled BOOLEAN NOT NULL,
last_login TIMESTAMP(6),
CONSTRAINT pk_um_user PRIMARY KEY (id),
CONSTRAINT uq_um_user_username UNIQUE (username)
);
CREATE TABLE um_view (
id INTEGER NOT NULL,
view_path VARCHAR(75) NOT NULL,
CONSTRAINT pk_um_view PRIMARY KEY (id),
CONSTRAINT uq_um_view_view_path UNIQUE (view_path)
);
CREATE TABLE um_rule (
id INTEGER NOT NULL,
priority INTEGER NOT NULL,
type VARCHAR(255),
path_regex VARCHAR(255),
description VARCHAR(255),
consider_parameters BOOLEAN,
CONSTRAINT pk_um_rule PRIMARY KEY (id)
);
CREATE TABLE um_link (
id INTEGER NOT NULL,
code VARCHAR(255) NOT NULL,
expiration TIMESTAMP(6) NOT NULL,
completed BOOLEAN NOT NULL,
link_type VARCHAR(31),
user_id INTEGER,
group_id INTEGER,
authority_id INTEGER,
CONSTRAINT pk_um_link PRIMARY KEY (id),
CONSTRAINT fk_um_link_user FOREIGN KEY (user_id) REFERENCES um_user (id),
CONSTRAINT fk_um_link_group FOREIGN KEY (group_id) REFERENCES um_group (id),
CONSTRAINT fk_um_link_authority FOREIGN KEY (authority_id) REFERENCES um_authority (id)
);
CREATE TABLE um_persistent_login (
id INTEGER NOT NULL,
series VARCHAR(64) NOT NULL,
token VARCHAR(64) NOT NULL,
last_used TIMESTAMP(6) NOT NULL,
user_id INTEGER NOT NULL,
CONSTRAINT pk_um_persistent_login PRIMARY KEY (id),
CONSTRAINT uq_um_persistent_login_series UNIQUE (series),
CONSTRAINT fk_um_persistent_login_user FOREIGN KEY (user_id) REFERENCES um_user (id)
);
CREATE TABLE um_user_authority (
user_id INTEGER NOT NULL,
authority_id INTEGER NOT NULL,
CONSTRAINT fk_um_user_authority_user FOREIGN KEY (user_id) REFERENCES um_user (id),
CONSTRAINT fk_um_user_authority_authority FOREIGN KEY (authority_id) REFERENCES um_authority (id)
);
CREATE TABLE um_user_group (
user_id INTEGER NOT NULL,
group_id INTEGER NOT NULL,
CONSTRAINT fk_um_user_group_user FOREIGN KEY (user_id) REFERENCES um_user (id),
CONSTRAINT fk_um_user_group_group FOREIGN KEY (group_id) REFERENCES um_group (id)
);
CREATE TABLE um_group_authority (
group_id INTEGER NOT NULL,
authority_id INTEGER NOT NULL,
CONSTRAINT fk_um_group_authority_group FOREIGN KEY (group_id) REFERENCES um_group (id),
CONSTRAINT fk_um_group_authority_authority FOREIGN KEY (authority_id) REFERENCES um_authority (id)
);
CREATE TABLE um_req_auth (
rule_id INTEGER NOT NULL,
authority_id INTEGER NOT NULL,
CONSTRAINT fk_um_req_auth_rule FOREIGN KEY (rule_id) REFERENCES um_rule (id),
CONSTRAINT fk_um_req_auth_authority FOREIGN KEY (authority_id) REFERENCES um_authority (id)
);
CREATE TABLE um_excl_auth (
rule_id INTEGER NOT NULL,
authority_id INTEGER NOT NULL,
CONSTRAINT fk_um_excl_auth_rule FOREIGN KEY (rule_id) REFERENCES um_rule (id),
CONSTRAINT fk_um_excl_auth_authority FOREIGN KEY (authority_id) REFERENCES um_authority (id)
);
CREATE TABLE um_rule_view (
rule_id INTEGER NOT NULL,
view_id INTEGER NOT NULL,
CONSTRAINT fk_um_rule_view_rule FOREIGN KEY (rule_id) REFERENCES um_rule (id),
CONSTRAINT fk_um_rule_view_view FOREIGN KEY (view_id) REFERENCES um_view (id)
);
CREATE TABLE um_auth_provider (
id INTEGER NOT NULL,
type VARCHAR(50) NOT NULL,
display_name VARCHAR(100),
enabled BOOLEAN NOT NULL,
allow_signup BOOLEAN NOT NULL DEFAULT TRUE,
client_id VARCHAR(255),
client_secret VARCHAR(500),
scopes VARCHAR(500),
default_group_id INTEGER,
CONSTRAINT pk_um_auth_provider PRIMARY KEY (id),
CONSTRAINT uq_um_auth_provider_type UNIQUE (type),
CONSTRAINT fk_um_auth_provider_group FOREIGN KEY (default_group_id) REFERENCES um_group (id)
);
um_authority, um_group, and um_view are created before um_user and um_rule because the join tables reference all of them. The password column holds a BCrypt hash and is always 60 characters. The link_type discriminator column in um_link stores one of four values: USER_REGISTRATION_LINK, PASSWORD_RESET_LINK, GROUP_LINK, or AUTH_LINK. The type column in um_rule stores the string representation of the RuleType enum. The type column in um_auth_provider stores the string representation of the AuthProviderType enum (REGULAR, GOOGLE, GITHUB, plus any additional values contributed by custom providers); client_secret is stored AES-encrypted, not in plain text.
Module Overview
User Manager is structured as six Maven modules following the AppJars layered architecture:
| Module | Artifact ID | Description |
|---|---|---|
| Model | appjars-user-manager-model |
DTOs, enums, UserManagerAutoConfiguration |
| Business API | appjars-user-manager-business |
UserService, GroupService, AuthorityService, AccessRuleService, ViewSecurityService, RegistrationLinkService, PersistentLoginService, PasswordService interfaces |
| Business Impl | appjars-user-manager-business-impl |
Service implementations, DefaultDataGenerator, RememberMeServicesProvider |
| Data API | appjars-user-manager-data |
DAO interfaces |
| Data Impl | appjars-user-manager-data-impl |
JPA entities, DAO implementations, entity-to-DTO converters |
| Flow UI | appjars-user-manager-flow |
Vaadin views, RouteConfigurer, UrlGenerator, dialogs and components |
A monolithic application includes the three implementation modules (-business-impl, -data-impl, -flow). The API modules (-business, -data) are pulled in transitively.
Spring Auto-Configuration
User Manager registers itself through Spring Boot's auto-configuration mechanism. The entry point is UserManagerAutoConfiguration, declared in:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
This class scans the com.appjars.usermanager package for all Spring components and JPA entities. On first startup, DefaultDataGenerator seeds the database with two default authorities (ANONYMOUS and ADMIN) and an initial admin user with a BCrypt-encoded admin password assigned the ADMIN authority.
PasswordServiceImpl registers a BCryptPasswordEncoder bean conditionally — if no PasswordEncoder bean is already present in the application context. RememberMeServicesProvider registers a PersistentTokenBasedRememberMeServices bean named "UserManagerRememberMeService" backed by PersistentLoginService.
Configuration Properties
Security
| Property | Default | Description |
|---|---|---|
com.appjars.usermanager.encoding.secret.key |
1234567890 |
Secret key used by the Remember Me token service |
Registration Links
| Property | Default | Description |
|---|---|---|
com.appjars.usermanager.default.days.expiration |
7 |
Default validity period in days for generated registration links |
com.appjars.usermanager.maximum.days.expiration |
99 |
Maximum validity period in days that can be set when generating a link |
com.appjars.usermanager.external-url |
http://localhost:8080/ |
Base URL prepended to registration link codes when generating full URLs for email delivery. Also used to build the OAuth2 redirect URI shown in the Authentication Providers view. Trailing slashes are stripped when building the redirect URI to prevent double-slash issues. |
External Authentication
| Property | Default | Description |
|---|---|---|
com.appjars.usermanager.external-auth.secret.key |
(none) | Base64-encoded AES-256 key used to encrypt and decrypt the OAuth2 client secrets stored in um_auth_provider.client_secret. The decoded value must be exactly 32 bytes (44 base64 characters). The property has no default; it is required only when external authentication is used, and is validated lazily on the first encryption or decryption rather than at startup. |
View URLs
| Property | Default | Description |
|---|---|---|
com.appjars.usermanager.url.users |
um/users |
URL path of the user list view |
com.appjars.usermanager.url.users-edit |
um/users/edit |
URL path of the user edit view |
com.appjars.usermanager.url.users-create |
um/users/create |
URL path of the user creation view |
com.appjars.usermanager.url.user-registration-link |
um/users/register |
URL path of the self-registration view (public) |
com.appjars.usermanager.url.password-change-link |
um/users/password-reset |
URL path of the password reset view (public) |
com.appjars.usermanager.url.groups |
um/groups |
URL path of the group list view |
com.appjars.usermanager.url.groups-edit |
um/groups/edit |
URL path of the group edit view |
com.appjars.usermanager.url.groups-create |
um/groups/create |
URL path of the group creation view |
com.appjars.usermanager.url.roles |
um/roles |
URL path of the authority (role) management view |
com.appjars.usermanager.url.profile |
um/profile |
URL path of the user profile view |
com.appjars.usermanager.url.access-rules |
um/access-rules |
URL path of the access rules management view |
com.appjars.usermanager.url.views |
um/views |
URL path of the view security management view |
com.appjars.usermanager.url.auth-providers |
um/auth-providers |
URL path of the authentication providers management view |
Authority and Group Hierarchy
User Manager models permissions through a two-level hierarchy. An authority is the atomic unit of access control and maps directly to a Spring Security GrantedAuthority. A group holds a set of authorities and acts as a reusable role bundle. A user accumulates their effective authority set from two sources: directly assigned authorities and authorities inherited from every group the user belongs to.
When evaluating access, UserEntity.getAllAuthorities() merges both sources into a single set. This means granting a user membership in a group is equivalent to granting them all of that group's authorities individually, with the advantage that changing the group's authority set propagates to all members automatically.
Registration Links
Registration links provide a token-based mechanism for out-of-band actions that cannot be performed through an authenticated session: creating a new user account, resetting a forgotten password, joining a group, and being granted an authority. All link types share a base table (um_link) via single-table inheritance, distinguished by the link_type discriminator column.
Each link carries an expiration timestamp and a completed flag. Consuming a link — completing registration or resetting a password — sets completed to true, preventing reuse. Expired or already-completed links are rejected silently.
The RegistrationLinkService.getDefaultDaysToExpiration() and getMaximumDaysToExpiration() methods expose the configured expiration bounds so UI code can offer a date picker constrained to the allowed range.
Access Rules
Access rules control which authenticated roles may navigate to which routes. Each rule carries a priority integer that determines evaluation order: lower values are evaluated first, and the first matching rule wins. Rules match routes using one of five strategies:
| Rule type | Matching behaviour |
|---|---|
SIMPLE |
Exact string equality |
REGEX |
Full regex pattern match |
CONTAINS |
Route contains the pattern as a substring |
STARTS_WITH |
Route starts with the pattern |
ENDS_WITH |
Route ends with the pattern |
A rule defines two authority sets: necessaryAuthorities (the user must hold all of them) and disalowedAuthorities (the user must hold none of them). Both sets are evaluated simultaneously. A rule can optionally extend its matching to include route parameters and query parameters by setting considerParameters to true.
AccessRuleService.checkAccess() accepts the full route context and the user's authority set and returns a boolean access decision. ViewSecurityService.calculateAllowedViewsByAccessRules() filters a list of registered view paths down to those accessible by the given authority set.
Relationship to @RolesAllowed
Access rules work alongside the @RolesAllowed annotation:
@RolesAllowed: Defined at development time in the application code; requires code changes and redeployment to modify.- Access rules: Defined at runtime through the administrator interface; can be changed and take effect immediately without code changes.
Access rules provide runtime flexibility, while @RolesAllowed provides code-level default access control. Requirements declared with @RolesAllowed are surfaced in the administrator-facing Views page as annotation-based roles that cannot be modified through the interface.
Spring Security Integration
UserServiceImpl implements Spring Security's UserDetailsService, making User Manager a direct drop-in for Spring Security's authentication provider. The loadUserByUsername() method loads the user, merges direct and group-inherited authorities into GrantedAuthority instances, and returns a UserDetails object. A disabled user (enabled = false) causes DisabledException to be thrown during authentication.
PersistentLoginService implements PersistentTokenRepository, the Spring Security contract for database-backed Remember Me tokens. Inject the provided RememberMeServicesProvider bean into the application's Spring Security configuration:
@Autowired
@Qualifier("UserManagerRememberMeService")
private AbstractRememberMeServices rememberMeServices;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.rememberMe(rememberMe -> rememberMe
.rememberMeServices(rememberMeServices));
return http.build();
}
External Authentication Architecture
The external-authentication subsystem is gated on the presence of spring-boot-starter-oauth2-client on the classpath. Every bean involved in the flow is annotated with @ConditionalOnClass(OAuth2LoginConfigurer.class), so an application built without that dependency continues to function unchanged with regular authentication only.
Components
| Component | Purpose |
|---|---|
AuthProviderEntity / AuthProviderDto / AuthProviderService |
Persistence and CRUD for the um_auth_provider table. Includes the findByType(AuthProviderType) lookup and the isRegularAuthAvailable() predicate used to gate features. |
SecretEncryptionHelper |
AES/GCM helper used by AuthProviderServiceImpl to encrypt the client secret before save and decrypt it on load (a fresh random IV is generated per encryption). The key is read from com.appjars.usermanager.external-auth.secret.key and must be a base64-encoded 32-byte (AES-256) value. |
ExternalAuthProvider |
SPI implemented by every provider (Google, GitHub, custom). See Adding a custom authentication provider. |
AuthProviderRegistry |
Spring-managed registry that discovers all ExternalAuthProvider beans at startup and exposes lookup helpers (findById, findByIdOptional, listRegisteredTypes). |
DatabaseClientRegistrationRepository |
Spring Security ClientRegistrationRepository backed by the database. Builds ClientRegistration objects on the fly from each enabled non-REGULAR provider row, combined with the static metadata from its ExternalAuthProvider SPI. |
WrappingDaoAuthenticationProvider |
Wraps the default DaoAuthenticationProvider and refuses to authenticate when the REGULAR provider is disabled. Ensures the form-login path honours the runtime configuration. |
ExternalAuthStartController |
GET /um/external-auth-start/{registrationId} endpoint. Sets short-lived cookies for the remember-me preference and the optional registration-link code, then redirects to /oauth2/authorization/{registrationId}. Robust against Spring Security's session-fixation defenses, which would otherwise wipe the corresponding session attributes. |
OAuth2AuthenticationSuccessHandler |
Runs after a successful OAuth callback. Resolves the authenticated user against the database (consuming the registration link cookie on identity match), enforces the per-provider allowSignup flag, and issues the persistent-login token when the remember-me cookie is present. |
ExternalAuthSecurityConfigurer |
The integration point host applications call from their HttpSecurity configuration. Wires the OAuth2 login filter, the failure handler that logs the OAuth2 error code, and the permitAll rules for the redirect endpoints. |
Login flow
sequenceDiagram
autonumber
participant User as Browser
participant UM as User Manager (login view)
participant Start as ExternalAuthStartController
participant OAuth as Spring OAuth2 client
participant Provider as External provider
participant Success as OAuth2AuthenticationSuccessHandler
participant DB as Database
User->>UM: Open /login
UM-->>User: Render form + provider buttons
User->>Start: GET /um/external-auth-start/GOOGLE?rememberMe=1
Start-->>User: Set EXTERNAL_AUTH_REMEMBER_ME cookie<br/>302 → /oauth2/authorization/GOOGLE
User->>OAuth: GET /oauth2/authorization/GOOGLE
OAuth->>Provider: Authorize request
Provider-->>User: Consent page
User->>Provider: Approve
Provider->>OAuth: Authorization code
OAuth->>Provider: Exchange code for tokens
OAuth->>Provider: Fetch userinfo
OAuth->>Success: onAuthenticationSuccess
Success->>DB: Lookup / create user
Success->>DB: Mark registration link SUCCESS (if cookie present)
Success-->>User: 302 → original destination
The same handler also reads the registration-link cookie set by the registration view. When the cookie carries an active UserRegistrationLinkDto whose username matches the OAuth-extracted identity (case-insensitive), the link is marked as completed via RegistrationLinkService.completeUserRegistrationViaExternalAuth and the pre-provisioned user is authenticated. Identity mismatches leave the link active so that the intended invitee can still consume it.
allowSignup evaluation
When the user does not exist in the database after the OAuth callback, the success handler checks the provider's allowSignup flag:
allowSignup = true: a newUserDtois created with no password and assigned to the provider's default group.allowSignup = false:SignupNotAllowedExceptionis thrown and the user is redirected to/login?error=signup_disabled, which the login view recognises and renders as a dedicated error message.
The REGULAR provider always allows signup; the flag is meaningful only for external types.
Adding a Custom Authentication Provider
User Manager discovers external providers through the ExternalAuthProvider SPI. A custom provider is a Spring bean that implements this interface, declares the @ConditionalOnClass(OAuth2LoginConfigurer.class) annotation so the host application keeps compiling without OAuth2, and supplies the static metadata that DatabaseClientRegistrationRepository needs to build a ClientRegistration.
The SPI
public interface ExternalAuthProvider {
// Identifier; also the value stored in um_auth_provider.type
String getId();
// Human-readable label pre-filled in the create dialog
String getDisplayName();
// OAuth2 / OIDC endpoint URLs
String getAuthorizationUri();
String getTokenUri();
String getUserInfoUri();
// Default scopes used when the administrator leaves the scopes field blank
Set<String> getDefaultScopes();
// Extract the local username from the provider's userinfo response. Must match the
// username stored in um_user.username for pre-provisioned users to be recognised.
String extractUsername(OAuth2User oauth2User);
// Optional: the attribute used by Spring as the principal name. Defaults to "sub".
default String getUserNameAttributeName() { return "sub"; }
// Optional: JWK set URI for OIDC providers that issue id_tokens. Null for plain OAuth2.
default String getJwkSetUri() { return null; }
// Optional: button colour and icon, used by the login view to render branded buttons.
default String getButtonColor() { return null; }
default AbstractStreamResource getIcon() { return null; }
}
Minimal example
@Component
@ConditionalOnClass(OAuth2LoginConfigurer.class)
public class GitLabAuthProvider implements ExternalAuthProvider {
@Override
public String getId() { return "GITLAB"; }
@Override
public String getDisplayName() { return "GitLab"; }
@Override
public String getAuthorizationUri() { return "https://gitlab.com/oauth/authorize"; }
@Override
public String getTokenUri() { return "https://gitlab.com/oauth/token"; }
@Override
public String getUserInfoUri() { return "https://gitlab.com/oauth/userinfo"; }
@Override
public Set<String> getDefaultScopes() {
return Set.of("openid", "email", "profile");
}
@Override
public String extractUsername(OAuth2User oauth2User) {
return oauth2User.getAttribute("email");
}
@Override
public String getJwkSetUri() {
return "https://gitlab.com/oauth/discovery/keys";
}
}
Registering the new type
AuthProviderType is an enum, so a new external type must also be added to that enum for the administration view to offer it in the Type dropdown and for findByType to resolve it. The enum lives in the model module:
public enum AuthProviderType {
REGULAR,
GOOGLE,
GITHUB,
GITLAB
}
After adding the enum value and the SPI bean, restart the application. The new type appears in the Type dropdown of the create dialog, and administrators can register an instance with their own client ID and client secret.
Choosing the username attribute
Most OIDC providers issue an id_token whose subject is a stable opaque identifier, suitable for the userNameAttributeName but rarely meaningful as a local username. Returning the e-mail address from extractUsername is the standard pattern because it allows administrators to pre-provision users with the same identifier the provider will return. If the provider does not return an e-mail, extractUsername may fall back to another attribute (as GitHubAuthProvider does, using login when email is null), but the consequence is that pre-provisioning requires the administrator to know the provider-specific handle.
Integrating Vaadin SSO Kit
Vaadin SSO Kit is a commercial extension that adds Single Sign-On against Keycloak and other OpenID Connect providers to a Vaadin application. The kit and User Manager solve overlapping problems but at different layers: SSO Kit handles the federation and token exchange with the identity provider, while User Manager owns the local user model, the role and group assignments, and the runtime access rules. Integrating both makes sense when an organisation already runs an SSO infrastructure and wants User Manager's role-based access rules to apply on top of those identities.
The integration is implemented by writing a custom ExternalAuthProvider for each SSO Kit-provided identity provider, so it appears as just another row in the Authentication Providers view. SSO Kit's own login button can be hidden and the User Manager login view used as the single entry point.
Add the SSO Kit dependency
Follow the Vaadin SSO Kit documentation to add the dependency and obtain a commercial licence:
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>sso-kit-starter</artifactId>
</dependency>
The kit pulls in spring-boot-starter-oauth2-client transitively, which is exactly what User Manager's external-authentication subsystem needs.
Configure the identity provider in application.properties
SSO Kit reads its provider configuration from Spring Security's standard properties:
spring.security.oauth2.client.registration.keycloak.client-id=appjars-portal
spring.security.oauth2.client.registration.keycloak.client-secret=<secret>
spring.security.oauth2.client.registration.keycloak.scope=openid,profile,email
spring.security.oauth2.client.provider.keycloak.issuer-uri=https://sso.example.com/realms/main
This configuration is read by SSO Kit on startup. User Manager's DatabaseClientRegistrationRepository runs in parallel and serves the registrations stored in um_auth_provider. When both are present, Spring resolves a request by registration ID — so SSO Kit registrations and User Manager registrations coexist without collision, as long as the IDs differ.
Implement the ExternalAuthProvider
Even though SSO Kit configures the OAuth2 client itself, registering a matching ExternalAuthProvider is what makes the kit's identity provider appear on User Manager's login page and what wires User Manager's success handler into the callback. The example below covers Keycloak; the same pattern applies to any other provider supported by SSO Kit (Okta, Azure AD, Ping, and so on):
@Component
@ConditionalOnClass(OAuth2LoginConfigurer.class)
public class KeycloakAuthProvider implements ExternalAuthProvider {
@Value("${spring.security.oauth2.client.provider.keycloak.issuer-uri}")
private String issuerUri;
@Override
public String getId() { return "KEYCLOAK"; }
@Override
public String getDisplayName() { return "Single Sign-On"; }
@Override
public String getAuthorizationUri() {
return issuerUri + "/protocol/openid-connect/auth";
}
@Override
public String getTokenUri() {
return issuerUri + "/protocol/openid-connect/token";
}
@Override
public String getUserInfoUri() {
return issuerUri + "/protocol/openid-connect/userinfo";
}
@Override
public String getJwkSetUri() {
return issuerUri + "/protocol/openid-connect/certs";
}
@Override
public Set<String> getDefaultScopes() {
return Set.of("openid", "profile", "email");
}
@Override
public String extractUsername(OAuth2User oauth2User) {
// Keycloak puts the user's e-mail in the `email` claim when the scope is requested.
// Falling back to `preferred_username` covers realms configured without an e-mail.
String email = oauth2User.getAttribute("email");
return email != null ? email : oauth2User.getAttribute("preferred_username");
}
}
Add a corresponding KEYCLOAK value to AuthProviderType so administrators can register it from the Authentication Providers view.
Disable SSO Kit's own login UI
By default, SSO Kit installs its own login page and redirects unauthenticated requests to it. With User Manager owning the login view, SSO Kit's UI should be suppressed so users see one consistent screen:
vaadin.sso.auto-login.enabled=false
vaadin.sso.login-route=login
The first property prevents SSO Kit from intercepting anonymous navigation; the second points the kit at User Manager's login route, so the few internal redirects the kit emits land on the right view.
Reuse SSO Kit's user information
When the user signs in through Keycloak, SSO Kit's AuthenticationContext exposes the OIDC user details (e-mail, full name, roles claim, raw id_token). User Manager's OAuth2AuthenticationSuccessHandler only stores the username — anything else that the application wants to keep (display name, photo, preferred locale) is best persisted through the User Profile AppJar, which subscribes to the same Authentication and can copy the desired claims on first login.
For role mapping, SSO Kit returns the realm and client roles in the OIDC claims, but those values do not automatically become Spring GrantedAuthority instances on the User Manager side. The simplest approach is to leave User Manager's role and group assignments under administrator control (managed through the regular Roles and Groups views), and to use the Default Group field of the Authentication Providers view to assign a baseline group to every Keycloak-authenticated user. Custom claim-to-role mapping can be implemented by overriding OAuth2AuthenticationSuccessHandler and re-publishing it with @Primary.
Verify the flow
After restarting the application:
- Open the Authentication Providers view and create a new provider of type
KEYCLOAK, supplying the same client ID and secret used inapplication.properties. Pick a default group if desired. - Sign out and reopen the login view. A Single Sign-On button appears below the regular login fields.
- Click the button. The browser is redirected to Keycloak's login page; after authenticating, the user is returned to the application and signed in. The created
UserDtocarries the e-mail address as username and belongs to the configured default group. - Open the user in the Users management view to assign additional roles and groups as needed.
Service API
UserService
// Find a user by their username
Optional<UserDto> findByUsername(String username);
// Toggle the enabled state of a user
void switchEnableUser(UserDto user);
// Validate a raw password against a user's stored hash
boolean validatePassword(String username, String password);
// Update the stored password hash for a user
void updatePasswordByUser(String username, String newPassword);
// Return the total number of users
long countUsers();
GroupService
// Return the number of users in each group
Map<GroupDto, Integer> getUserCountPerGroup();
// Return the number of authorities in each group
Map<GroupDto, Integer> getAuthsCountPerGroup();
// Find a group by its name
Optional<GroupDto> findByName(String groupName);
// Save a group along with its user membership set
Integer save(GroupDto group, Set<UserDto> groupUsers);
AuthorityService
// Return the number of users directly assigned to each authority
Map<AuthorityDto, Integer> getUserCountPerAuthority();
// Return the number of groups assigned to each authority
Map<AuthorityDto, Integer> getGroupCountPerAuthority();
// Find an authority by its name
Optional<AuthorityDto> findByName(String authorityName);
// Return all authorities except ANONYMOUS
List<AuthorityDto> findAllWithoutAnonymous();
// Return the ANONYMOUS authority
Optional<AuthorityDto> findAnonymous();
AccessRuleService
// Move a rule to a higher priority position (lower priority value) than another
void moveAbove(AccessRuleDto aimedItem, AccessRuleDto movingItem);
// Move a rule to a lower priority position than another
void moveBelow(AccessRuleDto aimedItem, AccessRuleDto movingItem);
// Return the highest priority value currently assigned
int getLastPriority();
// Evaluate access for a route context against a set of user authorities
boolean checkAccess(RouteDataValueDto routeDataValues,
RouteParametersValueDto routeParametersValues,
QueryParametersValueDto queryParametersValues,
Set<AuthorityDto> authorities);
ViewSecurityService
// Return all view paths accessible by the given authority set
List<ViewSecurityDto> calculateAllowedViewsByAccessRules(Set<AuthorityDto> authorities);
// Find a registered view by its path
Optional<ViewSecurityDto> findByViewPath(String viewPath);
RegistrationLinkService
// Return the most recently created link of any type for a user
Optional<RegistrationLinkDto> getLatestLinkByUser(UserDto user);
// Return the most recent user registration link for a user
Optional<UserRegistrationLinkDto> getLatestRegistrationLinkByUser(UserDto user);
// Return the most recent password reset link for a user
Optional<PasswordChangeDto> getLatestPasswordChangeByUser(UserDto user);
// Return the most recent group invitation link for a group
Optional<GroupRegistrationLinkDto> getLatestLinkByGroup(GroupDto group);
// Return the most recent authority invitation link for an authority
Optional<AuthorityRegistrationLinkDto> getLatestLinkByAuthority(AuthorityDto authority);
// Find a link by its code token
Optional<RegistrationLinkDto> findByCode(String code);
// Complete a user registration: save the username and encoded password, mark the link as used
void completeUserRegistration(String username, String encodedPassword, RegistrationLinkDto regLink);
// Complete a user registration through an external authentication provider, without storing a
// password. Marks the link as used and returns the linked user. Does not require regular
// authentication to be available.
UserDto completeUserRegistrationViaExternalAuth(UserRegistrationLinkDto link);
// Complete a password reset: update the stored hash, mark the link as used
void completePasswordChange(PasswordChangeDto link, String encodedPassword);
// Return the configured default expiration in days
int getDefaultDaysToExpiration();
// Return the configured maximum expiration in days
int getMaximumDaysToExpiration();
PasswordService
// Validate a password and return the result of each configured rule
HashMap<String, RuleResultDetailDto> getRulesValidations(String username, String password);
// Return localised hint text for each configured password rule
HashMap<String, TranslationMetadataDto> getRulesHints();
AuthProviderService
// Find a provider by its type (REGULAR, GOOGLE, ...)
Optional<AuthProviderDto> findByType(AuthProviderType type);
// Return all enabled providers
List<AuthProviderDto> findAllEnabled();
// Whether the REGULAR provider is enabled. Used to gate features that depend on
// username/password authentication (registration links, password reset, change password).
boolean isRegularAuthAvailable();
Password Validation Extension
The default password policy enforces: length between 6 and 30 characters, at least one uppercase letter, at least one lowercase letter, at least one digit, no whitespace, and the username must not appear in the password.
To apply a custom policy, provide an implementation of PasswordRulesProvider annotated with @Primary:
@Primary
@Component
public class AppPasswordRulesProvider implements PasswordRulesProvider {
@Override
public PasswordValidator getPasswordValidator() {
return new PasswordValidator(
new LengthRule(8, 64),
new UppercaseCharacterRule(1),
new DigitCharacterRule(2),
new SpecialCharacterRule(1)
);
}
}
PasswordRulesProvider uses the Passay library. Any PasswordValidator composed from Passay rules is accepted.
Customisation
Assigning a Router Layout
By default, User Manager registers its views without a parent layout. To wrap them in the application's main layout, inject the RouteConfigurer bean using its qualifier and call setViewsRouterLayout in a @PostConstruct method:
@Autowired
@Qualifier("UserManagerRouteConfigurer")
private RouteConfigurer routeConfigurer;
@PostConstruct
public void configure() {
routeConfigurer.setViewsRouterLayout(MainLayout.class);
}
The @Qualifier is required because multiple AppJars may contribute a bean named RouteConfigurer.
Customising View URLs
The default URL paths can be overridden in application.properties:
com.appjars.usermanager.url.users=myapp/users
com.appjars.usermanager.url.groups=myapp/groups
com.appjars.usermanager.url.roles=myapp/roles
com.appjars.usermanager.url.profile=myapp/profile
com.appjars.usermanager.url.access-rules=myapp/access-rules
com.appjars.usermanager.url.views=myapp/views