McpExtension built its middleware chain entirely internally, so a consumer had no way to add rate limiting, audit logging or tracing to /mcp — routine on every other Flash route. McpConfig.middleware(...) appends to the chain after the transport guards and after whatever McpSecurity resolved to, so it composes with OAuth2 protection instead of replacing it, and never satisfies REQUIRED. Docs: flash-ext-auth-core and flash-ext-auth-oidc both get a docs/ directory — oidc had none at all, and its module README documented types that no longer exist. Includes a migration table from flash-ext-oidc.
4.6 KiB
flash-ext-auth-core
Authorization, and the plumbing that carries a caller's identity through a request. It does not
know how anyone signed in — that is a CredentialSource, and flash-ext-auth-oidc ships the
OpenID Connect one.
The split follows the same shape as flash-ext-cache-core/-caffeine and
flash-ext-data-core/-hibernate: the abstract half here, the implementations beside it.
The model
request ──► CredentialSource.authenticate(req, res) ──► claims
│
ClaimsHolder.set (this module only)
│
AuthMiddleware matches roles / scopes
│
handler
| Type | What it is |
|---|---|
CredentialSource |
Turns what a request carries into claims, or rejects it. One per mechanism. |
AuthMiddleware |
Publishes the claims, enforces @RolesAllowed/@ScopesAllowed, clears up. |
ClaimsHolder |
The current request's claims. Read from anywhere; written only from here. |
Claims |
Typed view over a claims map — sub(), email(), roles(path), scopes(). |
AuthPolicy |
What a handler's annotations compiled to, resolved once at boot. |
Session, SessionStore |
Server-side sessions for sources that keep them. |
Nothing outside this module can write ClaimsHolder. A source returns claims and the middleware
publishes them, so no code can put claims on a request that did not carry them.
Using it
You rarely install this module directly — an extension that contributes a source does it for you:
// inside your extension's configure(...)
AuthMiddleware auth = AuthMiddleware.install(ctx, AuthConfig.builder()
.rolesClaimPath("realm_access.roles")
.scopeClaimPaths("scope,scp")
.build(), mySource);
install publishes the middleware in the context and registers the annotation processor, so every
scanned handler carrying an auth annotation is mounted behind it. See
credential-sources.md to write a source of your own.
On lambda routes, take the middleware out of the context:
AuthMiddleware auth = app.ctx().require(AuthMiddleware.class);
app.get("/api/me", (req, res) -> ClaimsHolder.claim("sub"), auth.protect());
app.get("/", homeHandler, auth.optional());
app.delete("/admin/users/{id}", deleteHandler, auth.requireRole("admin"));
app.post("/orders", createOrder, auth.requireScopes("orders:write"));
Annotations
On a scanned handler class, and mounted automatically:
| Annotation | Effect |
|---|---|
@Authenticated |
Any accepted credential. No role check. |
@Authenticated(optional = true) |
Never rejects; publishes claims when there are some. |
@RolesAllowed({"a","b"}) |
Authenticated and holding at least one of the roles. |
@ScopesAllowed({"x","y"}) |
Authenticated and holding all of the scopes. |
@ScopesAllowed(value = {...}, match = ANY) |
…at least one of them. |
@Authenticated(optional = true) cannot be combined with a role or scope requirement — asking for
a role on a route that admits anonymous callers is a contradiction, and it fails at boot rather
than at 3am.
Where roles and scopes are read from
AuthConfig names the claim paths, because every provider spells them differently:
| Default | Common alternatives | |
|---|---|---|
rolesClaimPath |
roles |
realm_access.roles (Keycloak), groups (Authelia) |
scopeClaimPaths |
scope,scp |
plus e.g. permissions.scopes |
Paths are dot-separated and walk nested maps. Scope paths are a comma-separated list tried in
order, so a token that puts scopes in scp and a legacy one that uses scope both work.
Matching is deliberate about a distinction that bites otherwise:
- a string claim is split on spaces, tabs, newlines and commas —
"openid orders:read"is two scopes; - a list claim is compared entry by entry, whole and trimmed —
["a b"]is one role nameda b, not two.
Prefix matches never count: administrator does not satisfy admin.
Ordering around authentication
AuthMiddleware.POLICY is the boot-time key the annotation-driven node mounts under. An extension
contributing its own middleware can order itself against it:
MiddlewareNode.of(MY_KEY, myMiddleware).afterIfPresent(AuthMiddleware.POLICY);