From 9d39e24ccb7bf4ec093dcc6dd20e333dd0264f08 Mon Sep 17 00:00:00 2001 From: Zakaria El Orche Date: Thu, 10 Sep 2026 19:06:15 +0000 Subject: [PATCH] refactor(ext-auth): generic sessions, shared annotation wiring, rename to flash-ext-auth-oidc AuthMiddleware.install(ctx, config, source) now owns the annotation processor and the flash.auth.policy key, so a second credential source gets annotation-driven authorization without copying the wiring. The key is public: an extension that contributes middleware can order itself around authentication. OidcSession becomes Session in auth-core, carrying claims, an expiry and an opaque attribute map. OpenID Connect keeps its access, id and refresh tokens in that map under its own keys, so renewal stays its business and core has no OAuth2 vocabulary in it. isAccessTokenExpired() becomes isExpired(), with the 30s eager-renewal window it always had and now a test for it. flash-ext-oidc is renamed flash-ext-auth-oidc, matching cache-core/cache-caffeine and data-core/data-hibernate. --- .idea/encodings.xml | 4 +- README.md | 6 +- .../relism/flash/ext/auth/AuthMiddleware.java | 29 ++++++++++ .../flash/ext/auth/InMemorySessionStore.java | 20 +++++++ .../dev/relism/flash/ext/auth/Session.java | 53 +++++++++++++++++ .../relism/flash/ext/auth/SessionStore.java | 13 +++++ .../relism/flash/ext/auth/SessionTest.java | 58 +++++++++++++++++++ .../README.md | 4 +- .../pom.xml | 2 +- .../flash/ext/oidc/ClientAuthMethod.java | 0 .../flash/ext/oidc/DiscoveryClient.java | 0 .../dev/relism/flash/ext/oidc/JwtUtils.java | 0 .../relism/flash/ext/oidc/JwtValidator.java | 0 .../dev/relism/flash/ext/oidc/OidcConfig.java | 15 +++-- .../flash/ext/oidc/OidcCredentialSource.java | 57 +++++++++++------- .../relism/flash/ext/oidc/OidcExtension.java | 17 ++---- .../flash/ext/oidc/OidcProviderMetadata.java | 0 .../relism/flash/ext/oidc/OidcStateStore.java | 0 .../flash/ext/oidc/OidcTokenResponse.java | 0 .../ext/oidc/OidcValidationException.java | 0 .../dev/relism/flash/ext/oidc/PkceUtils.java | 0 .../relism/flash/ext/oidc/TokenClient.java | 0 .../ext/oidc/OidcCredentialSourceTest.java | 0 .../ext/oidc/OidcOpenApiInteropTest.java | 0 flash-extensions/flash-ext-mcp/docs/README.md | 4 +- .../flash-ext-mcp/docs/jackson-interop.md | 2 +- .../flash-ext-mcp/docs/keycloak.md | 2 +- .../flash-ext-mcp/docs/security.md | 20 +++---- flash-extensions/flash-ext-mcp/pom.xml | 2 +- .../relism/flash/ext/mcp/McpAuthPolicy.java | 2 +- .../dev/relism/flash/ext/mcp/McpConfig.java | 4 +- .../relism/flash/ext/mcp/McpExtension.java | 10 ++-- .../dev/relism/flash/ext/mcp/McpJson.java | 2 +- .../flash/ext/mcp/McpOidcIntegration.java | 8 +-- .../dev/relism/flash/ext/mcp/McpRegistry.java | 2 +- .../dev/relism/flash/ext/mcp/McpSecurity.java | 8 +-- .../flash/ext/mcp/McpTransportGuards.java | 2 +- .../flash/ext/mcp/FakeOidcProvider.java | 2 +- .../ext/mcp/McpExtensionSecurityTest.java | 2 +- .../ext/oidc/InMemoryOidcSessionStore.java | 20 ------- .../relism/flash/ext/oidc/OidcSession.java | 47 --------------- .../flash/ext/oidc/OidcSessionStore.java | 14 ----- flash-extensions/flash-ext-openapi/README.md | 2 +- flash-extensions/pom.xml | 2 +- pom.xml | 2 +- 45 files changed, 271 insertions(+), 166 deletions(-) create mode 100644 flash-extensions/flash-ext-auth-core/src/main/java/dev/relism/flash/ext/auth/InMemorySessionStore.java create mode 100644 flash-extensions/flash-ext-auth-core/src/main/java/dev/relism/flash/ext/auth/Session.java create mode 100644 flash-extensions/flash-ext-auth-core/src/main/java/dev/relism/flash/ext/auth/SessionStore.java create mode 100644 flash-extensions/flash-ext-auth-core/src/test/java/dev/relism/flash/ext/auth/SessionTest.java rename flash-extensions/{flash-ext-oidc => flash-ext-auth-oidc}/README.md (99%) rename flash-extensions/{flash-ext-oidc => flash-ext-auth-oidc}/pom.xml (96%) rename flash-extensions/{flash-ext-oidc => flash-ext-auth-oidc}/src/main/java/dev/relism/flash/ext/oidc/ClientAuthMethod.java (100%) rename flash-extensions/{flash-ext-oidc => flash-ext-auth-oidc}/src/main/java/dev/relism/flash/ext/oidc/DiscoveryClient.java (100%) rename flash-extensions/{flash-ext-oidc => flash-ext-auth-oidc}/src/main/java/dev/relism/flash/ext/oidc/JwtUtils.java (100%) rename flash-extensions/{flash-ext-oidc => flash-ext-auth-oidc}/src/main/java/dev/relism/flash/ext/oidc/JwtValidator.java (100%) rename flash-extensions/{flash-ext-oidc => flash-ext-auth-oidc}/src/main/java/dev/relism/flash/ext/oidc/OidcConfig.java (96%) rename flash-extensions/{flash-ext-oidc => flash-ext-auth-oidc}/src/main/java/dev/relism/flash/ext/oidc/OidcCredentialSource.java (85%) rename flash-extensions/{flash-ext-oidc => flash-ext-auth-oidc}/src/main/java/dev/relism/flash/ext/oidc/OidcExtension.java (95%) rename flash-extensions/{flash-ext-oidc => flash-ext-auth-oidc}/src/main/java/dev/relism/flash/ext/oidc/OidcProviderMetadata.java (100%) rename flash-extensions/{flash-ext-oidc => flash-ext-auth-oidc}/src/main/java/dev/relism/flash/ext/oidc/OidcStateStore.java (100%) rename flash-extensions/{flash-ext-oidc => flash-ext-auth-oidc}/src/main/java/dev/relism/flash/ext/oidc/OidcTokenResponse.java (100%) rename flash-extensions/{flash-ext-oidc => flash-ext-auth-oidc}/src/main/java/dev/relism/flash/ext/oidc/OidcValidationException.java (100%) rename flash-extensions/{flash-ext-oidc => flash-ext-auth-oidc}/src/main/java/dev/relism/flash/ext/oidc/PkceUtils.java (100%) rename flash-extensions/{flash-ext-oidc => flash-ext-auth-oidc}/src/main/java/dev/relism/flash/ext/oidc/TokenClient.java (100%) rename flash-extensions/{flash-ext-oidc => flash-ext-auth-oidc}/src/test/java/dev/relism/flash/ext/oidc/OidcCredentialSourceTest.java (100%) rename flash-extensions/{flash-ext-oidc => flash-ext-auth-oidc}/src/test/java/dev/relism/flash/ext/oidc/OidcOpenApiInteropTest.java (100%) delete mode 100644 flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/InMemoryOidcSessionStore.java delete mode 100644 flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcSession.java delete mode 100644 flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcSessionStore.java diff --git a/.idea/encodings.xml b/.idea/encodings.xml index aa0097f..cf88791 100644 --- a/.idea/encodings.xml +++ b/.idea/encodings.xml @@ -17,8 +17,8 @@ - - + + diff --git a/README.md b/README.md index 10ac1e4..89fc701 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ a zero-allocation FSM router, bounded protocol state, and one shared request/res | `flash-testing` | JUnit 5 harness — boot an app on an ephemeral port, fake its services, assert on responses | | `flash-extensions/flash-ext-jackson` | Jackson JSON integration | | `flash-extensions/flash-ext-openapi` | OpenAPI 3.0 spec + Swagger UI | -| `flash-extensions/flash-ext-oidc` | OIDC Authorization Code + PKCE flow | -| `flash-extensions/flash-ext-mcp` | MCP (Model Context Protocol) server — Streamable HTTP, optional OAuth2 via flash-ext-oidc | +| `flash-extensions/flash-ext-auth-oidc` | OIDC Authorization Code + PKCE flow | +| `flash-extensions/flash-ext-mcp` | MCP (Model Context Protocol) server — Streamable HTTP, optional OAuth2 via flash-ext-auth-oidc | | `flash-extensions/flash-ext-view-core` | Minimal shared SSR runtime primitives | | `flash-extensions/flash-ext-view-jte` | Opinionated jte SSR extension | | `flash-extensions/flash-ext-view-thymeleaf` | Opinionated Thymeleaf SSR extension | @@ -146,7 +146,7 @@ FlashApp.create(8080) See extension-specific READMEs for full details: - [`flash-ext-jackson`](flash-extensions/flash-ext-jackson/README.md) - [`flash-ext-openapi`](flash-extensions/flash-ext-openapi/README.md) -- [`flash-ext-oidc`](flash-extensions/flash-ext-oidc/README.md) +- [`flash-ext-auth-oidc`](flash-extensions/flash-ext-auth-oidc/README.md) - [`flash-ext-mcp`](flash-extensions/flash-ext-mcp/docs/README.md) - [`flash-ext-view-jte`](flash-extensions/flash-ext-view-jte/README.md) - [`flash-ext-view-thymeleaf`](flash-extensions/flash-ext-view-thymeleaf/README.md) diff --git a/flash-extensions/flash-ext-auth-core/src/main/java/dev/relism/flash/ext/auth/AuthMiddleware.java b/flash-extensions/flash-ext-auth-core/src/main/java/dev/relism/flash/ext/auth/AuthMiddleware.java index 30c7a4d..2c4a104 100644 --- a/flash-extensions/flash-ext-auth-core/src/main/java/dev/relism/flash/ext/auth/AuthMiddleware.java +++ b/flash-extensions/flash-ext-auth-core/src/main/java/dev/relism/flash/ext/auth/AuthMiddleware.java @@ -5,6 +5,8 @@ import dev.relism.flash.extension.FlashContext; import dev.relism.flash.models.Request; import dev.relism.flash.models.Response; import dev.relism.flash.routing.Middleware; +import dev.relism.flash.routing.MiddlewareKey; +import dev.relism.flash.routing.MiddlewareNode; import java.util.ArrayList; import java.util.List; @@ -28,6 +30,13 @@ import java.util.Map; */ public class AuthMiddleware { + /** + * Boot-time identity of the node annotation-driven authorization mounts under. Public so an + * extension that contributes its own middleware can order itself around authentication — + * {@code MiddlewareNode.of(...).afterIfPresent(AuthMiddleware.POLICY)}. + */ + public static final MiddlewareKey POLICY = MiddlewareKey.of("flash.auth.policy"); + private final AuthConfig config; private final CredentialSource source; private final String[] roleClaimPathParts; @@ -40,6 +49,26 @@ public class AuthMiddleware { this.scopeClaimPathParts = splitClaimPaths(config.scopeClaimPaths()); } + /** + * Builds the middleware for {@code source}, publishes it in the context and registers the + * annotation processor that mounts {@link Authenticated}, {@link RolesAllowed} and + * {@link ScopesAllowed} on scanned handlers. + * + *

Every extension that contributes a {@link CredentialSource} calls this rather than + * repeating the wiring — the processor and the {@link #POLICY} key belong to one place. + */ + public static AuthMiddleware install(FlashContext ctx, AuthConfig config, CredentialSource source) { + AuthMiddleware middleware = new AuthMiddleware(config, source); + ctx.provide(AuthMiddleware.class, middleware); + ctx.addAnnotationProcessor(handlerClass -> { + AuthPolicy policy = AuthPolicy.compileFromAnnotations(handlerClass); + return policy != null + ? List.of(MiddlewareNode.of(POLICY, middleware.authorize(policy))) + : List.of(); + }); + return middleware; + } + // -- Public API ----------------------------------------------------------- /** The single configured claim path used by every transport for role checks. */ diff --git a/flash-extensions/flash-ext-auth-core/src/main/java/dev/relism/flash/ext/auth/InMemorySessionStore.java b/flash-extensions/flash-ext-auth-core/src/main/java/dev/relism/flash/ext/auth/InMemorySessionStore.java new file mode 100644 index 0000000..eff0fa1 --- /dev/null +++ b/flash-extensions/flash-ext-auth-core/src/main/java/dev/relism/flash/ext/auth/InMemorySessionStore.java @@ -0,0 +1,20 @@ +package dev.relism.flash.ext.auth; + +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Thread-safe in-memory {@link SessionStore}. + * + *

Sessions are lost on restart and not shared across instances. For + * production deployments with multiple nodes or restart-persistence requirements, + * supply another implementation to whichever {@link CredentialSource} owns the session. + */ +public final class InMemorySessionStore implements SessionStore { + + private final ConcurrentHashMap store = new ConcurrentHashMap<>(); + + @Override public void save(Session s) { store.put(s.id(), s); } + @Override public Optional find(String id) { return Optional.ofNullable(store.get(id)); } + @Override public void delete(String id) { store.remove(id); } +} diff --git a/flash-extensions/flash-ext-auth-core/src/main/java/dev/relism/flash/ext/auth/Session.java b/flash-extensions/flash-ext-auth-core/src/main/java/dev/relism/flash/ext/auth/Session.java new file mode 100644 index 0000000..0a77824 --- /dev/null +++ b/flash-extensions/flash-ext-auth-core/src/main/java/dev/relism/flash/ext/auth/Session.java @@ -0,0 +1,53 @@ +package dev.relism.flash.ext.auth; + +import java.time.Instant; +import java.util.Map; + +/** + * A signed-in caller's server-side session — saved in a {@link SessionStore} and looked up by a + * cookie on every request. + * + *

Immutable: renewing one produces a new instance that replaces the old under the same + * {@link #id()}. + * + *

{@link #attributes()} is whatever the {@link CredentialSource} needs to keep alongside the + * claims and nothing this module interprets — OpenID Connect stores its access, id and refresh + * tokens there so that renewal is its business rather than core's. + */ +public final class Session { + + /** Renew this far before the real expiry, so a session cannot lapse mid-request. */ + private static final long EAGER_RENEWAL_SECONDS = 30; + + private final String id; + private final Map claims; + private final Instant expiresAt; + private final Map attributes; + + public Session(String id, Map claims, Instant expiresAt, + Map attributes) { + this.id = id; + this.claims = Map.copyOf(claims); + this.expiresAt = expiresAt; + this.attributes = attributes == null ? Map.of() : Map.copyOf(attributes); + } + + /** True once the session is within {@value #EAGER_RENEWAL_SECONDS} seconds of expiring. */ + public boolean isExpired() { + return Instant.now().isAfter(expiresAt.minusSeconds(EAGER_RENEWAL_SECONDS)); + } + + /** One attribute, or {@code null} when the source never stored it. */ + public Object attribute(String key) { return attributes.get(key); } + + /** One attribute as a String, or {@code null}. */ + public String attributeAsString(String key) { + Object v = attributes.get(key); + return v != null ? v.toString() : null; + } + + public String id() { return id; } + public Map claims() { return claims; } + public Instant expiresAt() { return expiresAt; } + public Map attributes() { return attributes; } +} diff --git a/flash-extensions/flash-ext-auth-core/src/main/java/dev/relism/flash/ext/auth/SessionStore.java b/flash-extensions/flash-ext-auth-core/src/main/java/dev/relism/flash/ext/auth/SessionStore.java new file mode 100644 index 0000000..2e0a058 --- /dev/null +++ b/flash-extensions/flash-ext-auth-core/src/main/java/dev/relism/flash/ext/auth/SessionStore.java @@ -0,0 +1,13 @@ +package dev.relism.flash.ext.auth; + +import java.util.Optional; + +/** + * Where {@link Session}s live between requests. {@link InMemorySessionStore} is the default; + * supply another for Redis, JDBC, or anything that survives a restart or spans instances. + */ +public interface SessionStore { + void save(Session session); + Optional find(String sessionId); + void delete(String sessionId); +} diff --git a/flash-extensions/flash-ext-auth-core/src/test/java/dev/relism/flash/ext/auth/SessionTest.java b/flash-extensions/flash-ext-auth-core/src/test/java/dev/relism/flash/ext/auth/SessionTest.java new file mode 100644 index 0000000..6aef288 --- /dev/null +++ b/flash-extensions/flash-ext-auth-core/src/test/java/dev/relism/flash/ext/auth/SessionTest.java @@ -0,0 +1,58 @@ +package dev.relism.flash.ext.auth; + +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * The two things a session has to get right: it reports itself expired early enough that it + * cannot lapse midway through a request, and it hands back what a credential source stored on it + * without interpreting any of it. + */ +class SessionTest { + + private static Session at(Instant expiry, Map attributes) { + return new Session("s1", Map.of("sub", "u1"), expiry, attributes); + } + + @Test + void aSessionIsExpiredWellBeforeItsDeadline() { + // The eager window is what stops a session from lapsing between the check and the handler. + assertFalse(at(Instant.now().plusSeconds(120), Map.of()).isExpired()); + assertTrue(at(Instant.now().plusSeconds(10), Map.of()).isExpired()); + assertTrue(at(Instant.now().minusSeconds(1), Map.of()).isExpired()); + } + + @Test + void attributesAreReturnedUninterpreted() { + Session session = at(Instant.now().plusSeconds(60), Map.of("oidc.id_token", "abc", "n", 7)); + assertEquals("abc", session.attributeAsString("oidc.id_token")); + assertEquals("7", session.attributeAsString("n")); + assertEquals(7, session.attribute("n")); + assertNull(session.attributeAsString("absent")); + } + + @Test + void aSessionWithoutAttributesIsUsableRatherThanNull() { + assertNull(at(Instant.now().plusSeconds(60), null).attributeAsString("anything")); + } + + @Test + void claimsAndAttributesAreCopiedAndImmutable() { + Map mutable = new HashMap<>(Map.of("k", "v")); + Session session = at(Instant.now().plusSeconds(60), mutable); + mutable.put("k", "changed"); + + assertEquals("v", session.attributeAsString("k")); + assertThrows(UnsupportedOperationException.class, () -> session.attributes().put("x", "y")); + assertThrows(UnsupportedOperationException.class, () -> session.claims().put("x", "y")); + } +} diff --git a/flash-extensions/flash-ext-oidc/README.md b/flash-extensions/flash-ext-auth-oidc/README.md similarity index 99% rename from flash-extensions/flash-ext-oidc/README.md rename to flash-extensions/flash-ext-auth-oidc/README.md index 8382dce..5fccedf 100644 --- a/flash-extensions/flash-ext-oidc/README.md +++ b/flash-extensions/flash-ext-auth-oidc/README.md @@ -1,4 +1,4 @@ -# flash-ext-oidc +# flash-ext-auth-oidc Full OIDC Authorization Code + PKCE flow for the Flash HTTP server. Supports Keycloak, Authelia, Auth0, Google, and any RFC 8414-compliant provider. @@ -25,7 +25,7 @@ hot-path model (middleware compiled at mount time, no heavy runtime work). ```xml dev.relism - flash-ext-oidc + flash-ext-auth-oidc 1.0-SNAPSHOT ``` diff --git a/flash-extensions/flash-ext-oidc/pom.xml b/flash-extensions/flash-ext-auth-oidc/pom.xml similarity index 96% rename from flash-extensions/flash-ext-oidc/pom.xml rename to flash-extensions/flash-ext-auth-oidc/pom.xml index 9967473..43aa29a 100644 --- a/flash-extensions/flash-ext-oidc/pom.xml +++ b/flash-extensions/flash-ext-auth-oidc/pom.xml @@ -10,7 +10,7 @@ 2.1.0-SNAPSHOT - flash-ext-oidc + flash-ext-auth-oidc diff --git a/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/ClientAuthMethod.java b/flash-extensions/flash-ext-auth-oidc/src/main/java/dev/relism/flash/ext/oidc/ClientAuthMethod.java similarity index 100% rename from flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/ClientAuthMethod.java rename to flash-extensions/flash-ext-auth-oidc/src/main/java/dev/relism/flash/ext/oidc/ClientAuthMethod.java diff --git a/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/DiscoveryClient.java b/flash-extensions/flash-ext-auth-oidc/src/main/java/dev/relism/flash/ext/oidc/DiscoveryClient.java similarity index 100% rename from flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/DiscoveryClient.java rename to flash-extensions/flash-ext-auth-oidc/src/main/java/dev/relism/flash/ext/oidc/DiscoveryClient.java diff --git a/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/JwtUtils.java b/flash-extensions/flash-ext-auth-oidc/src/main/java/dev/relism/flash/ext/oidc/JwtUtils.java similarity index 100% rename from flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/JwtUtils.java rename to flash-extensions/flash-ext-auth-oidc/src/main/java/dev/relism/flash/ext/oidc/JwtUtils.java diff --git a/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/JwtValidator.java b/flash-extensions/flash-ext-auth-oidc/src/main/java/dev/relism/flash/ext/oidc/JwtValidator.java similarity index 100% rename from flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/JwtValidator.java rename to flash-extensions/flash-ext-auth-oidc/src/main/java/dev/relism/flash/ext/oidc/JwtValidator.java diff --git a/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcConfig.java b/flash-extensions/flash-ext-auth-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcConfig.java similarity index 96% rename from flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcConfig.java rename to flash-extensions/flash-ext-auth-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcConfig.java index 837af03..997b71a 100644 --- a/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcConfig.java +++ b/flash-extensions/flash-ext-auth-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcConfig.java @@ -1,5 +1,8 @@ package dev.relism.flash.ext.oidc; +import dev.relism.flash.ext.auth.InMemorySessionStore; +import dev.relism.flash.ext.auth.SessionStore; + /** * Full OIDC client configuration. Build via * {@link #builder(String, String, String, String)} or {@link #fromEnv()}. @@ -50,7 +53,7 @@ public final class OidcConfig { private final String scopeClaimPaths; private final String algorithm; private final String postLogoutRedirectUri; - private final OidcSessionStore sessionStore; + private final SessionStore sessionStore; private final boolean insecureTls; private final ClientAuthMethod clientAuthMethod; private final String schemeName; @@ -68,7 +71,7 @@ public final class OidcConfig { this.algorithm = b.algorithm; this.postLogoutRedirectUri = b.postLogoutRedirectUri; this.sessionStore = b.sessionStore != null ? b.sessionStore - : new InMemoryOidcSessionStore(); + : new InMemorySessionStore(); this.insecureTls = b.insecureTls; this.clientAuthMethod = b.clientAuthMethod; this.schemeName = b.schemeName != null ? b.schemeName : deriveScheme(this.issuer); @@ -88,7 +91,7 @@ public final class OidcConfig { public String scopeClaimPaths() { return scopeClaimPaths; } public String algorithm() { return algorithm; } public String postLogoutRedirectUri() { return postLogoutRedirectUri; } - public OidcSessionStore sessionStore() { return sessionStore; } + public SessionStore sessionStore() { return sessionStore; } /** If {@code true}, TLS certificate validation is skipped. Never use in production. */ public boolean insecureTls() { return insecureTls; } public ClientAuthMethod clientAuthMethod() { return clientAuthMethod; } @@ -190,7 +193,7 @@ public final class OidcConfig { private String scopeClaimPaths = "scope,scp"; private String algorithm = "RS256"; private String postLogoutRedirectUri = "/"; - private OidcSessionStore sessionStore; + private SessionStore sessionStore; private boolean insecureTls = false; private ClientAuthMethod clientAuthMethod = ClientAuthMethod.POST; private String schemeName = null; @@ -218,8 +221,8 @@ public final class OidcConfig { public Builder algorithm(String algorithm) { this.algorithm = algorithm; return this; } /** Where to redirect after logout (default: {@code /}). */ public Builder postLogoutRedirectUri(String uri) { this.postLogoutRedirectUri = uri; return this; } - /** Custom session store (default: {@link InMemoryOidcSessionStore}). */ - public Builder sessionStore(OidcSessionStore store) { this.sessionStore = store; return this; } + /** Custom session store (default: {@link InMemorySessionStore}). */ + public Builder sessionStore(SessionStore store) { this.sessionStore = store; return this; } /** * Disables TLS certificate verification for all HTTP calls made by this extension. * Only use in development with self-signed certificates — never in production. diff --git a/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcCredentialSource.java b/flash-extensions/flash-ext-auth-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcCredentialSource.java similarity index 85% rename from flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcCredentialSource.java rename to flash-extensions/flash-ext-auth-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcCredentialSource.java index 36f6adf..cd95e63 100644 --- a/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcCredentialSource.java +++ b/flash-extensions/flash-ext-auth-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcCredentialSource.java @@ -2,6 +2,7 @@ package dev.relism.flash.ext.oidc; import dev.relism.flash.exceptions.HttpException; import dev.relism.flash.ext.auth.CredentialSource; +import dev.relism.flash.ext.auth.Session; import dev.relism.flash.models.Response; import dev.relism.flash.models.Request; @@ -20,7 +21,7 @@ import java.util.Optional; *

Resolution order on each request: *

    *
  1. {@code Authorization: Bearer ...} header — validated via JWKS ({@link JwtValidator}).
  2. - *
  3. {@code oidc_session} cookie — looked up in {@link OidcSessionStore}; transparently + *
  4. {@code oidc_session} cookie — looked up in {@link dev.relism.flash.ext.auth.SessionStore}; transparently * refreshed if the access token is expired.
  5. *
  6. Browser clients (no {@code Accept: application/json}) → redirect to * {@code {routePrefix}/login?redirect={path}}.
  7. @@ -31,6 +32,24 @@ public final class OidcCredentialSource implements CredentialSource { private static final String BEARER = "Bearer"; + /** + * Keys this source stores its OAuth2 tokens under in {@link Session#attributes()}. Core keeps + * the session; the tokens inside it are nobody else's business. + */ + static final String ACCESS_TOKEN = "oidc.access_token"; + static final String ID_TOKEN = "oidc.id_token"; + static final String REFRESH_TOKEN = "oidc.refresh_token"; + + /** The one place an OIDC session is built, so its attribute keys stay in one place too. */ + static Session newSession(String id, String accessToken, String idToken, String refreshToken, + Instant expiresAt, Map claims) { + Map attributes = new HashMap<>(3); + if (accessToken != null) attributes.put(ACCESS_TOKEN, accessToken); + if (idToken != null) attributes.put(ID_TOKEN, idToken); + if (refreshToken != null) attributes.put(REFRESH_TOKEN, refreshToken); + return new Session(id, claims, expiresAt, attributes); + } + private final JwtValidator validator; private final OidcConfig config; private final OidcProviderMetadata meta; @@ -108,14 +127,14 @@ public final class OidcCredentialSource implements CredentialSource { String sessionId = cookieValue(req, "oidc_session"); if (sessionId != null) { - Optional found = config.sessionStore().find(sessionId); + Optional found = config.sessionStore().find(sessionId); if (found.isPresent()) { - OidcSession session = found.get(); - if (!session.isAccessTokenExpired()) + Session session = found.get(); + if (!session.isExpired()) return session.claims(); - if (session.refreshToken() != null) { + if (session.attributeAsString(REFRESH_TOKEN) != null) { try { - OidcSession refreshed = doRefresh(session); + Session refreshed = doRefresh(session); config.sessionStore().save(refreshed); return refreshed.claims(); } catch (Exception ignored) { } @@ -149,17 +168,17 @@ public final class OidcCredentialSource implements CredentialSource { // 2. Session cookie String sessionId = cookieValue(req, "oidc_session"); if (sessionId != null) { - Optional found = config.sessionStore().find(sessionId); + Optional found = config.sessionStore().find(sessionId); if (found.isPresent()) { - OidcSession session = found.get(); + Session session = found.get(); - if (!session.isAccessTokenExpired()) + if (!session.isExpired()) return session.claims(); // Access token expired — try silent refresh - if (session.refreshToken() != null) { + if (session.attributeAsString(REFRESH_TOKEN) != null) { try { - OidcSession refreshed = doRefresh(session); + Session refreshed = doRefresh(session); config.sessionStore().save(refreshed); return refreshed.claims(); } catch (Exception ignored) { @@ -184,19 +203,17 @@ public final class OidcCredentialSource implements CredentialSource { return null; } - private OidcSession doRefresh(OidcSession old) throws Exception { + private Session doRefresh(Session old) throws Exception { OidcTokenResponse tokens = tokenClient.refresh( - meta.tokenEndpoint(), old.refreshToken()); + meta.tokenEndpoint(), old.attributeAsString(REFRESH_TOKEN)); - Map claims = mergeRefreshedClaims(tokens, old); - - return new OidcSession( + return newSession( old.id(), tokens.accessToken(), - tokens.idToken() != null ? tokens.idToken() : old.idToken(), - tokens.refreshToken() != null ? tokens.refreshToken() : old.refreshToken(), + tokens.idToken() != null ? tokens.idToken() : old.attributeAsString(ID_TOKEN), + tokens.refreshToken() != null ? tokens.refreshToken() : old.attributeAsString(REFRESH_TOKEN), Instant.now().plusSeconds(tokens.expiresIn()), - claims + mergeRefreshedClaims(tokens, old) ); } @@ -280,7 +297,7 @@ public final class OidcCredentialSource implements CredentialSource { return out.toString(); } - private static Map mergeRefreshedClaims(OidcTokenResponse tokens, OidcSession old) { + private static Map mergeRefreshedClaims(OidcTokenResponse tokens, Session old) { Map merged = new HashMap<>(); // Fall back to old claims first, then overlay fresh token claims merged.putAll(old.claims()); diff --git a/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcExtension.java b/flash-extensions/flash-ext-auth-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcExtension.java similarity index 95% rename from flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcExtension.java rename to flash-extensions/flash-ext-auth-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcExtension.java index 133c152..65605f0 100644 --- a/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcExtension.java +++ b/flash-extensions/flash-ext-auth-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcExtension.java @@ -5,6 +5,7 @@ import dev.relism.flash.ext.openapi.OpenApiContributorRegistry; import dev.relism.flash.ext.openapi.OpenApiOperationContribution; import dev.relism.flash.ext.openapi.OpenApiResponseContribution; import dev.relism.flash.ext.auth.AuthConfig; +import dev.relism.flash.ext.auth.Session; import dev.relism.flash.ext.auth.AuthMiddleware; import dev.relism.flash.ext.auth.AuthPolicy; import dev.relism.flash.ext.auth.Authenticated; @@ -13,8 +14,6 @@ import dev.relism.flash.ext.auth.ScopesAllowed; import dev.relism.flash.extension.FlashContext; import dev.relism.flash.extension.FlashExtension; import dev.relism.flash.extension.FlashRegistrar; -import dev.relism.flash.routing.MiddlewareKey; -import dev.relism.flash.routing.MiddlewareNode; import dev.relism.flash.models.Request; import javax.net.ssl.SSLContext; @@ -63,7 +62,6 @@ import java.util.*; * } */ public class OidcExtension implements FlashExtension { - private static final MiddlewareKey POLICY = MiddlewareKey.of("flash.auth.policy"); private final OidcConfig config; @@ -96,19 +94,14 @@ public class OidcExtension implements FlashExtension { stateStore = new OidcStateStore(); tokenClient = new TokenClient(http, config); source = new OidcCredentialSource(validator, config, meta, tokenClient); - authMw = new AuthMiddleware(AuthConfig.builder() + authMw = AuthMiddleware.install(ctx, AuthConfig.builder() .rolesClaimPath(config.rolesClaimPath()) .scopeClaimPaths(config.scopeClaimPaths()) .build(), source); - ctx.provide(AuthMiddleware.class, authMw); ctx.provide(OidcCredentialSource.class, source); ctx.provide(JwtValidator.class, validator); - ctx.addAnnotationProcessor(handlerClass -> { - AuthPolicy policy = AuthPolicy.compileFromAnnotations(handlerClass); - return policy != null ? List.of(MiddlewareNode.of(POLICY, authMw.authorize(policy))) : List.of(); - }); ctx.onReady(() -> registerRoutes(app, ctx)); } @@ -177,7 +170,7 @@ public class OidcExtension implements FlashExtension { } Map claims = mergeClaims(tokens); - OidcSession session = new OidcSession( + Session session = OidcCredentialSource.newSession( UUID.randomUUID().toString(), tokens.accessToken(), tokens.idToken(), tokens.refreshToken(), Instant.now().plusSeconds(tokens.expiresIn()), claims); @@ -195,8 +188,8 @@ public class OidcExtension implements FlashExtension { String idTokenHint = null; if (sessionId != null) { - OidcSession session = config.sessionStore().find(sessionId).orElse(null); - if (session != null) idTokenHint = session.idToken(); + Session session = config.sessionStore().find(sessionId).orElse(null); + if (session != null) idTokenHint = session.attributeAsString(OidcCredentialSource.ID_TOKEN); config.sessionStore().delete(sessionId); } diff --git a/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcProviderMetadata.java b/flash-extensions/flash-ext-auth-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcProviderMetadata.java similarity index 100% rename from flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcProviderMetadata.java rename to flash-extensions/flash-ext-auth-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcProviderMetadata.java diff --git a/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcStateStore.java b/flash-extensions/flash-ext-auth-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcStateStore.java similarity index 100% rename from flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcStateStore.java rename to flash-extensions/flash-ext-auth-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcStateStore.java diff --git a/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcTokenResponse.java b/flash-extensions/flash-ext-auth-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcTokenResponse.java similarity index 100% rename from flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcTokenResponse.java rename to flash-extensions/flash-ext-auth-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcTokenResponse.java diff --git a/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcValidationException.java b/flash-extensions/flash-ext-auth-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcValidationException.java similarity index 100% rename from flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcValidationException.java rename to flash-extensions/flash-ext-auth-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcValidationException.java diff --git a/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/PkceUtils.java b/flash-extensions/flash-ext-auth-oidc/src/main/java/dev/relism/flash/ext/oidc/PkceUtils.java similarity index 100% rename from flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/PkceUtils.java rename to flash-extensions/flash-ext-auth-oidc/src/main/java/dev/relism/flash/ext/oidc/PkceUtils.java diff --git a/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/TokenClient.java b/flash-extensions/flash-ext-auth-oidc/src/main/java/dev/relism/flash/ext/oidc/TokenClient.java similarity index 100% rename from flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/TokenClient.java rename to flash-extensions/flash-ext-auth-oidc/src/main/java/dev/relism/flash/ext/oidc/TokenClient.java diff --git a/flash-extensions/flash-ext-oidc/src/test/java/dev/relism/flash/ext/oidc/OidcCredentialSourceTest.java b/flash-extensions/flash-ext-auth-oidc/src/test/java/dev/relism/flash/ext/oidc/OidcCredentialSourceTest.java similarity index 100% rename from flash-extensions/flash-ext-oidc/src/test/java/dev/relism/flash/ext/oidc/OidcCredentialSourceTest.java rename to flash-extensions/flash-ext-auth-oidc/src/test/java/dev/relism/flash/ext/oidc/OidcCredentialSourceTest.java diff --git a/flash-extensions/flash-ext-oidc/src/test/java/dev/relism/flash/ext/oidc/OidcOpenApiInteropTest.java b/flash-extensions/flash-ext-auth-oidc/src/test/java/dev/relism/flash/ext/oidc/OidcOpenApiInteropTest.java similarity index 100% rename from flash-extensions/flash-ext-oidc/src/test/java/dev/relism/flash/ext/oidc/OidcOpenApiInteropTest.java rename to flash-extensions/flash-ext-auth-oidc/src/test/java/dev/relism/flash/ext/oidc/OidcOpenApiInteropTest.java diff --git a/flash-extensions/flash-ext-mcp/docs/README.md b/flash-extensions/flash-ext-mcp/docs/README.md index 643ce0b..4022668 100644 --- a/flash-extensions/flash-ext-mcp/docs/README.md +++ b/flash-extensions/flash-ext-mcp/docs/README.md @@ -3,7 +3,7 @@ `flash-ext-mcp` turns a Flash5 app into an [MCP](https://modelcontextprotocol.io) (Model Context Protocol) server: JSON-RPC 2.0 over the Streamable HTTP transport, tools/resources/prompts declared as plain classes and discovered at boot, optional OAuth2 protection built on -`flash-ext-oidc`. +`flash-ext-auth-oidc`. ## Quick Start @@ -44,7 +44,7 @@ public class GetWeatherTool extends McpTool { `tools-resources-prompts.md`. - **Transport**: Streamable HTTP, `POST`-only, stateless in this revision — see `transport.md` for exactly what that means and why. -- **Security**: optional, policy-driven OAuth2 via `flash-ext-oidc` — see `security.md`. +- **Security**: optional, policy-driven OAuth2 via `flash-ext-auth-oidc` — see `security.md`. - **JSON**: this extension owns its JSON handling independently of `flash-ext-jackson` — see `jackson-interop.md` for why, and how a future opt-in reuse could work. diff --git a/flash-extensions/flash-ext-mcp/docs/jackson-interop.md b/flash-extensions/flash-ext-mcp/docs/jackson-interop.md index dddee59..a968ad9 100644 --- a/flash-extensions/flash-ext-mcp/docs/jackson-interop.md +++ b/flash-extensions/flash-ext-mcp/docs/jackson-interop.md @@ -24,7 +24,7 @@ see `tools-resources-prompts.md` — and the fixed `TextContent`/`TextResourceCo as a `JsonNode` tree, not as a databound class, for the same reason — a JSON-RPC tool call's arguments aren't a DTO with getters/setters, they're a dynamic, per-tool-defined bag of values. -This mirrors how `flash-ext-oidc` already handles its own internal JSON needs (`json-smart` for +This mirrors how `flash-ext-auth-oidc` already handles its own internal JSON needs (`json-smart` for token-endpoint responses) independently of `flash-ext-jackson` — extensions with protocol-level JSON needs that are shaped by a spec, not by user code, own that JSON handling themselves rather than routing it through the app's general-purpose JSON extension. diff --git a/flash-extensions/flash-ext-mcp/docs/keycloak.md b/flash-extensions/flash-ext-mcp/docs/keycloak.md index 6326541..e930b77 100644 --- a/flash-extensions/flash-ext-mcp/docs/keycloak.md +++ b/flash-extensions/flash-ext-mcp/docs/keycloak.md @@ -1,7 +1,7 @@ # Keycloak cookbook `security.md` covers the OAuth2 mechanics `McpOidcIntegration` implements against any -`flash-ext-oidc`-compatible provider. This is the Keycloak-specific setup: the exact Admin +`flash-ext-auth-oidc`-compatible provider. This is the Keycloak-specific setup: the exact Admin Console configuration for a working MCP OAuth2 flow with open Dynamic Client Registration (DCR) — no pre-registered clients, any MCP client self-registers on first connect. diff --git a/flash-extensions/flash-ext-mcp/docs/security.md b/flash-extensions/flash-ext-mcp/docs/security.md index 43e3903..412b12e 100644 --- a/flash-extensions/flash-ext-mcp/docs/security.md +++ b/flash-extensions/flash-ext-mcp/docs/security.md @@ -7,10 +7,10 @@ Allowed Client Scopes configuration `scopes_supported` needs to actually work. ## `McpSecurity` -`McpConfig.security(...)` controls how the MCP endpoint reacts to `flash-ext-oidc` being +`McpConfig.security(...)` controls how the MCP endpoint reacts to `flash-ext-auth-oidc` being installed (`ctx.find(OidcMiddleware.class)`), resolved once at boot in `McpExtension.routes()`: -| Policy | `flash-ext-oidc` installed | `flash-ext-oidc` absent | +| Policy | `flash-ext-auth-oidc` installed | `flash-ext-auth-oidc` absent | |---|---|---| | `REQUIRED` | protected | **boot fails** (`IllegalStateException`) | | `AUTO` (default) | protected | runs unprotected, logs a warning | @@ -21,18 +21,18 @@ turns "someone forgot to wire up OAuth2" into a startup crash instead of a silen endpoint. `AUTO` is meant for local development, where spinning up a real identity provider is friction you don't want yet. -## Why `flash-ext-oidc` is an *optional* Maven dependency, concretely +## Why `flash-ext-auth-oidc` is an *optional* Maven dependency, concretely Maven's `true` only affects **transitive** propagation: consumers of -`flash-ext-mcp` don't get `flash-ext-oidc` pulled in automatically unless they add it themselves. -Within `flash-ext-mcp` itself, `flash-ext-oidc`'s classes are on the compile/test classpath as +`flash-ext-mcp` don't get `flash-ext-auth-oidc` pulled in automatically unless they add it themselves. +Within `flash-ext-mcp` itself, `flash-ext-auth-oidc`'s classes are on the compile/test classpath as normal — this extension can (and does) reference `OidcMiddleware`/`ClaimsHolder` directly in source. That reference is isolated in its own class, `McpOidcIntegration`, invoked only from inside a `catch (NoClassDefFoundError)` block. A bare class-literal like `OidcMiddleware.class` (which `ctx.find(OidcMiddleware.class)` needs) forces the JVM to resolve that type the moment it's -evaluated — if `flash-ext-oidc` is not on the *runtime* classpath at all (a genuinely +evaluated — if `flash-ext-auth-oidc` is not on the *runtime* classpath at all (a genuinely MCP-only install, no OAuth2 anywhere in the app), the first such reference throws `NoClassDefFoundError`. Keeping that reference inside a separate, lazily-loaded class means `McpExtension` itself loads and works fine standalone; only the attempt to actually use OIDC @@ -45,7 +45,7 @@ When oidc is available and `security() != NONE`, `McpOidcIntegration` (an isolat lazily-loaded bridge — see its javadoc) derives everything an MCP OAuth2 resource server needs straight from the installed `OidcMiddleware`, with no additional `McpConfig` calls required: -1. The MCP route is wrapped with `flash-ext-oidc`'s own `OidcMiddleware.protect(resourceMetadataPath)` +1. The MCP route is wrapped with `flash-ext-auth-oidc`'s own `OidcMiddleware.protect(resourceMetadataPath)` — the same Bearer-token/JWKS validation path used everywhere else in Flash5, plus a `resource_metadata` challenge parameter (see below). No JWT parsing or JWKS handling is reimplemented here. @@ -114,7 +114,7 @@ unaffected — this parameter is additive and MCP-specific. ## Per-tool `@RolesAllowed`/`@ScopesAllowed` -`McpTool` subclasses can carry `flash-ext-oidc`'s `@RolesAllowed`/`@ScopesAllowed`: +`McpTool` subclasses can carry `flash-ext-auth-oidc`'s `@RolesAllowed`/`@ScopesAllowed`: ```java @Tool(name = "delete_route", description = "Delete a route") @@ -124,7 +124,7 @@ public class DeleteRouteTool extends McpTool { } ``` -This does **not** reuse `flash-ext-oidc`'s per-route middleware mechanism (`ctx.addAnnotationProcessor`, +This does **not** reuse `flash-ext-auth-oidc`'s per-route middleware mechanism (`ctx.addAnnotationProcessor`, the thing that makes these annotations work on a `RequestHandler`) — it can't: every tool shares one HTTP route (`POST {rootPath}`), already wrapped by whatever `McpSecurity` resolved above, so there is no per-tool route to attach a different middleware chain to. Instead, @@ -156,7 +156,7 @@ at `app.start()`. ## The `HttpException` safety net -`flash-ext-oidc`'s middleware throws `HttpException.unauthorized()`/`forbidden()` on auth +`flash-ext-auth-oidc`'s middleware throws `HttpException.unauthorized()`/`forbidden()` on auth failure. Flash5's core does **not** special-case `HttpException` in the default exception handler — the out-of-the-box `AbstractRouter` default always returns a generic `500`, regardless of the thrown exception's embedded status code; only an app that explicitly calls diff --git a/flash-extensions/flash-ext-mcp/pom.xml b/flash-extensions/flash-ext-mcp/pom.xml index db7f5ac..b0a873f 100644 --- a/flash-extensions/flash-ext-mcp/pom.xml +++ b/flash-extensions/flash-ext-mcp/pom.xml @@ -19,7 +19,7 @@ dev.relism - flash-ext-oidc + flash-ext-auth-oidc true diff --git a/flash-extensions/flash-ext-mcp/src/main/java/dev/relism/flash/ext/mcp/McpAuthPolicy.java b/flash-extensions/flash-ext-mcp/src/main/java/dev/relism/flash/ext/mcp/McpAuthPolicy.java index 088d35e..6bdd880 100644 --- a/flash-extensions/flash-ext-mcp/src/main/java/dev/relism/flash/ext/mcp/McpAuthPolicy.java +++ b/flash-extensions/flash-ext-mcp/src/main/java/dev/relism/flash/ext/mcp/McpAuthPolicy.java @@ -10,7 +10,7 @@ import java.util.function.Supplier; * *

    {@code check} is a closure, not a raw role/scope list — this is what lets this record (and * its only caller, {@link McpDispatcher}) stay free of any compile-time reference to a {@code - * flash-ext-oidc} type, preserving the same classload isolation {@link McpOidcIntegration}'s + * flash-ext-auth-oidc} type, preserving the same classload isolation {@link McpOidcIntegration}'s * javadoc describes for the rest of the OIDC bridge. Only the plain-JDK {@link Supplier} * signature crosses the boundary; the closure itself, built once inside {@code * McpOidcIntegration}, is the only place that ever touches {@code OidcUser}/{@code ClaimsHolder}. diff --git a/flash-extensions/flash-ext-mcp/src/main/java/dev/relism/flash/ext/mcp/McpConfig.java b/flash-extensions/flash-ext-mcp/src/main/java/dev/relism/flash/ext/mcp/McpConfig.java index a0538d7..f088b60 100644 --- a/flash-extensions/flash-ext-mcp/src/main/java/dev/relism/flash/ext/mcp/McpConfig.java +++ b/flash-extensions/flash-ext-mcp/src/main/java/dev/relism/flash/ext/mcp/McpConfig.java @@ -91,7 +91,7 @@ public final class McpConfig { /** * Canonical URI of this MCP endpoint, used for RFC 8707 audience binding: tokens whose * {@code aud} claim does not include this value are rejected. Optional — when - * {@code flash-ext-oidc} is installed, this is auto-derived per request from the + * {@code flash-ext-auth-oidc} is installed, this is auto-derived per request from the * forwarded/{@code Host} headers (same resolution {@code OidcExtension} uses for its own * redirect URIs) and audience binding is enforced unconditionally. Set this explicitly * only to override that guess — a reverse proxy that forwards neither @@ -102,7 +102,7 @@ public final class McpConfig { /** * Authorization server issuer URL, published in the RFC 9728 Protected Resource * Metadata document at {@code /.well-known/oauth-protected-resource{rootPath}}. Optional - * — when {@code flash-ext-oidc} is installed, this is auto-derived from its configured + * — when {@code flash-ext-auth-oidc} is installed, this is auto-derived from its configured * issuer. Set this explicitly only to override that (e.g. publishing a different issuer * than the one actually validating tokens). */ diff --git a/flash-extensions/flash-ext-mcp/src/main/java/dev/relism/flash/ext/mcp/McpExtension.java b/flash-extensions/flash-ext-mcp/src/main/java/dev/relism/flash/ext/mcp/McpExtension.java index baa0dff..3126f99 100644 --- a/flash-extensions/flash-ext-mcp/src/main/java/dev/relism/flash/ext/mcp/McpExtension.java +++ b/flash-extensions/flash-ext-mcp/src/main/java/dev/relism/flash/ext/mcp/McpExtension.java @@ -25,7 +25,7 @@ import java.util.List; * .build())) * .start(); * - * // With flash-ext-oidc as the OAuth2 resource server — zero extra config: issuer, canonical + * // With flash-ext-auth-oidc as the OAuth2 resource server — zero extra config: issuer, canonical * // resource identifier, RFC 8707 audience binding and RFC 9728 metadata are all derived from * // the installed OidcExtension. * FlashApp.create(8080) @@ -83,20 +83,20 @@ public class McpExtension implements FlashExtension { try { resolved = McpOidcIntegration.resolve(ctx, config); } catch (NoClassDefFoundError e) { - resolved = null; // flash-ext-oidc not on the classpath at all + resolved = null; // flash-ext-auth-oidc not on the classpath at all } if (resolved != null) return resolved; if (config.security() == McpSecurity.REQUIRED) { throw new IllegalStateException( - "McpSecurity.REQUIRED but flash-ext-oidc is not installed for MCP server \"" + config.name() + + "McpSecurity.REQUIRED but flash-ext-auth-oidc is not installed for MCP server \"" + config.name() + "\" — install an OidcExtension before this McpExtension, or relax security to " + "McpSecurity.AUTO/NONE if this server is meant to be public."); } log.warn("[flash-ext-mcp] MCP server \"{}\" is running WITHOUT OAuth2 protection — " + - "flash-ext-oidc is not installed and McpSecurity.AUTO degrades to unprotected. " + - "Install flash-ext-oidc or set McpSecurity.REQUIRED to make this a hard failure instead.", + "flash-ext-auth-oidc is not installed and McpSecurity.AUTO degrades to unprotected. " + + "Install flash-ext-auth-oidc or set McpSecurity.REQUIRED to make this a hard failure instead.", config.name()); return null; } diff --git a/flash-extensions/flash-ext-mcp/src/main/java/dev/relism/flash/ext/mcp/McpJson.java b/flash-extensions/flash-ext-mcp/src/main/java/dev/relism/flash/ext/mcp/McpJson.java index 426fc01..64f3c4e 100644 --- a/flash-extensions/flash-ext-mcp/src/main/java/dev/relism/flash/ext/mcp/McpJson.java +++ b/flash-extensions/flash-ext-mcp/src/main/java/dev/relism/flash/ext/mcp/McpJson.java @@ -19,7 +19,7 @@ import java.nio.charset.StandardCharsets; * *

    Not wired to {@code flash-ext-jackson} on purpose: the MCP JSON-RPC envelope is internal * protocol plumbing, not a user-facing serialization concern, so this extension owns its - * mapper independently — same reasoning {@code flash-ext-oidc} applies to its own JSON needs + * mapper independently — same reasoning {@code flash-ext-auth-oidc} applies to its own JSON needs * (see {@code json-smart} there). See {@code docs/jackson-interop.md} for the full rationale * and how a future opt-in reuse of a shared {@code ObjectMapper} could work. */ diff --git a/flash-extensions/flash-ext-mcp/src/main/java/dev/relism/flash/ext/mcp/McpOidcIntegration.java b/flash-extensions/flash-ext-mcp/src/main/java/dev/relism/flash/ext/mcp/McpOidcIntegration.java index c971968..5262bb4 100644 --- a/flash-extensions/flash-ext-mcp/src/main/java/dev/relism/flash/ext/mcp/McpOidcIntegration.java +++ b/flash-extensions/flash-ext-mcp/src/main/java/dev/relism/flash/ext/mcp/McpOidcIntegration.java @@ -20,19 +20,19 @@ import java.util.function.Function; import java.util.function.Supplier; /** - * Lazy, isolated bridge to {@code flash-ext-oidc} and {@code flash-ext-auth-core}. + * Lazy, isolated bridge to {@code flash-ext-auth-oidc} and {@code flash-ext-auth-core}. * *

    References to OIDC types only ever resolve when {@link #resolve}/{@link #compileToolPolicy} * are actually invoked — never at {@link McpExtension} class-load time — because they live in * this separate nested class. The caller wraps the invocation in {@code catch * (NoClassDefFoundError)}, exactly like {@code OidcExtension}'s own lazy bridge to {@code * flash-ext-openapi}. This is what lets {@code flash-ext-mcp} run standalone (MCP-only, no - * OAuth2) when {@code flash-ext-oidc} is not even on the classpath. {@link Resolved}/{@link + * OAuth2) when {@code flash-ext-auth-oidc} is not even on the classpath. {@link Resolved}/{@link * McpAuthPolicy} carry only oidc-free types back out ({@link Middleware}, {@link String}, a * {@link Function}, a {@link Supplier}) so no other class in this package ever has to reference * an OIDC type. * - *

    Zero-config by design: when {@code flash-ext-oidc} is installed, everything an MCP OAuth2 + *

    Zero-config by design: when {@code flash-ext-auth-oidc} is installed, everything an MCP OAuth2 * resource server needs — issuer, canonical resource identifier, RFC 8707 audience binding, and * a spec-compliant {@code WWW-Authenticate} challenge (RFC 9728 §5.1) — is derived straight from * the installed {@link OidcCredentialSource}, with no additional {@link McpConfig} calls. @@ -123,7 +123,7 @@ final class McpOidcIntegration { if (!oidcActive) { throw new IllegalStateException( "MCP tool \"" + toolClass.getSimpleName() + "\" declares @Authenticated/@RolesAllowed/" + - "@ScopesAllowed, but this MCP server has no active OAuth2 protection — flash-ext-oidc " + + "@ScopesAllowed, but this MCP server has no active OAuth2 protection — flash-ext-auth-oidc " + "is not installed for it, or McpSecurity is NONE. These annotations require " + "McpSecurity.AUTO/REQUIRED with an OidcExtension installed; install one, or remove the " + "annotation from " + toolClass.getSimpleName() + "."); diff --git a/flash-extensions/flash-ext-mcp/src/main/java/dev/relism/flash/ext/mcp/McpRegistry.java b/flash-extensions/flash-ext-mcp/src/main/java/dev/relism/flash/ext/mcp/McpRegistry.java index 4615862..61105d4 100644 --- a/flash-extensions/flash-ext-mcp/src/main/java/dev/relism/flash/ext/mcp/McpRegistry.java +++ b/flash-extensions/flash-ext-mcp/src/main/java/dev/relism/flash/ext/mcp/McpRegistry.java @@ -175,7 +175,7 @@ final class McpRegistry { /** * Isolated the same way {@link McpOidcIntegration#resolve} is — {@code - * NoClassDefFoundError} here means {@code flash-ext-oidc} genuinely isn't on the runtime + * NoClassDefFoundError} here means {@code flash-ext-auth-oidc} genuinely isn't on the runtime * classpath, in which case a tool couldn't have been compiled against * {@code @RolesAllowed}/{@code @ScopesAllowed} in the first place, so there's nothing to * check (and nothing lost: {@code oidcActive} is only ever {@code true} once {@link diff --git a/flash-extensions/flash-ext-mcp/src/main/java/dev/relism/flash/ext/mcp/McpSecurity.java b/flash-extensions/flash-ext-mcp/src/main/java/dev/relism/flash/ext/mcp/McpSecurity.java index 211ea5d..21ee37b 100644 --- a/flash-extensions/flash-ext-mcp/src/main/java/dev/relism/flash/ext/mcp/McpSecurity.java +++ b/flash-extensions/flash-ext-mcp/src/main/java/dev/relism/flash/ext/mcp/McpSecurity.java @@ -2,16 +2,16 @@ package dev.relism.flash.ext.mcp; /** * OAuth2 requirement policy for the MCP endpoint, resolved against whether - * {@code flash-ext-oidc} is installed ({@code ctx.find(OidcMiddleware.class)}). + * {@code flash-ext-auth-oidc} is installed ({@code ctx.find(OidcMiddleware.class)}). */ public enum McpSecurity { - /** Fail fast at boot if {@code flash-ext-oidc} is not installed — never expose an unprotected MCP endpoint. */ + /** Fail fast at boot if {@code flash-ext-auth-oidc} is not installed — never expose an unprotected MCP endpoint. */ REQUIRED, - /** Protect the endpoint if {@code flash-ext-oidc} is installed; otherwise run unprotected and log a warning. */ + /** Protect the endpoint if {@code flash-ext-auth-oidc} is installed; otherwise run unprotected and log a warning. */ AUTO, - /** Never protect the endpoint, even if {@code flash-ext-oidc} is installed elsewhere in the app. */ + /** Never protect the endpoint, even if {@code flash-ext-auth-oidc} is installed elsewhere in the app. */ NONE } diff --git a/flash-extensions/flash-ext-mcp/src/main/java/dev/relism/flash/ext/mcp/McpTransportGuards.java b/flash-extensions/flash-ext-mcp/src/main/java/dev/relism/flash/ext/mcp/McpTransportGuards.java index 94ab5a0..af6be7b 100644 --- a/flash-extensions/flash-ext-mcp/src/main/java/dev/relism/flash/ext/mcp/McpTransportGuards.java +++ b/flash-extensions/flash-ext-mcp/src/main/java/dev/relism/flash/ext/mcp/McpTransportGuards.java @@ -38,7 +38,7 @@ final class McpTransportGuards { /** * Safety net around the whole MCP route: translates {@link HttpException} (thrown by - * {@link #originGuard} or by {@code flash-ext-oidc}'s middleware) into a proper HTTP status + * {@link #originGuard} or by {@code flash-ext-auth-oidc}'s middleware) into a proper HTTP status * directly, instead of relying on the app's global exception handler — which defaults to a * generic 500 for every exception type unless the app owner overrides it (see * {@code AbstractRouter}'s default {@code exceptionHandler}). Keeps the MCP endpoint diff --git a/flash-extensions/flash-ext-mcp/src/test/java/dev/relism/flash/ext/mcp/FakeOidcProvider.java b/flash-extensions/flash-ext-mcp/src/test/java/dev/relism/flash/ext/mcp/FakeOidcProvider.java index 8eb5f29..799a7a4 100644 --- a/flash-extensions/flash-ext-mcp/src/test/java/dev/relism/flash/ext/mcp/FakeOidcProvider.java +++ b/flash-extensions/flash-ext-mcp/src/test/java/dev/relism/flash/ext/mcp/FakeOidcProvider.java @@ -26,7 +26,7 @@ import java.util.UUID; /** * Minimal, self-contained fake OIDC provider for tests: real discovery document, real JWKS * endpoint, real RS256-signed tokens — no network dependency beyond localhost, no mocking - * framework. Exercises {@code flash-ext-oidc}'s actual discovery + JWKS + JWT validation path. + * framework. Exercises {@code flash-ext-auth-oidc}'s actual discovery + JWKS + JWT validation path. */ final class FakeOidcProvider implements AutoCloseable { diff --git a/flash-extensions/flash-ext-mcp/src/test/java/dev/relism/flash/ext/mcp/McpExtensionSecurityTest.java b/flash-extensions/flash-ext-mcp/src/test/java/dev/relism/flash/ext/mcp/McpExtensionSecurityTest.java index 111b5ea..4b8f606 100644 --- a/flash-extensions/flash-ext-mcp/src/test/java/dev/relism/flash/ext/mcp/McpExtensionSecurityTest.java +++ b/flash-extensions/flash-ext-mcp/src/test/java/dev/relism/flash/ext/mcp/McpExtensionSecurityTest.java @@ -16,7 +16,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; /** - * Exercises the actual OAuth2 resolution rules against a real {@code flash-ext-oidc} + * Exercises the actual OAuth2 resolution rules against a real {@code flash-ext-auth-oidc} * installation backed by {@link FakeOidcProvider} — real discovery, real JWKS, real RS256 * tokens — plus the fail-fast/degrade behavior when oidc is absent. * diff --git a/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/InMemoryOidcSessionStore.java b/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/InMemoryOidcSessionStore.java deleted file mode 100644 index 3df2afc..0000000 --- a/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/InMemoryOidcSessionStore.java +++ /dev/null @@ -1,20 +0,0 @@ -package dev.relism.flash.ext.oidc; - -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Thread-safe in-memory {@link OidcSessionStore}. - * - *

    Sessions are lost on restart and not shared across instances. For - * production deployments with multiple nodes or restart-persistence requirements, - * supply a custom implementation via {@link OidcConfig.Builder#sessionStore}. - */ -public final class InMemoryOidcSessionStore implements OidcSessionStore { - - private final ConcurrentHashMap store = new ConcurrentHashMap<>(); - - @Override public void save(OidcSession s) { store.put(s.id(), s); } - @Override public Optional find(String id) { return Optional.ofNullable(store.get(id)); } - @Override public void delete(String id) { store.remove(id); } -} diff --git a/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcSession.java b/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcSession.java deleted file mode 100644 index 742404b..0000000 --- a/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcSession.java +++ /dev/null @@ -1,47 +0,0 @@ -package dev.relism.flash.ext.oidc; - -import java.time.Instant; -import java.util.Map; - -/** - * An authenticated user's OIDC session — persisted in {@link OidcSessionStore} and - * looked up via the {@code oidc_session} cookie on every request. - * - *

    Sessions are immutable; a refreshed access token produces a new instance - * that replaces the old one in the store (same {@link #id()}). - */ -public final class OidcSession { - - private final String id; - private final String accessToken; - private final String idToken; - private final String refreshToken; // may be null - private final Instant accessTokenExpiresAt; - private final Map claims; // decoded from id_token - - public OidcSession(String id, String accessToken, String idToken, - String refreshToken, Instant accessTokenExpiresAt, - Map claims) { - this.id = id; - this.accessToken = accessToken; - this.idToken = idToken; - this.refreshToken = refreshToken; - this.accessTokenExpiresAt = accessTokenExpiresAt; - this.claims = Map.copyOf(claims); - } - - /** - * Returns {@code true} if the access token has expired or will expire within - * the next 30 seconds (eager refresh to avoid mid-request expiry). - */ - public boolean isAccessTokenExpired() { - return Instant.now().isAfter(accessTokenExpiresAt.minusSeconds(30)); - } - - public String id() { return id; } - public String accessToken() { return accessToken; } - public String idToken() { return idToken; } - public String refreshToken() { return refreshToken; } - public Instant accessTokenExpiresAt() { return accessTokenExpiresAt; } - public Map claims() { return claims; } -} diff --git a/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcSessionStore.java b/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcSessionStore.java deleted file mode 100644 index b23f816..0000000 --- a/flash-extensions/flash-ext-oidc/src/main/java/dev/relism/flash/ext/oidc/OidcSessionStore.java +++ /dev/null @@ -1,14 +0,0 @@ -package dev.relism.flash.ext.oidc; - -import java.util.Optional; - -/** - * Backing store for {@link OidcSession} objects. The default implementation is - * {@link InMemoryOidcSessionStore}; supply a custom one via - * {@link OidcConfig.Builder#sessionStore(OidcSessionStore)} for Redis, JDBC, etc. - */ -public interface OidcSessionStore { - void save(OidcSession session); - Optional find(String sessionId); - void delete(String sessionId); -} diff --git a/flash-extensions/flash-ext-openapi/README.md b/flash-extensions/flash-ext-openapi/README.md index b49159c..977c86c 100644 --- a/flash-extensions/flash-ext-openapi/README.md +++ b/flash-extensions/flash-ext-openapi/README.md @@ -123,7 +123,7 @@ Merge policy: ## OIDC interop -When `flash-ext-oidc` is installed, OpenAPI integrates automatically: +When `flash-ext-auth-oidc` is installed, OpenAPI integrates automatically: - security scheme under `components.securitySchemes` - per-operation `security` diff --git a/flash-extensions/pom.xml b/flash-extensions/pom.xml index a4f195e..fb4b3fb 100644 --- a/flash-extensions/pom.xml +++ b/flash-extensions/pom.xml @@ -17,7 +17,7 @@ flash-ext-jackson flash-ext-openapi flash-ext-auth-core - flash-ext-oidc + flash-ext-auth-oidc flash-ext-routeviewer flash-ext-view-core flash-ext-view-jte diff --git a/pom.xml b/pom.xml index 452ddc5..14a638e 100644 --- a/pom.xml +++ b/pom.xml @@ -106,7 +106,7 @@ dev.relism - flash-ext-oidc + flash-ext-auth-oidc ${project.version}