Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

### 0.7.2 (unreleased)

#### Features
* `jpro-auth-routing`: Added `RoutingAuth`, a high-level entry point for adding Google / OAuth2 / username-password login to a `RouteApp` (plus `dummy`/`defaultUser` logins for tests and desktop). See the module README and the `routing-auth` example.

#### Breaking
* `jpro-auth-routing`: Renamed the auth route transformers from `*Filter` to `*Transformer` (`AuthBasicFilter`, `AuthBasicOAuth2Filter`, `AuthRestrictionFilter`) and `AuthUIProvider.createFilter()` to `createTransformer()`.

### 0.7.1 (June 14, 2026)

#### Bugfixes
Expand Down
37 changes: 26 additions & 11 deletions jpro-auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,17 +167,32 @@ so there is no need to add it explicitly.
```

### Combined with the Routing API
Adding the `jpro-auth-routing` dependency lets you wire authentication into a `RouteApp` with a
single route filter. The module provides:
Adding the `jpro-auth-routing` dependency lets you wire authentication into a `RouteApp`.

- `UserSession` — stores the logged-in `User` in the JPro session.
- `AuthUIProviders` — factories for the login UI (`createGoogle`, `createOAuth2`, `createBasicProvider`, `combine`).
- `AuthBasicFilter` — route filter for username/password authentication.
- `AuthBasicOAuth2Filter` — route filter for OAuth2/OpenID authentication.
The easiest way is **`RoutingAuth`** — a single, central configuration that declares your login
methods and produces the login UI, the login page and the route transformers:

```java
RoutingAuth auth = RoutingAuth.config()
.google(CLIENT_ID, CLIENT_SECRET)
.usernamePassword(userManager)
.build(this);

