Skip to content

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        : ""
Hold "Alt" / "Option" to enable pan & zoom

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
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 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
Hold "Alt" / "Option" to enable pan & zoom

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 new UserDto is created with no password and assigned to the provider's default group.
  • allowSignup = false: SignupNotAllowedException is 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:

  1. Open the Authentication Providers view and create a new provider of type KEYCLOAK, supplying the same client ID and secret used in application.properties. Pick a default group if desired.
  2. Sign out and reopen the login view. A Single Sign-On button appears below the regular login fields.
  3. 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 UserDto carries the e-mail address as username and belongs to the configured default group.
  4. 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