Pluggable authentication, authorization, and annotation-driven protection for
Vaadin Flow, lightweight REST, and plain-Java / desktop / CLI applications.
Uses Java SPI (ServiceLoader) for application-provided services.
The library is split into a framework-neutral core, three adapters (Vaadin, REST, standalone), one transport-level shared module, and four reference demos. Concrete roles and permissions live in applications or demo modules — never in the library.
| Module | Artifact | Description |
|---|---|---|
security-core |
security-core |
Generic, framework-neutral security concepts and decision logic |
security-vaadin |
security-vaadin |
Vaadin Flow adapter — view and navigation security |
security-rest |
security-rest |
Framework-light REST adapter — request and handler security |
security-standalone |
security-standalone |
Plain-Java / desktop / CLI adapter — ThreadLocal subject + dynamic-proxy method-level enforcement |
demo-rest-shared |
demo-rest-shared |
Transport-level constants + tiny JSON helper, shared between the REST server and any client |
demo-vaadin |
demo-vaadin |
Standalone Vaadin demo (WAR) — auth runs in-JVM |
demo-rest |
demo-rest |
Runnable REST reference: JDK-only HTTP server + CLI client |
demo-vaadin-rest-client |
demo-vaadin-rest-client |
Vaadin demo where demo-rest is the authoritative backend; UI talks to it through one encapsulated Java client |
demo-standalone |
demo-standalone |
Interactive CLI demo (library-borrowing) showing Secured.wrap(...) + StandaloneLoginFlow |
security-core -> (no project deps)
security-vaadin -> security-core
security-rest -> security-core
security-standalone -> security-core
demo-rest-shared -> (no project deps; transport-only)
demo-vaadin -> security-core, security-vaadin
demo-rest -> security-core, security-rest, demo-rest-shared
demo-vaadin-rest-client -> security-core, security-vaadin, demo-rest-shared
(test scope only: demo-rest)
demo-standalone -> security-core, security-standalone
security-core has no Vaadin, Servlet, or REST-framework dependencies.
The three adapter modules never depend on each other.
# Full build (requires Maven 3.9.9+, Java 26+)
mvn clean installmvn install is required at least once because the demos depend on
each other through the local ~/.m2 repository (see § Module
Structure — demo-vaadin-rest-client depends on demo-rest for tests,
and demo-rest-shared is consumed by both REST-side modules).
| You want to see … | Run |
|---|---|
| Vaadin role/permission UI in a single JVM, no backend | demo-vaadin |
| Pure REST security (HTTP server + interactive CLI), no UI | demo-rest |
| Vaadin UI talking to a separate REST backend (real two-tier setup) | demo-vaadin-rest-client |
| Plain-Java / CLI / desktop integration (no HTTP, no Vaadin) | mvn -pl demo-standalone exec:java -Dexec.mainClass=com.svenruppert.vaadin.security.demo.standalone.DemoApp |
cd demo-vaadin && mvn jetty:run
# Browser: http://localhost:8080/First run shows the bootstrap setup (the demo prints a token to the
console). After setup, log in as the chosen admin. Demo users
user/user and demo/demo are pre-populated; admin is created via
the bootstrap flow. Walkthrough: docs/demo-vaadin.md.
# Terminal 1 — JDK-only HTTP server on http://localhost:8080
mvn -pl :demo-rest exec:java
# Prints a bootstrap token to the console (TRANSIENT_CONSOLE mode).
# Terminal 2 — interactive CLI
mvn -pl :demo-rest exec:java \
-Dexec.mainClass=com.svenruppert.vaadin.security.demo.rest.cli.DemoRestCli
# Use `init-admin` to create the first admin via the bootstrap token.
# Then `login admin <new-password>` and play with `operations` / `call …`.Demo users: editor/editor, viewer/viewer. admin is created via
the bootstrap flow; with -Dsecurity.bootstrap.mode=DISABLED the
default admin/admin is pre-populated instead. Walkthrough:
docs/demo-rest.md.
# Terminal 1 — backend (same as the REST demo above)
mvn -pl :demo-rest exec:java
# Prints a bootstrap token to the console.
# Terminal 2 — Vaadin UI
mvn -pl :demo-vaadin-rest-client jetty:run
# Browser: http://localhost:9090/Browser opens /setup (because the backend has no admin yet). Paste
the token from the backend console, choose a username and password,
submit — the Vaadin UI calls POST /api/bootstrap/admin against
the backend, no in-JVM auth. Then log in. The UI never speaks HTTP
directly: only the encapsulated DemoBackendClient does.
Walkthrough: docs/demo-vaadin-rest-client.md.
mvn -pl demo-standalone exec:java \
-Dexec.mainClass=com.svenruppert.vaadin.security.demo.standalone.DemoAppDemo users are seeded: admin/admin, librarian/librarian,
alice/alice. After login, commands cover list, borrow <title>,
return <title>, add <title> (LIBRARIAN+), remove <title> (ADMIN
only). Calls run through a Secured.wrap(LibraryService.class, ...)
proxy that enforces the method-level @RequiresPermission /
@RequiresRole annotations; rejections surface as DENIED — … lines
in the terminal.
# Whole reactor — ~570 tests across all modules
mvn test
# Single module
mvn -pl :security-core -am test
mvn -pl :demo-rest -am test
mvn -pl :demo-vaadin-rest-client -am testFor a Vaadin Flow application:
<dependency>
<groupId>com.svenruppert</groupId>
<artifactId>security-vaadin</artifactId>
<version>00.60.00</version>
</dependency>For a REST handler / servlet application:
<dependency>
<groupId>com.svenruppert</groupId>
<artifactId>security-rest</artifactId>
<version>00.60.00</version>
</dependency>For a plain-Java / desktop / CLI application:
<dependency>
<groupId>com.svenruppert</groupId>
<artifactId>security-standalone</artifactId>
<version>00.60.00</version>
</dependency>security-core is pulled in transitively by any of the three adapters.
To secure a Vaadin Flow application, implement the following SPI contracts and
register them via META-INF/services/ files. Reference: demo-vaadin.
public record MyUser(String username, Set<String> roles) {}Validates credentials and loads the user subject.
public class MyAuthenticationService
implements AuthenticationService<Credentials, MyUser> {
@Override
public boolean checkCredentials(Credentials credentials) { /* ... */ }
@Override
public MyUser loadSubject(Credentials credentials) { /* ... */ }
@Override
public Class<MyUser> subjectType() { return MyUser.class; }
}Register in META-INF/services/com.svenruppert.vaadin.security.authentication.AuthenticationService:
com.example.MyAuthenticationService
Maps a user to roles. Only rolesFor() is required — permissionsFor() has a
default implementation returning empty permissions.
public class MyAuthorizationService implements AuthorizationService<MyUser> {
@Override
public HasRoles rolesFor(MyUser subject) { /* ... */ }
}Register in META-INF/services/com.svenruppert.vaadin.security.authorization.api.AuthorizationService.
@Retention(RUNTIME)
@SecurityAnnotation(MyRoleAccessEvaluator.class)
public @interface VisibleFor {
MyRole[] value();
}Or use the generic annotations from security-core:
@RequiresRole("ROLE_ADMIN")
@RequiresPermission("demo:edit")public class MyRoleAccessEvaluator
implements AccessEvaluator<VisibleFor> {
@Override
public AccessDecision evaluate(AccessContext context, VisibleFor annotation) {
// return AccessDecision.granted() or AccessDecision.denied("login", false)
}
}Or extend RoleBasedAccessEvaluator:
public class MyRoleAccessEvaluator
extends RoleBasedAccessEvaluator<VisibleFor, MyUser> {
@Override
public Set<RoleName> requiredRoles(VisibleFor annotation) { /* ... */ }
@Override
public String alternativeNavigationTarget(
AccessContext context, VisibleFor annotation) { /* ... */ }
}Register in META-INF/services/com.svenruppert.vaadin.security.authorization.api.AccessEvaluator.
public class MyLoginListener extends LoginListener<MyUser> {
@Override
public Class<? extends LoginView> loginNavigationTarget() {
return MyLoginView.class;
}
@Override
public Class<? extends Component> defaultNavigationTarget() {
return MainView.class;
}
}Register in META-INF/services/com.svenruppert.vaadin.security.authorization.LoginListener.
Create your login UI by extending the abstract LoginView base class.
@Route("admin")
@VisibleFor(MyRole.ADMIN)
public class AdminView extends Div { /* ... */ }To secure REST handlers, implement RestSubjectResolver, annotate handlers with
generic permission annotations, and run them through RestAuthorizationFilter.
A complete runnable reference lives in demo-rest: a JDK-only HTTP server
(com.sun.net.httpserver.HttpServer) and an interactive CLI
(java.net.http.HttpClient) demonstrating login, server-side operation
filtering, and the 200 / 401 / 403 decision flow. See
docs/demo-rest.md for run instructions and example
sessions.
public enum DemoPermission {
DOCUMENT_READ("document:read"),
DOCUMENT_DELETE("document:delete");
private final PermissionName permissionName;
// ...
}public final class DemoRolePermissionMapping implements RolePermissionMapping {
@Override
public Set<PermissionName> permissionsFor(RoleName role) { /* ... */ }
}public final class MyRestSubjectResolver implements RestSubjectResolver {
private static final BearerTokenExtractor BEARER = new BearerTokenExtractor();
@Override
public Optional<SecuritySubject> resolveSubject(RestRequest request) {
return BEARER.extract(request) // case-insensitive Bearer parser
.flatMap(myTokenStore::resolve)
.map(this::toSubject);
}
}The library does not enforce a token strategy. BearerTokenExtractor and
RestHeaders (case-insensitive header lookup) live in security-rest —
no need to roll your own.
public final class DocumentHandlers {
@RequiresPermission("document:read")
public void read(RestRequest request, RestResponse response) { /* ... */ }
@RequiresPermission("document:delete")
public void delete(RestRequest request, RestResponse response) { /* ... */ }
@RequiresPermission("document:create")
public void create(RestRequest request, RestResponse response) {
// Pattern-match instead of casting to a concrete adapter request type
if (request instanceof BodyRestRequest body) {
String json = body.bodyAsUtf8();
// ...
}
}
}Use BodyRestRequest (in security-rest) when a handler needs the request
body. Adapters supply the raw bytes; helpers decode UTF-8.
RestAuthorizationFilter filter =
new RestAuthorizationFilter(new MyRestSubjectResolver());
filter.authorizeAndHandle(
request, response, handlers::delete, handlerMethod);The filter:
- Resolves the subject from the request.
- Scans the handler method/class for a security annotation.
- Builds an
AccessContextwithresourceType="rest-endpoint". - Runs the matching
AuthorizationEvaluator. - Maps the decision:
Grantedruns the handler;Unauthenticated→401;Forbidden→403. Error bodies are short and generic — no internals leak.
For endpoints that need any authenticated subject but no specific permission
(/me, /logout, …), use RestAuthenticationFilter instead of writing
your own subject check:
RestAuthenticationFilter authFilter = new RestAuthenticationFilter(resolver);
authFilter.requireAuthenticated(request, response, handlers::me);
// 401 with body "Unauthorized" if no subject; delegates otherwisedemo-rest shows a GET /api/operations endpoint that returns only the
operations the current subject is allowed to invoke. Built on
SecuredOperationRegistry + OperationVisibilityService from
security-core — the same permission model that protects the handlers is
used to filter the discovery list. Clients never make local authorization
decisions.
To secure plain-Java code — desktop, CLI, daemon — annotate a service
interface, wrap implementations once with Secured.wrap(...), and drive
the login lifecycle with StandaloneLoginFlow. There is no listener,
no filter chain, no navigation phase; every method call on the wrapped
interface runs through the same SecurityAnnotationScanner + evaluator
machinery as the Vaadin and REST adapters.
A complete runnable reference lives in demo-standalone: an
interactive library-borrowing CLI with three seeded users and a
role/permission matrix exercising both @RequiresPermission and
@RequiresRole.
public interface LibraryService {
@RequiresPermission("book:list")
List<String> listBooks();
@RequiresPermission("book:borrow")
void borrowBook(String title);
@RequiresRole("ADMIN")
void removeBook(String title);
}LibraryService secured =
Secured.wrap(LibraryService.class, new InMemoryLibraryService());
secured.listBooks(); // runs if the bound subject has book:list
secured.removeBook("x"); // throws AccessDeniedException for non-ADMINSecured.wrap(...) returns a JDK dynamic-proxy implementing the
interface. Every call scans the method (then the declaring class) for a
@SecurityAnnotation-meta-annotated annotation, runs the matching
evaluator, and either delegates to the real implementation or throws
AccessDeniedException. Object methods bypass enforcement.
For callbacks / lambdas where wrapping an interface is awkward, call the single-shot helper:
Secured.requireAllowed(MyOps.class, "delete");
// throws AccessDeniedException if the calling subject is not allowedStandaloneLoginFlow<Credentials, User> flow = new StandaloneLoginFlow<>();
LoginResult<User> result = flow.login(new Credentials("alice", "alice"), "alice");
switch (result) {
case LoginResult.Success<User> s -> /* proceed */;
case LoginResult.Rejected<User> r -> /* wrong credentials */;
case LoginResult.LockedOut<User> l -> /* throttled — retry in l.decision().remaining() */;
}The flow consults LoginAttemptPolicy.beforeAttempt(...) first, then
calls the SPI-registered AuthenticationService.checkCredentials /
loadSubject, binds the subject through the active SubjectStore,
records success/failure on the policy, and publishes LoginSucceeded /
LoginFailed to the SecurityAuditService. flow.logout() clears the
SubjectStore for the current thread.
security-standalone registers ThreadLocalSubjectStore as the SPI
SubjectStore. It is not inherited across threads — a value bound
on the main thread is invisible to a background Executor. Propagating
the subject to worker threads is the application's responsibility:
capture the user before submitting work, then call
SubjectStores.subjectStore().setCurrentSubject(user, User.class) on
the worker thread (or use a Runnable wrapper that does that).
The library uses two decision types:
| Type | Module | Purpose |
|---|---|---|
AuthorizationDecision |
security-core |
Adapter-neutral: Granted / Unauthenticated / Forbidden |
AccessDecision |
security-core |
Vaadin-oriented (legacy, kept for backward compatibility) |
Adapters map these to framework-specific behavior:
security-vaadin→ navigation: continue, reroute to login, or reroute to error.security-rest→ HTTP status:200/handler,401, or403.
SecurityAnnotationScanner scans classes, methods, or any AnnotatedElement
for restriction annotations meta-annotated with @SecurityAnnotation. Both
adapters use the same scanner.
Generic annotations (in security-core):
@RequiresRole({"ROLE_ADMIN"})→RequiresRoleEvaluator@RequiresPermission("document:delete")→RequiresPermissionEvaluator@ProtectedBy(...)→ProtectedByEvaluator
Project-specific annotations are encouraged for Vaadin views (e.g. @VisibleFor).
| Type | Module / package | Purpose |
|---|---|---|
SecurityServiceResolver |
security-core/.../authorization/api |
Central SPI cache. Strict accessors throw IllegalStateException for missing services; find…() returns Optional; set…(…) is a programmatic test seam. Covers Authentication / Authorization / Audit / Action / LoginAttempt / Session / PasswordHasher / Logout. |
PermissionGuard |
security-core/.../authorization/api |
Stateless hasPermission / requirePermission (and role variants) on any HasPermissions/HasRoles. Throws AccessDeniedException. |
AuthenticationService<T,U> |
security-core/.../authentication |
SPI: credential validation + subject loading. Adapter-neutral. |
LogoutService |
security-core/.../logout |
logout(SubjectId, LogoutScope) SPI, paired with SubjectClearingLogoutService default + SubjectSessionRegistry for multi-session logout. Vaadin-side: VaadinLogoutService rotates HTTP session and redirects. |
LoginAttemptPolicy + InMemoryLoginAttemptPolicy |
security-core/.../bruteforce |
Pluggable login throttling. Sealed LoginAttemptDecision = Allowed | LockedOut(Duration, int). Configured via LoginAttemptConfiguration[Loader] (sysprop/env/default). |
SessionPolicy<U> + TimeoutSessionPolicy |
security-core/.../session |
Idle/absolute lifetime checks. evaluate(SessionMetadata) pure-query path consumed by SessionLifetimeListener (Vaadin) and the REST filters. rotateSessionAfterLogin honoured via VaadinService.reinitializeSession(...). |
SecurityAuditService, sealed AuditEvent (16 record variants), RingBufferAuditSink, LoggingAuditSink, CompositeAuditService, DefaultCompositeAuditService |
security-core/.../audit |
Typed publish/query audit pipeline. RingBufferAuditSink backs the Vaadin /audit-route and the REST GET /api/audit endpoint. |
ActionAuthorizationService<U>, ActionPermission, StaticActionAuthorizationService |
security-core/.../action |
Stable SPI for isAllowed/requireAllowed action checks with ACTION_DENIED audit on denial. |
PasswordHasher, PasswordHash, Pbkdf2PasswordHasher |
security-core/.../authentication |
Hash + verify + needsRehash (drift detection); demos rehash transparently on successful login. |
StaticRolePermissionMapping, RolePermissionResolver |
…/api/permissions |
Immutable role → permissions map with a builder; permission-merge across roles. |
SecuredOperationDescriptor, SecuredOperationRegistry, OperationVisibilityService |
…/api/operations |
Generic operation discovery with subject-aware filtering. Adapter metadata (HTTP method, path, view class) goes into the descriptor's attributes. |
BootstrapConfigurationLoader, BootstrapStatus |
security-core/.../bootstrap |
Centralised sysprop+env+default loading with TTL parsing; leak-safe status snapshot. |
RestHeaders, BearerTokenExtractor |
security-rest |
Case-insensitive header lookup and Bearer-token parsing. |
RestAuthenticationFilter, RestAuthorizationFilter |
security-rest |
401/403 filters; the authorization filter additionally consults SessionPolicy.evaluate(...) when subject-resolved metadata is available. |
BodyRestRequest |
security-rest |
Body-capable RestRequest. Avoids concrete-class casts in handlers. |
BootstrapRestStatusMapper |
security-rest |
InitialAdminCreationResult → HTTP status code + stable error code. |
Stable: role-based access, REST adapter contracts, SecuritySubject,
AccessContext, AuthorizationDecision, scanner.
Experimental (marked with @ExperimentalSecurityApi): permission-based
access types — PermissionBasedAccessEvaluator, PermissionName,
HasPermissions, PermissionAuthorizationService. May change in incompatible
ways in future releases.
Library modules contain no concrete business permissions. Examples like
document:read belong in demo-rest. Real applications define their own
catalog (e.g. shortlink:create, audit:read) inside the consuming project.
See docs/security-modules.md for the full
extension model.
Both demos ship without any administrator account. The first administrator
is created via a one-time bootstrap token in either PERSISTENT_FILE
or TRANSIENT_CONSOLE mode. The same library powers the REST endpoint,
the CLI init-admin command, and the Vaadin /setup view. Token values
are never written to logs, never echoed in responses, and the mechanism
turns itself off once an administrator exists.
Configurable via system properties (preferred) or environment variables —
both read centrally by BootstrapConfigurationLoader:
| System property | Environment variable | Default (demos) |
|---|---|---|
security.bootstrap.mode |
SECURITY_BOOTSTRAP_MODE |
TRANSIENT_CONSOLE |
security.bootstrap.token.file |
SECURITY_BOOTSTRAP_TOKEN_FILE |
./data/bootstrap.token |
security.bootstrap.token.ttl |
SECURITY_BOOTSTRAP_TOKEN_TTL |
PT24H |
See docs/bootstrap.md for modes, endpoints, and the
operator workflow.
Konzept-V00.60.00.md outlines further steps: SecurityAuditService,
LoginAttemptPolicy (brute-force), minimal SessionPolicy, central
LogoutService, and ActionAuthorizationService (isAllowed /
requireAllowed). The bootstrap mechanism and PasswordHasher
abstraction are now in place; the rest is pending.
EUPL 1.2