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), a contract-only persistence testkit plus an Eclipse-Store-backed reference persistence layer, one transport-level shared module, and five reference demos — 13 modules in total. Concrete roles and permissions live in applications or demo modules — never in the library.
| Module | Artifact | Description |
|---|---|---|
jSentinel-core |
jSentinel-core |
Generic, framework-neutral security concepts and decision logic. Owns every SPI contract, all 11 persistence-store interfaces (Phase 2), the JSentinelVersion drift-detection stack (Phase 4), and the account-lifecycle / token / rate-limit services (Phase 7) |
jSentinel-vaadin |
jSentinel-vaadin |
Vaadin Flow adapter — view and navigation security; ships the Phase-8 SecuredButton / SecuredRouterLink / SecuredMenuItem / SessionManagementView building blocks |
jSentinel-rest |
jSentinel-rest |
Framework-light REST adapter — request and handler security; ships the Phase-4c RestJSentinelVersionFilter and the Phase-8d OpenApiJSentinelMetadataGenerator |
jSentinel-standalone |
jSentinel-standalone |
Plain-Java / desktop / CLI adapter — ThreadLocal subject + dynamic-proxy method-level enforcement |
jSentinel-test |
jSentinel-test |
Reusable test fixtures: FakeAuthenticationService, FakeAuthorizationService, InMemorySubjectStore, RecordingAuditSink, JUnit-5 JSentinelTestExtension. Pull in as <scope>test</scope> |
jSentinel-processor |
jSentinel-processor |
Compile-time annotation processor: generates <Type>Secured subclasses for @Secured-annotated concrete classes. Wire as <annotationProcessorPath>, not as a regular dependency |
jSentinel-persistence-testkit |
jSentinel-persistence-testkit |
Contract test suites for every persistence-store SPI in jSentinel-core — @Test default-method interfaces a custom store adapter implements to be vetted against the library's persistence contract. Persistence-tech-agnostic |
jSentinel-persistence-eclipsestore |
jSentinel-persistence-eclipsestore |
Eclipse-Store (org.eclipse.store:storage-embedded) reference impl of every persistence-store SPI; passes the same 95+ contract suite as the in-memory defaults. Drop-in for apps that want durable persistence |
jSentinel-crypto-bc |
jSentinel-crypto-bc |
V00.71 optional opt-in module: Argon2id, bcrypt and scrypt password hash providers via BouncyCastle (bcprov-jdk18on:1.78.1). BouncyCastleHashingServices.modern() wires the modern profile. The core stays JDK-only when this module is absent |
jSentinel-credentials-hibp |
jSentinel-credentials-hibp |
V00.71 optional opt-in module: HaveIBeenPwned k-anonymity compromised-password checker. Uses JDK HttpClient only — no extra runtime dependencies. Plaintext never leaves the JVM (only the SHA-1 first-5-hex prefix is transmitted) |
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 + member directory) showing both SecuredProxy.wrap(...) (dynamic-proxy) and the annotation-processor-generated <Type>Secured wrapper |
jSentinel-core -> (no project deps)
jSentinel-vaadin -> jSentinel-core
jSentinel-rest -> jSentinel-core
jSentinel-standalone -> jSentinel-core
jSentinel-test -> jSentinel-core (compile; the test extension implements JUnit lifecycle types)
jSentinel-processor -> jSentinel-core, com.svenruppert:proxybuilder:00.11.00, com.svenruppert:proxybuilder-annotations:00.11.00
jSentinel-persistence-testkit -> jSentinel-core (compile; suites use ServiceLoader-free wiring)
jSentinel-persistence-eclipsestore -> jSentinel-core, org.eclipse.store:storage-embedded:4.1.0
(test scope: jSentinel-persistence-testkit)
jSentinel-crypto-bc -> jSentinel-core, org.bouncycastle:bcprov-jdk18on:1.78.1
(V00.71 optional opt-in)
jSentinel-credentials-hibp -> jSentinel-core (JDK HttpClient only;
V00.71 optional opt-in)
demo-rest-shared -> (no project deps; transport-only)
demo-vaadin -> jSentinel-core, jSentinel-vaadin
demo-rest -> jSentinel-core, jSentinel-rest, demo-rest-shared
demo-vaadin-rest-client -> jSentinel-core, jSentinel-vaadin, demo-rest-shared
(test scope only: demo-rest)
demo-standalone -> jSentinel-core, jSentinel-standalone
(annotationProcessorPath: jSentinel-processor)
jSentinel-core has no Vaadin, Servlet, or REST-framework dependencies.
The four adapter modules (jSentinel-vaadin, jSentinel-rest,
jSentinel-standalone, jSentinel-processor) never depend on each other.
jSentinel-persistence-eclipsestore is the only module with a third-party
storage dependency. jSentinel-crypto-bc is the only module that pulls in
BouncyCastle — it stays opt-in so the core remains JDK-only.
The repository ships ten battle-tested Claude Code skills under
docs/skills/claude/ that automate the
integration of jSentinel into a fresh application. Each skill is a
SKILL.md (with frontmatter description Claude Code uses for
auto-discovery) plus a references/ directory of .java.tmpl
templates that get rendered with project-specific slots.
Every skill listed below was validated against the reactor itself —
the matching demo-jsentinel-* module in this repository is the
verbatim output of running the skill, and all ten modules compile in
the standard reactor build.
The skills are organised as three adapters (Vaadin, REST, Standalone/CLI) × three layers (basic → persistence → hardening), plus one hybrid that turns a Vaadin frontend into a REST-backend client:
| Vaadin Flow | REST (JDK HttpServer) | Standalone (CLI/desktop) | |
|---|---|---|---|
| Layer 1 — basic | jsentinel-vaadin |
jsentinel-rest |
jsentinel-standalone |
| Layer 2 — persistence | jsentinel-vaadin-persistence |
jsentinel-rest-persistence |
jsentinel-standalone-persistence |
| Layer 3 — hardening | jsentinel-vaadin-hardening |
jsentinel-rest-hardening |
jsentinel-standalone-hardening |
| Hybrid | jsentinel-vaadin-rest-client — Vaadin frontend delegates auth to a jsentinel-rest backend |
Each cell points at the canonical SKILL.md; the references/
sibling holds the templates Claude Code renders into the consumer's
codebase.
jsentinel-vaadin—VaadinSecurity.bootstrap()+@JSentinelAutoServiceAuthn/Authz + pre-seededadmin/admin&user/user+LoginView+ AppLayoutMainLayouthosting a publicPublicHomeViewand a@VisibleFor(USER)DashboardView+ filterable audit grid + session inventory + role admin. Also exports the project-localBootstrapExtensionSPI plus theBootstrapBuilderhelper that loads every registered extension viaServiceLoader— the seam through which layer 2 and layer 3 plug in additively.jsentinel-vaadin-persistence— Eclipse-Store-backed audit + session + bootstrap stores, plus the V00.70 token-based first-admin flow (SetupView→InitialAdminBootstrapService); the plaintextadmin/adminseed is replaced. Plugs in as aPersistenceBootstrapExtension(order=10) — no overwrite of the layer-1 entry-point listener.jsentinel-vaadin-hardening—BouncyCastleHashingServices.modern()(Argon2id) replaces PBKDF2, optional HIBP password-leak check, and Phase-4c drift detection (JSentinelVersionEnforcerListener+VersionBumper.bump(user)after every role mutation) — a revoked role reroutes the affected user to login on their next click. Plugs in as aHardeningBootstrapExtension(order=20); composes additively with persistence.jsentinel-rest—RestSecurity.bootstrap()+ Bearer-tokenTokenStore+ JDK-HttpServer-based Router withPOST /api/auth/login+whoami+ audit + sessions + users endpoints, gated by@RequiresPermissionsemantics. SameBootstrapExtensionSPI surface as the Vaadin variant.jsentinel-rest-persistence— same Eclipse-Store swap as the Vaadin variant, withPOST /api/setupreplacing the VaadinSetupViewand a 503 bootstrap-required guard on every other endpoint until the first admin is provisioned. Bootstrap-chain contributions flow throughPersistenceBootstrapExtension.jsentinel-rest-hardening— Argon2id + drift detection wired throughRestJSentinelVersionFilter; revoking a role makes every open Bearer token for that user start returning 401 on the next request. Bootstrap-chain contributions flow throughHardeningBootstrapExtension.jsentinel-standalone—StandaloneSecurity.bootstrap()+StandaloneLoginFlow+SecuredProxy.wrap(DocumentService.class, impl)for runtime enforcement on an interface, interactive CLI withlist / create / delete / audit / whoami / quit. SameBootstrapExtensionSPI surface.jsentinel-standalone-persistence— Eclipse-Store persistence + a CLIsetupprompt that reads the bootstrap token + creates the first admin before the regular login loop unlocks.PersistenceBootstrapExtension.jsentinel-standalone-hardening— Argon2id, HIBP, and drift wiring for the CLI; drift detection is best-effort because CLI lifetimes are short, but the API surface stays uniform across all three adapters.HardeningBootstrapExtension.jsentinel-vaadin-rest-client— small hybrid skill that patches ajsentinel-vaadinconsumer so itsAuthenticationServicePOSTs to/api/auth/loginand itsAuthorizationServicereads roles from/api/whoamion ajsentinel-restbackend — server holds the truth, the Vaadin app keeps only the UI shell.
The skills are not just documentation — every skill has a
corresponding demo-jsentinel-* module in the reactor that contains
the verbatim, compilable result of running the skill on a fresh
module:
./mvnw -pl :demo-jsentinel-vaadin, \
:demo-jsentinel-vaadin-persistence, \
:demo-jsentinel-vaadin-hardening, \
:demo-jsentinel-rest, \
:demo-jsentinel-rest-persistence, \
:demo-jsentinel-rest-hardening, \
:demo-jsentinel-standalone, \
:demo-jsentinel-standalone-persistence, \
:demo-jsentinel-standalone-hardening, \
:demo-jsentinel-vaadin-rest-client \
-am compile -DskipTestsEvery directory matches one cell of the matrix above. The skills
double as a regression test: a future API rename in jSentinel-core
or jSentinel-dx-* that breaks a skill template breaks the
corresponding demo module in the next reactor build.
Layer 1 ships a project-local SPI (BootstrapExtension) plus a
helper (BootstrapBuilder) that the entry-point listener
(JSentinelBootstrapInitListener / RestServer.start() /
Main.main()) delegates to:
// Layer 1: BootstrapExtension.java — project-local SPI
public interface BootstrapExtension {
default void contributeAudit(AuditBootstrap a) {}
default void contributeSessions(SessionBootstrap s) {}
default void contributeCredentials(CredentialBootstrap c) {}
default int order() { return 0; }
}
// Layer 1: entry-point listener — never re-rendered by later layers
JSentinelRuntime runtime = BootstrapBuilder.apply(
VaadinSecurity.bootstrap()
.use(VaadinJSentinelStarter.developmentDefaults())
.authentication(authn)
.authorization(authz)
.loginRoute("login")
).install();BootstrapBuilder.apply(...) loads every registered
BootstrapExtension via ServiceLoader, sorts by order(), and
invokes the three contribute* hooks inside a single
.audit(...) / .sessions(...) / .credentials(...) call on the
fluent chain. Layer 2 and layer 3 each ship one extension class plus
a one-line entry in META-INF/services/<base>.security.bootstrap.BootstrapExtension
— neither rewrites the entry-point listener:
// Layer 2: PersistenceBootstrapExtension (order=10)
public final class PersistenceBootstrapExtension implements BootstrapExtension {
static { // eager open
STORAGE = JSentinelStorageProvider.storage();
BootstrapWiring.instance(); // print token
}
@Override public void contributeAudit(AuditBootstrap a) {
a.storeBacked(STORAGE.auditEventStore()).logging();
}
@Override public void contributeSessions(SessionBootstrap s) {
s.storeBacked(STORAGE.sessionStore());
}
@Override public int order() { return 10; }
}
// Layer 3: HardeningBootstrapExtension (order=20)
public final class HardeningBootstrapExtension implements BootstrapExtension {
@Override public void contributeCredentials(CredentialBootstrap c) {
c.hashing(BouncyCastleHashingServices.modern());
}
@Override public void contributeSessions(SessionBootstrap s) {
JSentinelServiceResolver.findJSentinelVersionStore().ifPresent(s::securityVersion);
JSentinelServiceResolver.findSubjectIdResolver().ifPresent(s::subjectIdResolver);
}
@Override public int order() { return 20; }
}The same .sessions(...) sub-builder is configured twice within one
.sessions(...) call (storeBacked(...) by persistence,
securityVersion(...) + subjectIdResolver(...) by hardening) — both
contributions land on the same sub-builder and stack. This is what
makes the order persistence-vs-hardening irrelevant.
Adding a future layer (a hypothetical
jsentinel-vaadin-mfa, a project-local RateLimitBootstrapExtension,
…) means shipping a new BootstrapExtension implementation + one
service-file line. No existing skill changes; no entry-point listener
rewrite.
To consume a skill in your own project:
- Copy the skill directory from
docs/skills/claude/<skill-name>/into Claude Code's skill location:~/.claude/skills/<skill-name>/. Claude Code auto-discovers any directory at this path containing aSKILL.mdwith proper frontmatter. - From within Claude Code, invoke the skill — either via the
/<skill-name>slash command or by describing your intent ("integrate jSentinel into my Vaadin app", "secure my REST API with admin/user", "add token-based bootstrap to my REST module"). Claude Code's auto-discovery matches the description against the SKILL.md frontmatter and triggers the skill. - Claude Code reads the
SKILL.md, asks for the slots that are missing from your brief (typically: target Maven module, base package, subject type name), renders every.java.tmpl/services-*.tmpl/pom-snippet.xml.tmplfromreferences/with those slots substituted, and writes the rendered files into your project tree at the paths the SKILL.md specifies. - Run
./mvnw -pl <your-module> -am compileto verify — the same command the reactor runs to validate the matchingdemo-jsentinel-*module.
Day 1: jsentinel-<adapter> → working login + roles + admin UI
Day X: jsentinel-<adapter>-persistence → users / audit / sessions survive restart
Day Y: jsentinel-<adapter>-hardening → Argon2id + drift detection + HIBP
For Vaadin+REST hybrids:
Backend: jsentinel-rest [+ -persistence + -hardening]
Frontend: jsentinel-vaadin + jsentinel-vaadin-rest-client
Layer 1 is the only hard prerequisite — it ships the
BootstrapExtension SPI plus the BootstrapBuilder helper that the
later layers plug into. Once layer 1 is in place, layer 2 and layer
3 can be applied in either order, only one of them, or
both — the bootstrap chain composes them additively (see
"How composition works" above). No "merge manually" caveat, no
second-writer-wins on the listener file.
| What you need | Skills to run |
|---|---|
| In-memory demo for first 5 minutes | layer 1 only |
| Production-grade auth, in-memory user store | layer 1 + layer 3 |
| Persistent users, PBKDF2 still acceptable | layer 1 + layer 2 |
| Full production setup | layer 1 + layer 2 + layer 3 (any order) |
- OpenAPI metadata, CORS, refresh tokens, API keys, rate
limiting — V00.72+ features available via the
jSentinel-dx-rest.openApiMetadata(...)/.cors(...)surface but not templated. - Custom
JSentinelSubjectmappers — beyond the defaults, mapping lives inJSentinelSubjectMapperand stays project-specific. - Multi-tenant policies — the skills are single-tenant; multi-tenant variants are roadmap V00.74+.
- Multi-factor authentication — separate concern.
- Refresh-token rotation in the hybrid — the Vaadin client stores
the Bearer token in
VaadinSession; rotation belongs in the REST backend or a follow-up skill.
# Full build (Maven 4 via the wrapper, Java 26+)
./mvnw clean installThe project is pinned to Maven 4 (minimum-maven.version=4.0.0-rc-5)
through ./mvnw; the wrapper downloads the right distribution on first
use. install (rather than package) 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.jsentinel.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.jsentinel.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.jsentinel.demo.standalone.DemoAppDemo users are seeded: admin/admin, librarian/librarian,
alice/alice. The CLI exposes both enforcement paths side by
side:
- Runtime / dynamic-proxy — book commands (
list,borrow,return,add,remove) run throughSecuredProxy.wrap(LibraryService.class, …).LibraryServiceis an interface; the JDK proxy calls intoJSentinelEnforceron every invocation. - Compile-time / annotation processor — member commands
(
members,invite,remove-member,reset-members) operate onMemberDirectory, a concrete class annotated with@Secured. ThejSentinel-processorannotation processor generatesMemberDirectorySecuredat compile time; each guarded method inserts aJSentinelEnforcer.require…(…)call ahead ofsuper.<method>(…).
Both paths share the same JSentinelEnforcer, so the rules are
identical. Rejections surface as DENIED — … lines in the terminal.
# Whole reactor — over 1600 tests across all modules
./mvnw test
# Single module
./mvnw -pl :jSentinel-core -am test
./mvnw -pl :demo-rest -am test
./mvnw -pl :demo-vaadin-rest-client -am testLibrary test totals as of V00.70.00:
jSentinel-core 956, jSentinel-vaadin 172, jSentinel-rest 71,
jSentinel-standalone 30, jSentinel-test 44, jSentinel-processor 11,
jSentinel-persistence-testkit 104, jSentinel-persistence-eclipsestore
104 — all green. Demo tests: demo-vaadin 103, demo-rest 48,
demo-vaadin-rest-client 13, demo-standalone 34.
./mvnw -pl :jSentinel-core org.pitest:pitest-maven:mutationCoverageThe parent POM pins pitest-test-classes=com.svenruppert.*. Reports
land under <module>/target/pit-reports/index.html. Current state per
library module (V00.70.00):
| Module | Coverage | Tests |
|---|---|---|
jSentinel-core |
86 % (1191/1381) | 956 |
jSentinel-vaadin |
79 % (242/305) | 172 |
jSentinel-rest |
95 % (86/91) | 71 |
jSentinel-standalone |
97 % (33/34) | 30 |
jSentinel-processor |
82 % (23/28) | 11 |
jSentinel-persistence-eclipsestore |
70 % (231/328) | 104 |
jSentinel-test |
n/a (test fixtures) | 44 |
jSentinel-persistence-testkit |
n/a (contracts verified through consumers) | 104 |
For the per-module progression across 00.51.00 → 00.60.00 → 00.70.00
see the Mutation coverage section of each
release-notes file.
For a Vaadin Flow application:
<dependency>
<groupId>com.svenruppert</groupId>
<artifactId>jSentinel-vaadin</artifactId>
<version>00.70.00</version>
</dependency>For a REST handler / servlet application:
<dependency>
<groupId>com.svenruppert</groupId>
<artifactId>jSentinel-rest</artifactId>
<version>00.70.00</version>
</dependency>For a plain-Java / desktop / CLI application:
<dependency>
<groupId>com.svenruppert</groupId>
<artifactId>jSentinel-standalone</artifactId>
<version>00.70.00</version>
</dependency>jSentinel-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.jsentinel.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.jsentinel.authorization.api.AuthorizationService.
@Retention(RUNTIME)
@JSentinelAnnotation(MyRoleAccessEvaluator.class)
public @interface VisibleFor {
MyRole[] value();
}Or use the generic annotations from jSentinel-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.jsentinel.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.jsentinel.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<JSentinelSubject> 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 jSentinel-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 jSentinel-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
jSentinel-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 — pick whichever of
the two paths fits your service shape and drive the login lifecycle
with StandaloneLoginFlow. There is no listener, no filter chain, no
navigation phase; both paths land in the same JSentinelEnforcer as
the Vaadin and REST adapters.
- Interface available → wrap once with
SecuredProxy.wrap(MyService.class, impl)(runtime / JDK proxy). - Concrete class, no interface → annotate with
@Secured, addjSentinel-processorto the<annotationProcessorPaths>, and instantiate the generated<Type>Securedsubclass (compile-time).
A complete runnable reference lives in demo-standalone: an
interactive library-borrowing CLI with three seeded users that
exercises both paths — LibraryService (interface) via
SecuredProxy, MemberDirectory (concrete class) via the
processor-generated MemberDirectorySecured.
public interface LibraryService {
@RequiresPermission("book:list")
List<String> listBooks();
@RequiresPermission("book:borrow")
void borrowBook(String title);
@RequiresRole("ADMIN")
void removeBook(String title);
}LibraryService secured =
SecuredProxy.wrap(LibraryService.class, new InMemoryLibraryService());
secured.listBooks(); // runs if the bound subject has book:list
secured.removeBook("x"); // throws AccessDeniedException for non-ADMINSecuredProxy.wrap(...) returns a JDK dynamic-proxy implementing the
interface. Every call scans the method (then the declaring class) for a
@JSentinelAnnotation-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:
SecuredProxy.requireAllowed(MyOps.class, "delete");
// throws AccessDeniedException if the calling subject is not allowedFor concrete classes (no interface) the annotation processor in
jSentinel-processor generates a sealed <Type>Secured subclass at
build time:
@Secured
public class MemberDirectory {
@RequiresPermission("member:list")
public List<String> listMembers() { /* … */ }
@RequiresAnyPermission({"member:add", "member:invite"})
public void addMember(String name, String email) { /* … */ }
}
// Compile produces MemberDirectorySecured automatically.
MemberDirectory members = new MemberDirectorySecured();
members.listMembers(); // JSentinelEnforcer.requirePermission("member:list") firstWire the processor as an <annotationProcessorPath> in the consuming
module's maven-compiler-plugin configuration — never as a regular
compile dependency. The generated class extends the original (so the
original must not be final).
StandaloneLoginFlow<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 JSentinelAuditService. flow.logout() clears the
SubjectStore for the current thread.
jSentinel-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 sealed decision hierarchies that adapters dispatch
on via switch:
| Type | Module | Variants |
|---|---|---|
AuthorizationDecision |
jSentinel-core |
Granted / Unauthenticated(reason) / Forbidden(reason) / StepUpRequired(reason, method) |
AccessDecision |
jSentinel-core |
Vaadin-oriented (legacy): Granted / Reroute(target, asForward) / RerouteToError(type, message) / RerouteWithParameter(s) |
JSentinelVersionStatus |
jSentinel-core/session |
Current(at) / Drifted(snapshot, current) — Phase 4c drift verdict |
JSentinelVersionEnforcer.EnforcementOutcome |
jSentinel-core/session |
Continue / SessionStale(status) — adapter-neutral request verdict for drift |
RateLimitDecision |
jSentinel-core/ratelimiting |
Allowed(eventsInWindow, limit, window) / Throttled(eventsInWindow, limit, window, retryAfter) |
LoginAttemptDecision, SessionDecision, SessionPolicyDecision, NavigationAccessDecision, LoginResult<U>, InitialAdminCreationResult |
various | further sealed verdicts for login throttling, session lifetime, navigation, standalone login, bootstrap |
Adapters map these to framework-specific behavior:
jSentinel-vaadin→ navigation: continue, reroute to login, reroute to step-up, or reroute to error.JSentinelVersionEnforcerListenerreroutes drifted sessions to the configured login view.jSentinel-rest→ HTTP status:200/handler,401,403, or401 + WWW-Authenticate: StepUp/SessionStale(RFC 7235).
JSentinelAnnotationScanner scans classes, methods, or any AnnotatedElement
for restriction annotations meta-annotated with @JSentinelAnnotation. Both
adapters use the same scanner.
Generic annotations (in jSentinel-core):
@RequiresRole({"ROLE_ADMIN"})→RequiresRoleEvaluator(any-of semantics; honoursRoleHierarchy)@RequiresPermission({"document:delete"})→RequiresPermissionEvaluator(all-of semantics)@RequiresAllPermissions({"a", "b"})→RequiresAllPermissionsEvaluator(explicit AND)@RequiresAnyPermission({"a", "b"})→RequiresAnyPermissionEvaluator(OR)@RequiresPolicy("doc.owner-or-admin")→RequiresPolicyEvaluator@ProtectedBy(...)→ProtectedByEvaluator@Secured(class-level, source-retention) → not an evaluator; trigger for the compile-time annotation processor injSentinel-processor
Project-specific annotations are encouraged for Vaadin views (e.g. @VisibleFor).
For non-navigation enforcement (CLI services, REST handlers, plain-Java
classes) the framework offers two paths, both routed through the same
JSentinelEnforcer in jSentinel-core:
| Path | Target | Wiring | When to choose |
|---|---|---|---|
| Runtime / JDK Dynamic Proxy | Java interface | SecuredProxy.wrap(MyService.class, impl) (in jSentinel-standalone) |
The service has a clean interface; you're happy paying a per-call reflection check. Works for callbacks / lambdas via SecuredProxy.requireAllowed(Class, methodName). |
| Compile-time / Annotation Processor | Concrete class annotated with @Secured |
<annotationProcessorPath> for jSentinel-processor; instantiate the generated <Type>Secured subclass |
The class has no interface, or you want a stable stacktrace / no per-call reflection. Method-security annotations on final, private or static methods raise compile errors. Underlying generator: com.svenruppert:proxybuilder:00.11.00 + proxybuilder-annotations:00.11.00. |
Both paths land in the same JSentinelEnforcer.require…(…) helpers,
so a permission rule applies identically regardless of which path
expressed it. demo-standalone exercises both side by side
(LibraryService via SecuredProxy.wrap, MemberDirectory via
MemberDirectorySecured).
| Type | Module / package | Purpose |
|---|---|---|
JSentinelServiceResolver |
jSentinel-core/.../authorization/api |
Central SPI cache. Strict accessors throw IllegalStateException; find…() returns Optional; set…(…) is a programmatic test seam. Covers Authentication / Authorization / Audit / Action / LoginAttempt / Session / PasswordHasher / Logout / RoleHierarchy / ResourceResolver / JSentinelVersionStore / SubjectIdResolver / Step-Up route. |
JSentinelEnforcer |
jSentinel-core/.../authorization/api |
Central enforcement entry point. Generic enforce(Method, Class) for the runtime/dynamic-proxy path; explicit requirePermission / requireAllPermissions / requireAnyPermission / requireRole / requireAnyRole / requirePolicy for the compile-time/annotation-processor path. Throws AccessDeniedException on deny. |
SecuredProxy |
jSentinel-standalone |
SecuredProxy.wrap(Interface, impl) returns a JDK dynamic proxy that routes every call through JSentinelEnforcer.enforce(method, declaringClass). requireAllowed(Class, methodName) is the single-shot variant for callbacks / lambdas. |
SecuredAnnotationProcessor |
jSentinel-processor |
Compile-time annotation processor. For each @Secured concrete class it emits <Type>Secured extends <Type> and rewrites every annotated method as JSentinelEnforcer.require…(…) + super.<method>(…). Built on com.svenruppert:proxybuilder:00.11.00 (+ proxybuilder-annotations:00.11.00). |
PermissionGuard |
jSentinel-core/.../authorization/api |
Stateless hasPermission / requirePermission (and role variants) on any HasPermissions/HasRoles. |
SubjectIdResolver<U> |
jSentinel-core/.../authorization/api |
Phase 4c-Followup. Maps a typed user to SubjectId (+ optional TenantId). Apps register to unlock Vaadin's automatic JSentinelVersion-snapshot capture in LoginView. |
| Type | Module / package | Purpose |
|---|---|---|
JSentinelAuditService + sealed AuditEvent (27 record variants) |
jSentinel-core/.../audit |
Typed publish/query audit pipeline. Variants: LoginSucceeded, LoginFailed, LogoutPerformed, AccessGranted, AccessDenied, ActionDenied, BruteForceLimitReached, SessionCreated, SessionExpired, SessionInvalidated, SessionStale, RoleAssigned, RoleRevoked, UserCreated, UserDeleted, BootstrapAdminCreated, BootstrapTokenRejected, PolicyEvaluated, StepUpChallenged, PasswordResetRequested, PasswordResetCompleted, EmailVerificationRequested, EmailVerified, ApiKeyUsed, ApiKeyDenied, TokenRotated, RateLimitExceeded. |
AuditEventStore + InMemoryAuditEventStore |
jSentinel-core/.../audit |
Persistence SPI for audit events (Phase 2). Eclipse-Store impl available. |
RingBufferAuditSink, LoggingAuditSink, CompositeAuditService, DefaultCompositeAuditService |
jSentinel-core/.../audit |
Default sinks; the RingBuffer backs the Vaadin /audit-route and the REST GET /api/audit endpoint. |
StoreBackedJSentinelAuditService |
jSentinel-core/.../audit |
JSentinelAuditService over AuditEventStore (Phase 4b). Tenant-scoped, swallows store failures so audit cannot break the security flow. |
| Type | Module / package | Purpose |
|---|---|---|
AuthenticationService<T,U> |
jSentinel-core/.../authentication |
SPI: credential validation + subject loading. |
PasswordHasher, PasswordHash, Pbkdf2PasswordHasher |
jSentinel-core/.../authentication |
Hash + verify + needsRehash (drift detection). |
LoginAttemptPolicy + InMemoryLoginAttemptPolicy + StoreBackedLoginAttemptPolicy |
jSentinel-core/.../bruteforce |
Login throttling. LoginAttemptDecision = Allowed | LockedOut(Duration, int). Store-backed variant uses LoginAttemptStore (Phase 4b). |
SessionPolicy<U> + TimeoutSessionPolicy |
jSentinel-core/.../session |
Idle/absolute lifetime checks. |
SessionStore, SessionRecord, JSentinelVersion, JSentinelVersionKey, JSentinelVersionStore |
jSentinel-core/.../session |
Persistent session records + monotonic per-subject security version (Phase 2 + 4a). SessionStore.findAll() lists every session for an admin view. |
JSentinelVersionCheck, sealed JSentinelVersionStatus, JSentinelVersionEnforcer, sealed EnforcementOutcome |
jSentinel-core/.../session |
Phase 4c drift detection. Adapter-neutral check + enforcer; publishes SessionStale audit on drift. |
LogoutService, SubjectClearingLogoutService, SubjectSessionRegistry + StoreBackedSubjectSessionRegistry |
jSentinel-core/.../logout |
logout(SubjectId, LogoutScope) SPI with multi-session logout via store-backed registry. Vaadin-side: VaadinLogoutService rotates HTTP session. |
StandaloneLoginFlow, LoginResult |
jSentinel-standalone |
CLI/Desktop login driver — consults policy, calls AuthenticationService, binds subject, audits. |
RememberMeTokenStore + StoreBackedRememberMeService |
jSentinel-core/.../authentication |
Phase 2c + 4b. Hash-only persistent-login tokens, tenant-scoped issue/validate/revoke. |
ApiKeyStore, ApiKeyRecord, ApiKeyAuthenticationService |
jSentinel-core/.../authentication |
Phase 2d + 7b. Hash-only API keys with scopes; ApiKeyAuthenticationService.authenticate returns the active record or empty with a ApiKeyDenied audit reason. |
RefreshTokenStore, RefreshTokenRecord, TokenService |
jSentinel-core/.../authentication |
Phase 2d + 7b. Rotating refresh tokens with replay defense via markReplaced; access tokens are returned to the caller without server-side persistence. Emits TokenRotated. |
| Type | Module / package | Purpose |
|---|---|---|
PasswordResetTokenStore, PasswordResetTokenRecord, PasswordResetService |
jSentinel-core/.../accountlifecycle |
Phase 2c + 7a. Single-use hash-only reset tokens; tenant-scoped request / validate / consume. |
EmailVerificationTokenStore, EmailVerificationTokenRecord, EmailVerificationService |
jSentinel-core/.../accountlifecycle |
Phase 2c + 7a. Same lifecycle as password reset, carries the verified email on the record. |
JSentinelNotificationSender + LoggingNotificationSender, JSentinelNotification + Kind enum |
jSentinel-core/.../accountlifecycle |
Phase 7a. Notification dispatcher — apps plug in mail / SMS / log transport. Default sender logs NOTIFY type=… lines. |
BootstrapStateStore + StoreBackedBootstrapStateService |
jSentinel-core/.../bootstrap |
Phase 2b + 4b. Tenant-scoped "is the system bootstrapped?" state with idempotent markCompleted. |
| Type | Module / package | Purpose |
|---|---|---|
RoleAssignmentStore, RoleAssignmentKey, StoreBackedRoleAuthorizationService<U> |
jSentinel-core/.../authorization/api/roles |
Phase 2b + 4b. Persistent role assignments + generic AuthorizationService<U> reading from the store. |
RoleHierarchy + StaticRoleHierarchy, NoopRoleHierarchy |
jSentinel-core/.../authorization/api/roles |
Role-inheritance SPI; honoured by RequiresRoleEvaluator and RolePermissionResolver. |
ActionAuthorizationService<U>, ActionPermission, StaticActionAuthorizationService |
jSentinel-core/.../action |
Stable SPI for isAllowed/requireAllowed action checks with ACTION_DENIED audit on denial. |
StaticRolePermissionMapping, RolePermissionResolver |
…/api/permissions |
Immutable role → permissions map with a builder; hierarchy-aware permission merge. |
SecuredOperationDescriptor, SecuredOperationRegistry, OperationVisibilityService |
…/api/operations |
Generic operation discovery with subject-aware filtering. |
| Type | Module / package | Purpose |
|---|---|---|
RateLimitStore, RateLimitKey |
jSentinel-core/.../ratelimiting |
Phase 2d. Event-based sliding-window persistence (records timestamps, the policy decides the window). |
RateLimitPolicy + InMemoryRateLimitPolicy, sealed RateLimitDecision |
jSentinel-core/.../ratelimiting |
Phase 7c. Pluggable per-scope rate-limit policy (separate from LoginAttemptPolicy). Sliding-window default; Throttled carries retryAfter for the HTTP header. |
| Type | Module / package | Purpose |
|---|---|---|
LoginView, LoginListener<U>, AuthorizationListener, SessionLifetimeListener, VaadinLogoutService |
jSentinel-vaadin |
Annotation-driven view protection + Vaadin session/lifecycle integration. LoginView.captureJSentinelVersionSnapshot() automatically records the Phase-4c snapshot when JSentinelVersionStore and SubjectIdResolver are wired. |
VaadinJSentinelVersionContext, JSentinelVersionEnforcerListener |
jSentinel-vaadin/session/vaadin |
Phase 4c. Per-VaadinSession snapshot carrier + @ListenerPriority(Integer.MAX_VALUE) BeforeEnterListener that reroutes drifted sessions to the configured login view. |
SecuredButton, SecuredRouterLink, SecuredMenuItem, SecuredVisibility, SecuredVisibilityMode, SessionManagementView |
jSentinel-vaadin/components |
Phase 8a/8b. Permission-aware UI components (HIDE vs DISABLE on denial) and a reusable session-management Composite. |
| Type | Module / package | Purpose |
|---|---|---|
RestHeaders, BearerTokenExtractor |
jSentinel-rest |
Case-insensitive header lookup and Bearer-token parsing. |
RestAuthenticationFilter, RestAuthorizationFilter |
jSentinel-rest |
401/403 filters; the authorization filter additionally consults SessionPolicy.evaluate(...) when subject-resolved metadata is available. |
BodyRestRequest |
jSentinel-rest |
Body-capable RestRequest. |
BootstrapRestStatusMapper |
jSentinel-rest |
InitialAdminCreationResult → HTTP status code + stable error code. |
RestJSentinelVersionContext, RestJSentinelVersionFilter |
jSentinel-rest |
Phase 4c. Drift filter that returns 401 + WWW-Authenticate: SessionStale (RFC 7235) on a stale session. |
OpenApiJSentinelMetadataGenerator, JSentinelRequirement, HandlerJSentinelMetadata |
jSentinel-rest/openapi |
Phase 8d. Extracts the five framework @Requires…-annotations from a handler class as a JSON-free structured tree apps merge into their own OpenAPI builder. |
| Type | Module / package | Purpose |
|---|---|---|
BootstrapConfigurationLoader, BootstrapStatus |
jSentinel-core/.../bootstrap |
Centralised sysprop+env+default loading with TTL parsing; leak-safe status snapshot. |
AdministratorAccountStore, BootstrapTokenStore, BootstrapTokenOutput, InitialAdminBootstrapService |
jSentinel-core/.../bootstrap |
First-run admin creation flow; modes PERSISTENT_FILE / TRANSIENT_CONSOLE / DISABLED. |
| Type | Module / package | Purpose |
|---|---|---|
TenantId, ResourceRef, ResourceAccessContext |
jSentinel-core/.../authorization/api/tenant + …/policy/resource |
Phase 1. Adapter-neutral tenant scope (TenantId.DEFAULT for single-tenant) + tenant-aware resource references. Every Phase-2 store key and Phase-4/7 service is tenant-scoped. |
Stable: role-based access, REST adapter contracts, JSentinelSubject,
AccessContext, AuthorizationDecision, scanner.
Experimental (marked with @ExperimentalJSentinelApi): permission-based
access types — PermissionBasedAccessEvaluator, PermissionName,
HasPermissions, PermissionAuthorizationService. The newer V00.70 stacks
ship under the same flag: persistence-store SPIs and Store*-backed
services (Phase 2/4/7), the JSentinelVersion drift-detection types,
the account-lifecycle services, the OpenAPI metadata generator,
the Phase-8 secured Vaadin components, TenantId / ResourceRef /
SubjectIdResolver. 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.
V00.70.00 is feature-complete — all eight phases of
Konzept-V00.70.00.md are merged. See
RELEASE-NOTES-00.70.00.md for the
full inventory + migration notes; the phase summary:
- Tenant + resource model (
TenantId,ResourceRef,ResourceAccessContext). - Persistence-store SPIs — 11 hash-only / single-use stores in
jSentinel-core. - Contract testkit + Eclipse-Store reference impls in their own modules.
- Store-backed services (
StoreBacked*) +JSentinelVersiondrift detection end-to-end in Vaadin + REST + standalone, with automatic snapshot capture inLoginView. - Policy API + method-security annotation processor.
- Authorization ergonomy —
RoleHierarchy,@RequiresAnyPermission,@RequiresAllPermissions, hierarchy-aware permission merge. - Account lifecycle (
PasswordResetService,EmailVerificationService), API-key & rotating refresh-token services, sliding-windowRateLimitPolicy. SecuredButton/SecuredRouterLink/SecuredMenuItem,SessionManagementView,OpenApiJSentinelMetadataGenerator.
Konzept-V00.75.00.md and Konzept-V00.80.00.md outline the next
layers. Demo glue for the new V00.70 building blocks (V00.70-style
session management, API-key parallel-to-Bearer in
demo-vaadin-rest-client, the reset-flow demo via
LoggingNotificationSender) plus PIT re-runs for
jSentinel-processor and jSentinel-persistence-eclipsestore are
the planned 00.71 follow-ups.
Konzept-V00.71.00.md introduces a fully new credential-security
stack under com.svenruppert.jsentinel.credential.password.*.
Prompts 001–025 are landed on develop; see
Implementierungsplan-V00.71.00.md §20 for the per-prompt status
table and docs/v00.71.00/prompts/README.md for the prompt
inventory.
Headlines:
- Phase 1a – JDK-only PBKDF2-HMAC-SHA-256 core with a self-describing
$pwh$v=1$…envelope, sealedCredentialVerificationResult/RehashDecision/ProviderVerificationResulttypes, generic perimeter failures backed by differentiated audit classifications, dummy verification + concurrency-boundedKdfExecutionLimiter, bootstrap and demo-rest wired through the newPasswordHashingService. - Phase 1b – Optional
jSentinel-crypto-bcmodule adds Argon2id, bcrypt and scrypt providers (BouncyCastle 1.78.1, no JCA mutation, per-algorithm parameter validators). The modern profile is opt-in and fails fast when requested without the module on the classpath. - Phase 2 –
SecretValue(AutoCloseable),PasswordInputPolicywith Unicode normalisation, post-KDF HMAC-SHA-256 pepper with rotation (PepperReference,PEPPER_KEY_ROTATEDrehash reason), policy-version / format-version rejection lists, operator-drivenPbkdf2ParameterCalibratorwith reproducible persisted profiles, four newAuditEventvariants flowing through a sink-failure-tolerantCredentialAuditPublisher. - Phase 3 – Persistence-neutral
CredentialStorewith compare-and-swap updates, eight-stateCredentialLifecycleServicewith deterministic transitions, atomicPasswordChangeServicewith explicit re-authentication, selector/verifierTokenDigestServiceand single-use dual-CASPasswordResetService.
The optional foreign-hash import (Epic T) stays deferred; the
experimental PasswordHasher / Pbkdf2PasswordHasher / PasswordHash
types remain in the tree only so the V00.70 callers
(StoreBackedRememberMeService, legacy accountlifecycle reset and
email-verification services) keep compiling. No compatibility shim
translates between the old pbkdf2$… and the new $pwh$v=1$…
envelope — that carve-out matches Konzept-V00.71.00 §1 and §7.
Phase 4 (abuse detection, context-aware policy, optional history,
operational metrics) and Phase 5 (HIBP opt-in, FIPS / supply-chain
docs, emergency playbooks, tenant policies, compliance traceability)
are still pending — prompts 026–035 in
docs/v00.71.00/prompts/.
EUPL 1.2