Skip to content
Flag of Europe
Made in the European Union · Independently built · Released under EUPL 1.2
Standalone Integration

Standalone Integration

To secure a Core-Java application — a CLI tool, a desktop app, a batch job, anything without a Vaadin or REST surface — implement the same SPI contracts as for the framework adapters, then wrap your business services with Secured.wrap(…). Every annotated method call runs through the same SecurityAnnotationScanner that powers the Vaadin and REST adapters. The reference implementation is in demo-standalone.

Status: implemented. The security-standalone adapter is three classes (Secured, StandaloneLoginFlow, ThreadLocalSubjectStore) on top of security-core — no third-party dependencies. Released as part of v00.60 alongside an interactive CLI demo (demo-standalone).

When to use it

SetupAdapter
Vaadin Flow web UIsecurity-vaadin
HTTP server / REST endpointssecurity-rest
CLI, desktop, batch, embedded — anything not a web requestsecurity-standalone

The standalone adapter is the right fit when there is no request/response cycle to hook a filter into. It enforces security declaratively at the service-method boundary through a dynamic proxy.

1. Define your service interface with annotations

Just like in the Vaadin and REST cases, annotate methods on an interface with the generic security annotations from security-core:

public interface LibraryService {

  @RequiresPermission("book:list")
  List<String> listBooks();

  @RequiresPermission("book:borrow")
  void borrowBook(String title);

  @RequiresPermission("book:add")
  void addBook(String title);

  @RequiresRole("ADMIN")
  void removeBook(String title);
}

2. Wrap your implementation with Secured.wrap(…)

LibraryService library =
    Secured.wrap(LibraryService.class, new InMemoryLibraryService());

library.listBooks();        // throws AccessDeniedException if subject lacks book:list
library.addBook("Frank");   // throws AccessDeniedException if subject lacks book:add

Secured.wrap(…) returns a java.lang.reflect.Proxy that implements your interface. For each method invocation the proxy:

  1. Scans the method (and its declaring class) for security annotations.
  2. Resolves the current subject through SecurityServiceResolver and the configured SubjectStore.
  3. Runs the evaluator paired with the annotation.
  4. Either delegates to the real implementation or throws AccessDeniedException.

Methods without security annotations pass through unchanged.

3. (Optional) Single-shot check for callbacks

When an interface isn’t a clean fit — a Runnable, a callback, a lambda — use the static helper instead:

public void onClick() {
  Secured.requireAllowed(MyHandler.class, "deleteSelected");
  documentService.delete(selectedId);
}

It locates the named method on the owning class, reads its security annotations, and throws AccessDeniedException on denial.

4. Drive login with StandaloneLoginFlow

StandaloneLoginFlow<Credentials, User> flow = new StandaloneLoginFlow<>();

LoginResult<User> result = flow.login(new Credentials(user, pass), user);

switch (result) {
  case LoginResult.Success<User>(User subject) -> {
    // proceed — the subject is now bound into the SubjectStore
  }
  case LoginResult.Rejected<User> r ->
    System.out.println("Bad credentials.");
  case LoginResult.LockedOut<User>(var lockout) ->
    System.out.printf("Locked out — try again in %s%n", lockout.retryAfter());
}

StandaloneLoginFlow does the same lifecycle as the Vaadin and REST adapters:

  1. Asks the registered LoginAttemptPolicy whether the username is currently throttled.
  2. Delegates to the registered AuthenticationService for credential validation.
  3. On success: loads the subject, binds it into the active SubjectStore, records the success on the brute-force policy, publishes a LoginSucceeded audit event.
  4. On failure: records the failure on the policy, publishes a LoginFailed audit event.

Logout is symmetrical:

flow.logout();    // clears the subject from the SubjectStore + emits LogoutPerformed

5. Pick a SubjectStore

For a single-user desktop or CLI application, register ThreadLocalSubjectStore via META-INF/services/:

# META-INF/services/com.svenruppert.vaadin.security.authorization.api.SubjectStore
com.svenruppert.vaadin.security.standalone.ThreadLocalSubjectStore

It keeps the current subject in a thread-local map keyed by subject class. For server-side or multi-tenant setups, provide your own SubjectStore (request-scoped, session-scoped, …).

Reference: the demo-standalone library CLI

Run the demo with:

mvn -pl :demo-standalone exec:java \
    -Dexec.mainClass=com.svenruppert.vaadin.security.demo.standalone.DemoApp

It opens an interactive shell against an in-memory book library:

=== Library CLI ===
Seeded users: admin/admin, librarian/librarian, alice/alice

Username: alice
Password: alice
Welcome, Alice. Roles: [READER]

> list
- Effective Java
- Java Concurrency in Practice
- The Pragmatic Programmer

> add "Clean Code"
Access denied. (alice lacks 'book:add'.)

> logout
> exit

The LibraryService interface, the InMemoryLibraryService impl, and the role-permission mapping (READER, LIBRARIAN, ADMIN) live entirely in demo-standalone — the library modules carry no business-specific code.

Building blocks reference

TypeModule / packagePurpose
Secured.wrap(Class<T>, T)security-standaloneDynamic-proxy wrapper over an interface. Throws AccessDeniedException on denial.
Secured.requireAllowed(Class<?>, String)security-standaloneSingle-shot annotation check for callbacks/lambdas.
StandaloneLoginFlow<T, U>security-standaloneLogin + logout lifecycle without UI assumptions; integrates brute-force + audit.
LoginResult<U>security-standaloneSealed Success(U) | Rejected | LockedOut(LockedOut).
ThreadLocalSubjectStoresecurity-standaloneDefault SubjectStore for single-user JVM apps.

The adapter pulls in exactly one dependency: security-core. No Servlet, no HTTP client, no Vaadin.