Skip to content

feat(security): Spring Security parity — Tier 4 OAuth2 ecosystem (v26.6.33)#41

Merged
ancongui merged 6 commits into
mainfrom
feat/spring-security-tier4-oauth2-ecosystem
Jun 19, 2026
Merged

feat(security): Spring Security parity — Tier 4 OAuth2 ecosystem (v26.6.33)#41
ancongui merged 6 commits into
mainfrom
feat/spring-security-tier4-oauth2-ecosystem

Conversation

@ancongui

Copy link
Copy Markdown
Contributor

Spring Security parity — Tier 4: the OAuth2 ecosystem

The wider OAuth2 surface beyond the browser login flow. All additive — no
behaviour change to existing code. Adversarially reviewed before release.

Added

  • Opaque-token introspection (RFC 7662)RemoteTokenIntrospector
    (Spring's OpaqueTokenIntrospector); a drop-in Verifier for opaque bearer
    tokens, fail-closed.
  • Outbound client (AuthorizedClientManager)
    OAuth2AuthorizedClientManager + OAuth2AuthorizedClientService obtain,
    cache, and auto-refresh access tokens for downstream calls (client-credentials
    • refresh-token grants).
  • RP-initiated logout (OIDC)oidc_logout_url +
    end_session_endpoint/post_logout_redirect_uri; POST /logout redirects to
    the IdP end_session_endpoint with id_token_hint
    (OidcClientInitiatedLogoutSuccessHandler).
  • Authorization-server HTTP endpointsAuthorizationServerRouter mounts
    POST /oauth2/token (RFC 6749) and GET /.well-known/oauth-authorization-server
    (RFC 8414 metadata).

Adversarial review fixes (9 confirmed — all availability/hardening, no bypass)

  • HIGH: added connect/read timeouts to the OAuth2 HTTP clients (a slow/hostile
    endpoint could hang the bearer-verification hot path).
  • Bounded fallback expiry when expires_in is absent (no immortal cached token);
    response-body size cap; SERVER_ERROR → HTTP 500 (no raw signer-error leak);
    fallible logout redirect() (no panic); redacting Debug for
    ClientRegistration/OAuth2AuthorizedClient secrets.

Tests & docs

  • Lib + integration tests (introspection, outbound, login/logout, AS endpoints)
    via in-process mock servers — all green; fmt + clippy clean; full-workspace
    cargo check green at v26.6.33.
  • Book Spring Security Parity appendix updated (EN + ES), republished
    (PDF + EPUB). CHANGELOG v26.6.33; MODULES.md.

Version bumped to 26.6.33.

Andres Contreras added 6 commits June 19, 2026 21:05
Add the Rust analog of Spring's OpaqueTokenIntrospector, the first Tier 4
(OAuth2 ecosystem) piece:

- TokenIntrospector trait + RemoteTokenIntrospector: POSTs the token to a
  configured RFC 7662 introspection endpoint (HTTP Basic client auth,
  token + token_type_hint form), and on `active: true` maps the response
  claims to an Authentication (RFC 7662 `username` honored over the OIDC
  preferred_username/sub fallback).
- Implements Verifier, so it is a drop-in alternative to JwksVerifier behind
  a BearerLayer for the opaque-token resource-server pattern (the AS stays the
  source of truth; nothing is trusted locally).
- Fails closed: transport error, non-2xx, non-JSON, or active:false/absent all
  reject.

2 unit tests (active mapping incl. scope→authorities; inactive/missing/non-object
reject) + an end-to-end integration test against an in-process axum RFC 7662
endpoint (real HTTP round-trip + Verifier drop-in). fmt + clippy clean.
Add the Rust analog of Spring's OAuth2AuthorizedClientManager /
OAuth2AuthorizedClientService — the outbound side of OAuth2 (obtaining tokens
to call DOWNSTREAM services, vs the inbound login flow):

- OAuth2AuthorizedClient: a held token (access + optional refresh + expiry +
  scopes) for a (registration, principal) pair; is_expired() with clock-skew
  leeway (None expiry = non-expiring).
- OAuth2AuthorizedClientService trait + InMemoryOAuth2AuthorizedClientService:
  store keyed by (registration_id, principal_name).
- OAuth2AuthorizedClientManager: authorize_client_credentials() performs the
  client-credentials grant (service-to-service), caches the token, and reuses
  it until within the skew window of expiry; on expiry it refreshes (refresh_token
  grant) or re-fetches. refresh() drives the refresh-token grant directly. A
  refresh response that omits a new refresh token retains the existing one
  (RFC 6749 §6). client_secret_basic auth to the token endpoint; fail-mapped to
  OAuth2Error.

4 unit tests (token-response parsing incl. expiry/scope/refresh retention,
missing-access-token error, is_expired skew/missing-expiry) + 2 end-to-end
integration tests against an in-process token endpoint (grant+cache hit-once,
expired+refresh). fmt + clippy clean.
Add the Rust analog of Spring's OidcClientInitiatedLogoutSuccessHandler:

- ClientRegistration gains end_session_endpoint + post_logout_redirect_uri
  fields/setters; the keycloak() preset derives the end_session endpoint from
  the realm issuer.
- The OAuth2 login callback now stores the registration_id + raw id_token in the
  session (new SESSION_KEY_REGISTRATION_ID / SESSION_KEY_ID_TOKEN).
- POST /logout: after deregistering + invalidating the local session, when the
  login provider advertises an end_session_endpoint it redirects the browser
  there with id_token_hint + post_logout_redirect_uri + client_id (RP-initiated
  logout), so the session ends at the IdP too; otherwise redirects to "/".
- oidc_logout_url() builds the end_session URL (exported, testable).

1 unit test (URL build incl. explicit post-logout override + no-end_session →
None) + an end-to-end logout integration test (redirects to end_session with
the id_token hint, local session still invalidated); the existing plain-logout
test still passes. fmt + clippy clean.
…ta [T4.4]

Mount the existing (previously callable-only) AuthorizationServer as a real
OAuth2 HTTP surface:

- AuthorizationServerRouter::router() mounts:
  - POST /oauth2/token — the RFC 6749 token endpoint over the client_credentials
    and refresh_token grants, client_secret_post auth. Success -> 200
    TokenResponse; failure -> the RFC 6749 §5.2 error envelope
    ({error, error_description}; 401 for invalid_client, else 400; codes
    lowercased to the registered RFC names).
  - GET /.well-known/oauth-authorization-server — RFC 8414 Authorization Server
    Metadata (issuer, token_endpoint, grant_types_supported,
    token_endpoint_auth_methods_supported). No jwks_uri: tokens are HS256-signed
    (symmetric), so there is no public verification key to publish.

3 tests via Router oneshot (token issuance for client_credentials incl. scope;
bad-client -> 401 invalid_client; metadata advertises issuer + token endpoint).

Server-side authorization_code grant + PKCE and a client-authenticated
/oauth2/revoke remain a documented follow-up (the AS signs symmetric and has no
authorize endpoint yet). fmt + clippy clean.
Adversarial review of the Tier 4 OAuth2 surface surfaced 9 confirmed findings
(all availability/hardening/observability — no auth bypass); fixes:

- HTTP timeouts (HIGH): the OAuth2 reqwest clients (introspection, outbound
  token, JWKS, login) had no timeout, so a slow/hostile endpoint could hang the
  inbound bearer-verification hot path indefinitely. All now use a shared
  crate::default_http_client() with connect (5s) + read (10s) timeouts; a
  timeout maps to a fail-closed error.
- Non-expiring token (MEDIUM): a token response (incl. refresh) omitting
  expires_in produced a cached token with no expiry that was served forever.
  authorized_client_from_token_response now assumes a bounded fallback lifetime
  (DEFAULT_FALLBACK_TTL_SECONDS) so the token is re-fetched.
- Response-body cap (LOW): introspection + token responses now reject an
  over-large Content-Length (MAX_OAUTH2_RESPONSE_BYTES) before buffering.
- SERVER_ERROR mapping (LOW): the token endpoint maps an internal SERVER_ERROR
  to HTTP 500 with a generic description instead of 400 + the raw signer error.
- redirect() panic (LOW): the login redirect() builds the Location header
  fallibly (falls back to "/"), so a control char in a misconfigured
  end_session_endpoint can't panic the handler.
- Secret redaction (LOW): ClientRegistration (client_secret) and
  OAuth2AuthorizedClient (access/refresh tokens) now have manual Debug impls
  that redact, instead of deriving Debug.

Documented (not code-fixed): no single-flight on concurrent authorize/refresh
(rotating-refresh lost-update) and token-endpoint error bodies surfaced as
status only. All oauth2 lib + integration tests green; fmt + clippy clean.
- Spring Security Parity appendix (EN + ES): mark outbound AuthorizedClientManager,
  RFC 7662 opaque-token introspection, and RP-initiated logout as supported, the
  authorization server as partial (token + RFC 8414 metadata; authorization_code
  grant a follow-up); add an "OAuth2 ecosystem" section; mark the OAuth2-ecosystem
  tier done in the roadmap.
- CHANGELOG: v26.6.33 entry (Tier 4) incl. the review hardening + known
  limitations. MODULES.md: firefly-security oauth2 description expanded.
- Bump workspace + path-dep versions to 26.6.33; refresh Cargo.lock.
- Rebuild and republish both editions (PDF + EPUB, EN + ES) in docs/book/dist.
@ancongui ancongui merged commit 80260cf into main Jun 19, 2026
4 checks passed
@ancongui ancongui deleted the feat/spring-security-tier4-oauth2-ecosystem branch June 19, 2026 19:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant