https://github.com/vaadin-developer/security-for-flow
Proof Of Concept for a Security AddOn
https://github.com/vaadin-developer/security-for-flow
flow java jetty kotlin poc ruppert security sven vaadin
Last synced: 18 days ago
JSON representation
Proof Of Concept for a Security AddOn
- Host: GitHub
- URL: https://github.com/vaadin-developer/security-for-flow
- Owner: vaadin-developer
- License: eupl-1.2
- Created: 2019-07-07T11:09:19.000Z (almost 7 years ago)
- Default Branch: develop
- Last Pushed: 2026-05-13T15:50:40.000Z (19 days ago)
- Last Synced: 2026-05-13T17:33:51.268Z (19 days ago)
- Topics: flow, java, jetty, kotlin, poc, ruppert, security, sven, vaadin
- Language: Java
- Size: 1.1 MB
- Stars: 1
- Watchers: 5
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
- Security: docs/security-modules.md
Awesome Lists containing this project
README
# Security for Flow
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 Structure
| 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` |
### Dependency Rules
```text
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.
## Quick Start
### Build
```bash
# Full build (requires Maven 3.9.9+, Java 26+)
mvn clean install
```
`mvn 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).
### Pick the right demo
| You want to see … | Run |
|---|---|
| Vaadin role/permission UI in a single JVM, no backend | [`demo-vaadin`](docs/demo-vaadin.md) |
| Pure REST security (HTTP server + interactive CLI), no UI | [`demo-rest`](docs/demo-rest.md) |
| Vaadin UI talking to a separate REST backend (real two-tier setup) | [`demo-vaadin-rest-client`](docs/demo-vaadin-rest-client.md) |
| Plain-Java / CLI / desktop integration (no HTTP, no Vaadin) | `mvn -pl demo-standalone exec:java -Dexec.mainClass=com.svenruppert.vaadin.security.demo.standalone.DemoApp` |
### `demo-vaadin` — Standalone Vaadin demo
```bash
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`](docs/demo-vaadin.md).
### `demo-rest` — REST server + CLI
```bash
# 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 ` 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`](docs/demo-rest.md).
### `demo-vaadin-rest-client` — Vaadin UI + REST backend
```bash
# 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`](docs/demo-vaadin-rest-client.md).
### `demo-standalone` — Interactive CLI
```bash
mvn -pl demo-standalone exec:java \
-Dexec.mainClass=com.svenruppert.vaadin.security.demo.standalone.DemoApp
```
Demo users are seeded: `admin/admin`, `librarian/librarian`,
`alice/alice`. After login, commands cover `list`, `borrow `,
`return `, `add ` (LIBRARIAN+), `remove ` (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.
### Tests
```bash
# 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 test
```
### Add the dependency
For a Vaadin Flow application:
```xml
com.svenruppert
security-vaadin
00.51.01-SNAPSHOT
```
For a REST handler / servlet application:
```xml
com.svenruppert
security-rest
00.51.01-SNAPSHOT
```
For a plain-Java / desktop / CLI application:
```xml
com.svenruppert
security-standalone
00.51.01-SNAPSHOT
```
`security-core` is pulled in transitively by any of the three adapters.
## Vaadin Integration
To secure a Vaadin Flow application, implement the following SPI contracts and
register them via `META-INF/services/` files. Reference: `demo-vaadin`.
### 1. Define a user type
```java
public record MyUser(String username, Set roles) {}
```
### 2. Implement `AuthenticationService`
Validates credentials and loads the user subject.
```java
public class MyAuthenticationService
implements AuthenticationService {
@Override
public boolean checkCredentials(Credentials credentials) { /* ... */ }
@Override
public MyUser loadSubject(Credentials credentials) { /* ... */ }
@Override
public Class subjectType() { return MyUser.class; }
}
```
Register in `META-INF/services/com.svenruppert.vaadin.security.authentication.AuthenticationService`:
```
com.example.MyAuthenticationService
```
### 3. Implement `AuthorizationService`
Maps a user to roles. Only `rolesFor()` is required — `permissionsFor()` has a
default implementation returning empty permissions.
```java
public class MyAuthorizationService implements AuthorizationService {
@Override
public HasRoles rolesFor(MyUser subject) { /* ... */ }
}
```
Register in `META-INF/services/com.svenruppert.vaadin.security.authorization.api.AuthorizationService`.
### 4. Define a restriction annotation with `@SecurityAnnotation`
```java
@Retention(RUNTIME)
@SecurityAnnotation(MyRoleAccessEvaluator.class)
public @interface VisibleFor {
MyRole[] value();
}
```
Or use the generic annotations from `security-core`:
```java
@RequiresRole("ROLE_ADMIN")
@RequiresPermission("demo:edit")
```
### 5. Implement `AccessEvaluator`
```java
public class MyRoleAccessEvaluator
implements AccessEvaluator {
@Override
public AccessDecision evaluate(AccessContext context, VisibleFor annotation) {
// return AccessDecision.granted() or AccessDecision.denied("login", false)
}
}
```
Or extend `RoleBasedAccessEvaluator`:
```java
public class MyRoleAccessEvaluator
extends RoleBasedAccessEvaluator {
@Override
public Set requiredRoles(VisibleFor annotation) { /* ... */ }
@Override
public String alternativeNavigationTarget(
AccessContext context, VisibleFor annotation) { /* ... */ }
}
```
Register in `META-INF/services/com.svenruppert.vaadin.security.authorization.api.AccessEvaluator`.
### 6. Extend `LoginListener`
```java
public class MyLoginListener extends LoginListener {
@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`.
### 7. Extend `LoginView`
Create your login UI by extending the abstract `LoginView` base class.
### 8. Annotate route views
```java
@Route("admin")
@VisibleFor(MyRole.ADMIN)
public class AdminView extends Div { /* ... */ }
```
## REST Integration
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`](docs/demo-rest.md) for run instructions and example
sessions.
### 1. Define project permissions and role mapping
```java
public enum DemoPermission {
DOCUMENT_READ("document:read"),
DOCUMENT_DELETE("document:delete");
private final PermissionName permissionName;
// ...
}
```
```java
public final class DemoRolePermissionMapping implements RolePermissionMapping {
@Override
public Set permissionsFor(RoleName role) { /* ... */ }
}
```
### 2. Implement `RestSubjectResolver`
```java
public final class MyRestSubjectResolver implements RestSubjectResolver {
private static final BearerTokenExtractor BEARER = new BearerTokenExtractor();
@Override
public Optional 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.
### 3. Annotate handlers
```java
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.
### 4. Wire the filter
```java
RestAuthorizationFilter filter =
new RestAuthorizationFilter(new MyRestSubjectResolver());
filter.authorizeAndHandle(
request, response, handlers::delete, handlerMethod);
```
The filter:
1. Resolves the subject from the request.
2. Scans the handler method/class for a security annotation.
3. Builds an `AccessContext` with `resourceType="rest-endpoint"`.
4. Runs the matching `AuthorizationEvaluator`.
5. Maps the decision: `Granted` runs the handler; `Unauthenticated` → `401`;
`Forbidden` → `403`. Error bodies are short and generic — no internals leak.
### 5. Authenticated-only endpoints
For endpoints that need any authenticated subject but no specific permission
(`/me`, `/logout`, …), use `RestAuthenticationFilter` instead of writing
your own subject check:
```java
RestAuthenticationFilter authFilter = new RestAuthenticationFilter(resolver);
authFilter.requireAuthenticated(request, response, handlers::me);
// 401 with body "Unauthorized" if no subject; delegates otherwise
```
### 6. (Optional) Operation discovery filtered server-side
`demo-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.
## Standalone Integration
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`.
### 1. Define the service interface
```java
public interface LibraryService {
@RequiresPermission("book:list")
List listBooks();
@RequiresPermission("book:borrow")
void borrowBook(String title);
@RequiresRole("ADMIN")
void removeBook(String title);
}
```
### 2. Wrap the implementation
```java
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-ADMIN
```
`Secured.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:
```java
Secured.requireAllowed(MyOps.class, "delete");
// throws AccessDeniedException if the calling subject is not allowed
```
### 3. Drive the login flow
```java
StandaloneLoginFlow flow = new StandaloneLoginFlow<>();
LoginResult result = flow.login(new Credentials("alice", "alice"), "alice");
switch (result) {
case LoginResult.Success s -> /* proceed */;
case LoginResult.Rejected r -> /* wrong credentials */;
case LoginResult.LockedOut 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.
### 4. SubjectStore — ThreadLocal by default
`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).
## Decision Model
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`, or `403`.
## Annotation-Driven Protection
`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`).
## Reusable security building blocks
| 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` | `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` + `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`, `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 vs. Experimental API
**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.
## Project-Specific Permissions Live in Applications
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`](docs/security-modules.md) for the full
extension model.
## First-run bootstrap
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`](docs/bootstrap.md) for modes, endpoints, and the
operator workflow.
## Roadmap
`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.
## License
EUPL 1.2