return Route.empty()
.and(protectedRoutes.transform(auth.requireLogin()))
.transform(auth.install()); // serves /login + handles callbacks
```

See the [`jpro-auth-routing` README](routing/README.md) for the full guide (it also covers dummy /
desktop "local user" logins). The lower-level building blocks below are what `RoutingAuth` is built
on, for when you need finer control:

> The full reference for these classes lives in the [`jpro-auth-routing` README](routing/README.md).
- `UserSession` — stores the logged-in `User` in the JPro session.
- `AuthUIProviders` — factories for the login UI (`createGoogle`, `createOAuth2`, `createBasicProvider`, `dummy`, `combine`).
- `AuthBasicTransformer` — route transformer for username/password authentication.
- `AuthBasicOAuth2Transformer` — route transformer for OAuth2/OpenID authentication.

**Username/password** with `BasicAuthenticationProvider` and `AuthBasicFilter`:
**Username/password** with `BasicAuthenticationProvider` and `AuthBasicTransformer`:

```java
public class BasicLoginApp extends RouteApp {
Expand Down Expand Up @@ -205,7 +220,7 @@ public class BasicLoginApp extends RouteApp {
: Response.node(authUIProvider.createAuthenticationNode())))
.when(request -> userSession.isLoggedIn(), Route.empty()
.and(Route.get("/user/signed-in", request -> Response.node(new SignedInPage(this)))))
.transform(AuthBasicFilter.create(basicAuthProvider, credentials,
.transform(AuthBasicTransformer.create(basicAuthProvider, credentials,
user -> {
userSession.setUser(user);
return Response.redirect("/user/signed-in");
Expand All @@ -215,7 +230,7 @@ public class BasicLoginApp extends RouteApp {
}
```

**OAuth2 (Google)** with `AuthUIProviders.createGoogle` and `AuthBasicOAuth2Filter`:
**OAuth2 (Google)** with `AuthUIProviders.createGoogle` and `AuthBasicOAuth2Transformer`:

```java
public class GoogleLoginApp extends RouteApp {
Expand All @@ -242,7 +257,7 @@ public class GoogleLoginApp extends RouteApp {
.and(Route.get("/", request -> Response.node(uiProvider.createAuthenticationNode())))
.when(request -> userSession.isLoggedIn(), Route.empty()
.and(Route.get("/user/signed-in", request -> Response.node(new SignedInPage(this)))))
.transform(AuthBasicOAuth2Filter.create(googleAuthProvider, userSession,
.transform(AuthBasicOAuth2Transformer.create(googleAuthProvider, userSession,
user -> Response.redirect("/user/signed-in"),
error -> Response.node(new ErrorPage(error))));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import one.jpro.platform.auth.example.basic.page.LoginPage;
import one.jpro.platform.auth.example.basic.page.SignedInPage;
import one.jpro.platform.auth.example.oauth.OAuthApp;
import one.jpro.platform.auth.routing.AuthBasicFilter;
import one.jpro.platform.auth.routing.AuthBasicTransformer;
import one.jpro.platform.auth.routing.AuthUIProvider;
import one.jpro.platform.auth.routing.AuthUIProviders;
import one.jpro.platform.auth.routing.UserSession;
Expand Down Expand Up @@ -95,7 +95,7 @@ public Route createRoute() {
}))
.when(request -> isUserAuthenticated(), Route.empty()
.and(Route.get("/user/signed-in", request -> Response.node(new SignedInPage(this)))))
.transform(AuthBasicFilter.create(basicAuthProvider, credentials, user -> {
.transform(AuthBasicTransformer.create(basicAuthProvider, credentials, user -> {
userSession.setUser(user);
return Response.redirect("/user/signed-in");
}, error -> Response.node(new ErrorPage(error))))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import one.jpro.platform.auth.core.basic.LoginPane;
import one.jpro.platform.auth.core.basic.UsernamePasswordCredentials;
import one.jpro.platform.auth.core.basic.provider.BasicAuthenticationProvider;
import one.jpro.platform.auth.routing.AuthBasicFilter;
import one.jpro.platform.auth.routing.AuthBasicTransformer;
import one.jpro.platform.auth.routing.AuthUIProvider;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import one.jpro.platform.auth.example.login.page.LoginPage;
import one.jpro.platform.auth.example.login.page.SignedInPage;
import one.jpro.platform.auth.example.oauth.OAuthApp;
import one.jpro.platform.auth.routing.AuthBasicOAuth2Filter;
import one.jpro.platform.auth.routing.AuthBasicOAuth2Transformer;
import one.jpro.platform.auth.routing.AuthUIProviders;
import one.jpro.platform.auth.routing.UserSession;
import one.jpro.platform.routing.Response;
Expand Down Expand Up @@ -75,7 +75,7 @@ public Route createRoute() {
.and(Route.get("/", request -> Response.node(new LoginPage(googleAuthProvider, uiProvider))))
.when(request -> isUserAuthenticated(), Route.empty()
.and(Route.get("/user/signed-in", request -> Response.node(new SignedInPage(this, googleAuthProvider)))))
.transform(AuthBasicOAuth2Filter.create(googleAuthProvider, userSession,
.transform(AuthBasicOAuth2Transformer.create(googleAuthProvider, userSession,
user -> Response.redirect("/user/signed-in"),
error -> Response.node(new ErrorPage(error))))
.transform(DevTransformer.create());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import one.jpro.platform.auth.core.oauth2.provider.GoogleAuthenticationProvider;
import one.jpro.platform.auth.routing.AuthBasicOAuth2Filter;
import one.jpro.platform.auth.routing.AuthBasicOAuth2Transformer;
import one.jpro.platform.auth.routing.AuthUIProvider;

import java.util.Optional;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import one.jpro.platform.auth.core.AuthAPI;
import one.jpro.platform.auth.core.oauth2.provider.OpenIDAuthenticationProvider;
import one.jpro.platform.auth.example.oauth.page.*;
import one.jpro.platform.auth.routing.AuthBasicOAuth2Filter;
import one.jpro.platform.auth.routing.AuthBasicOAuth2Transformer;
import one.jpro.platform.auth.routing.UserSession;
import one.jpro.platform.routing.Transformer;
import one.jpro.platform.routing.Response;
Expand Down Expand Up @@ -105,7 +105,7 @@ public Route createRoute() {
* @return A {@link Transformer} object configured for OAuth2 authentication flow.
*/
private Transformer oauth2Filter(OpenIDAuthenticationProvider openIDAuthProvider) {
return AuthBasicOAuth2Filter.create(openIDAuthProvider, userSession, user -> {
return AuthBasicOAuth2Transformer.create(openIDAuthProvider, userSession, user -> {
setAuthProvider(openIDAuthProvider);
return Response.redirect(USER_CONSOLE_PATH);
}, error -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import javafx.scene.layout.VBox;
import one.jpro.platform.auth.core.oauth2.provider.OpenIDAuthenticationProvider;
import one.jpro.platform.auth.example.oauth.OAuthApp;
import one.jpro.platform.auth.routing.AuthBasicOAuth2Filter;
import one.jpro.platform.auth.routing.AuthBasicOAuth2Transformer;
import simplefx.experimental.parts.FXFuture;

import java.util.Optional;
Expand Down Expand Up @@ -74,7 +74,7 @@ public AuthProviderPage(OAuthApp loginApp, OpenIDAuthenticationProvider authProv

final var signInBox = loginApp.createButtonWithDescription(
"Sign in with the selected authentication provider.", "Sign In",
event -> AuthBasicOAuth2Filter.authorize(this, authProvider, authCredentials));
event -> AuthBasicOAuth2Transformer.authorize(this, authProvider, authCredentials));

final var discoveryBox = loginApp.createButtonWithDescription(
"The OpenID Connect Discovery provides a client with configuration details.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public Route createRoute() {
return Route.empty()
.and(Route.get("/", request -> Response.node(publicHome())))
.and(protectedRoutes)
.transform(auth.filter()); // serves /login + handles callbacks (no /login route needed)
.transform(auth.install()); // serves /login + handles callbacks (no /login route needed)
}

private Node publicHome() {
Expand Down
10 changes: 5 additions & 5 deletions jpro-auth/routing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ public class MyApp extends RouteApp {
.and(Route.get("/", r -> Response.node(new PublicHome()))) // public
.and(Route.get("/home", r -> Response.node(new Home(auth.getUser())))
.transform(auth.requireLogin())) // protected
.transform(auth.filter()); // serves /login + handles login callbacks
.transform(auth.install()); // serves /login + handles login callbacks
}
}
```

One method per login option, `requireLogin()` on what to protect, `filter()` on the whole route —
One method per login option, `requireLogin()` on what to protect, `install()` on the whole route —
and `/login` is served for you. That's the whole integration; everything below is just variations.

> Build inside `createRoute()` (OAuth2 needs the started `Stage`), and keep `auth` in a field so
Expand All @@ -40,7 +40,7 @@ and `/login` is served for you. That's the whole integration; everything below i

**Mix public and protected** — place guarded routes *after* public ones (`and` tries earlier routes first):
```java
Route.empty().and(publicRoutes).and(secret.transform(auth.requireLogin())).transform(auth.filter());
Route.empty().and(publicRoutes).and(secret.transform(auth.requireLogin())).transform(auth.install());
```

**Customize the login page** — `loginResponse` decides what `/login` shows; reuse the built-in buttons via `auth.loginScreen()`:
Expand Down Expand Up @@ -79,7 +79,7 @@ The resulting `RoutingAuth`:

| | |
|---|---|
| `filter()` | transform for the **whole** route — serves `/login` and handles login callbacks |
| `install()` | transform for the **whole** route — serves `/login` and handles login callbacks |
| `requireLogin()` | transform for a route/sub-route — protects it (redirects to the login page) |
| `loginScreen()` | the combined login UI node, for embedding in a custom login page |
| `getUser()` · `isLoggedIn()` · `logout()` | current user state |
Expand All @@ -89,7 +89,7 @@ The resulting `RoutingAuth`:

`RoutingAuth` is a thin facade over `UserSession`, `AuthUIProviders`
(`createGoogle` / `createOAuth2` / `createBasicProvider` / `dummy` / `combine`) and the filters
`AuthBasicFilter`, `AuthBasicOAuth2Filter`, `AuthRestrictionFilter`. Use those directly for finer
`AuthBasicTransformer`, `AuthBasicOAuth2Transformer`, `AuthRestrictionTransformer`. Use those directly for finer
control — see the Javadoc and the [`jpro-auth-core` README](../README.md).

## Try it
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
*
* @author Besmir Beqiri
*/
public interface AuthBasicOAuth2Filter {
public interface AuthBasicOAuth2Transformer {

/**
* Creates {@link Route} filter from a given {@link OAuth2AuthenticationProvider},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
*
* @author Besmir Beqiri
*/
public interface AuthBasicFilter {
public interface AuthBasicTransformer {

/**
* Creates {@link Route} filter from a given {@link BasicAuthenticationProvider},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
import one.jpro.platform.routing.Response;
import org.jetbrains.annotations.NotNull;

public class AuthRestrictionFilter {
public class AuthRestrictionTransformer {
/**
* This makes the whole UI only accessible to authenticated users.
*/
public static Transformer create(AuthUIProvider routingAuthenticationProvider,
@NotNull UserSession userSession) {
var filter = routingAuthenticationProvider.createFilter();
var filter = routingAuthenticationProvider.createTransformer();
Transformer result1 = (route) -> (request) -> {
if (userSession.getUser() != null) {
return route.apply(request);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,5 @@ public interface AuthUIProvider {
*
* @return a {@link Transformer} to apply to the application route
*/
Transformer createFilter();
Transformer createTransformer();
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,13 @@ public static AuthUIProvider createOAuth2(@NotNull OpenIDAuthenticationProvider
@Override
public Node createAuthenticationNode() {
var button = createButton.get();
button.setOnAction(event -> AuthBasicOAuth2Filter.authorize(button, openidAuthProvider));
button.setOnAction(event -> AuthBasicOAuth2Transformer.authorize(button, openidAuthProvider));
return button;
}

@Override
public Transformer createFilter() {
return AuthBasicOAuth2Filter.create(openidAuthProvider, userSession,
public Transformer createTransformer() {
return AuthBasicOAuth2Transformer.create(openidAuthProvider, userSession,
user -> Response.redirect("/"), // That's ok, let the app handle it.
err -> {throw new RuntimeException(err);});
}
Expand Down Expand Up @@ -112,7 +112,7 @@ public Node createAuthenticationNode() {
}

@Override
public Transformer createFilter() {
public Transformer createTransformer() {
return Transformer.empty();
}
};
Expand Down Expand Up @@ -153,7 +153,7 @@ public Node createAuthenticationNode() {
}

@Override
public Transformer createFilter() {
public Transformer createTransformer() {
return Transformer.empty();
}
};
Expand Down Expand Up @@ -191,9 +191,9 @@ public Node createAuthenticationNode() {
}

@Override
public Transformer createFilter() {
public Transformer createTransformer() {
return Arrays.stream(providers)
.map(AuthUIProvider::createFilter)
.map(AuthUIProvider::createTransformer)
.reduce(Transformer::compose).orElse(Transformer.empty());
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@
* .build(this);
*
* return Route.empty()
* .and(Route.get("/login", r -> Response.node(auth.loginScreen())))
* .and(Route.get("/home", r -> Response.node(new HomePage())))
* .transform(auth.requireLogin());
* .and(Route.get("/home", r -> Response.node(new HomePage())))
* .transform(auth.requireLogin()) // gate the route(s)
* .transform(auth.install()); // serve /login + handle callbacks
* }</pre>
*
* For a desktop local user, automated tests, or local development, swap the configuration:
Expand Down Expand Up @@ -90,13 +90,13 @@ public Node loginScreen() {

/**
* The transform that wires auth into the app: it serves the login page at the configured
* {@code loginPage} path and handles the login callbacks (e.g. OAuth2 redirects). Apply it
* to the whole route (outermost). With this in place you don't need to declare a
* {@code /login} route yourself — adding auth to an existing app is just this transform
* plus {@link #requireLogin()} on the parts you want protected.
* {@code loginUrl} and handles the login callbacks (e.g. OAuth2 redirects). Apply it to the
* whole route (outermost). With this in place you don't need to declare a {@code /login}
* route yourself — adding auth to an existing app is just this transform plus
* {@link #requireLogin()} on the parts you want protected.
*/
public Transformer filter() {
Transformer callbacks = combined.createFilter();
public Transformer install() {
Transformer callbacks = combined.createTransformer();
return route -> {
Route inner = callbacks.apply(route);
return request -> request.getPath().equals(loginUrl)
Expand All @@ -112,7 +112,7 @@ public Transformer filter() {
*
* <p>Apply it to the whole route to gate the entire app, or place a guarded sub-route
* <em>after</em> your public routes (so they match first) to mix public and protected pages.
* Apply {@link #filter()} to the whole route as well, so login callbacks are always handled.</p>
* Apply {@link #install()} to the whole route as well, so login callbacks are always handled.</p>
*/
public Transformer requireLogin() {
return route -> request -> userSession.isLoggedIn()
Expand Down Expand Up @@ -166,7 +166,7 @@ public Builder sessionName(String sessionName) {
}

/** The URL of the login page (default {@code "/login"}); {@link RoutingAuth#requireLogin()}
* redirects there and {@link RoutingAuth#filter()} serves it. */
* redirects there and {@link RoutingAuth#install()} serves it. */
public Builder loginUrl(String path) {
this.loginUrl = path;
return this;
Expand Down Expand Up @@ -277,13 +277,13 @@ private AuthUIProvider oauthUI(OpenIDAuthenticationProvider provider, UserSessio
@Override
public Node createAuthenticationNode() {
Button b = button.get();
b.setOnAction(e -> AuthBasicOAuth2Filter.authorize(b, provider));
b.setOnAction(e -> AuthBasicOAuth2Transformer.authorize(b, provider));
return b;
}

@Override
public Transformer createFilter() {
return AuthBasicOAuth2Filter.create(provider, session, user -> {
public Transformer createTransformer() {
return AuthBasicOAuth2Transformer.create(provider, session, user -> {
onLogin.accept(user);
return Response.redirect(loginRedirect);
}, onError);
Expand Down Expand Up @@ -320,7 +320,7 @@ public Node createAuthenticationNode() {
}

@Override
public Transformer createFilter() {
public Transformer createTransformer() {
return Transformer.empty();
}
};
Expand Down
Loading