feat(ext-security): add an OAuth 2.1 authorization server

flash-ext-security-oauth-server issues RFC 9068 access tokens (code + PKCE S256,
CIMD and DCR clients, RFC 8707 resources, rotating refresh tokens) for resources on
the application's own origin. Around it: SecurityExtension resolves a configured
origin instead of X-Forwarded-* headers, mechanisms expose schemes() and a route can
be restricted to some of them, McpConfig.mechanisms(...) uses that, OIDC bearers must
be typed at+jwt, and PublicUrl guards outbound fetches against internal addresses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Zakaria El Orche
2026-09-22 11:39:09 +00:00
co-authored by Claude Opus 5
parent 7f225e0faf
commit f28fc43150
35 changed files with 1958 additions and 80 deletions
@@ -47,11 +47,30 @@ On a handler or an MCP tool class:
Mechanisms are tried in registration order, then the session cookie. The first to return a
principal wins. When none does:
- a browser (`Accept: text/html`) is redirected to the only login method, or to `loginPage` when there are several;
- a browser (`Accept: text/html`) is redirected to `loginPage` when one is set, otherwise to the only
login method when it is a redirect, otherwise to `/login`;
- anything else gets `401` with every mechanism's challenge in `WWW-Authenticate`.
`entryPoint(...)` replaces that, e.g. to pick an identity provider from the user's email domain.
`enforce(policy, entryPoint, mechanisms)` restricts a route to some mechanisms: any other credential,
the session cookie included, is no credential there — a bearer-only endpoint a browser's cookie must
not reach.
## Origin
`origin("https://app.example")` is where the application is served. Session cookies, sign-in
callbacks and token audiences are built from `origin(req)`: the configured origin, or the request's
own scheme and `Host` when none is. `X-Forwarded-*` is never read: the client can send anything, and
an origin taken from it lets a caller choose which audience a token must have. Configure it in every
deployment; the fallback is for development and tests.
## Addresses someone else chose
`PublicUrl.require(url)` refuses anything that is not https on a public address. Call it before the
server fetches a URL a customer or a client supplied — an identity provider, a metadata document — or
that fetch becomes a request forgery against the server's own network.
## Sessions
`signIn(req, res, principal[, expiresAt])` stores the principal under a `flash_session` cookie;
@@ -78,7 +97,7 @@ security.mechanism(new AuthenticationMechanism() {
if (p == null) throw new AuthenticationFailedException(null); // mine, and invalid
return p;
}
public SecurityScheme scheme() { return SecurityScheme.bearer("key", "opaque"); }
public List<SecurityScheme> schemes() { return List.of(SecurityScheme.bearer("key", "opaque")); }
});
```