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.
This commit is contained in:
Zakaria El Orche
2026-09-10 19:06:15 +00:00
parent c5be6ac7b8
commit 9d39e24ccb
45 changed files with 271 additions and 166 deletions
@@ -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.
@@ -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.
@@ -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.
+10 -10
View File
@@ -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 `<optional>true</optional>` 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