Secure-by-default reactive OAuth2 resource server for Spring WebFlux. A single auto-configuration turns any Firefly web application into a JWT-validating resource server with a default-deny filter chain and hardened response headers — no security code required, and nothing to silently leak.
- Overview
- Where it sits in the platform
- What it provides
- Key types
- Requirements
- Installation
- Quick Start
- Configuration
- Request flow
- Testing
- License
This module is the resource-server binding of the Firefly hexagonal security platform. It wires Spring Security 6's reactive OAuth2 resource server onto the framework's ports so that every inbound request must present a signature-validated bearer token before it reaches a controller or handler.
It is deliberately product-agnostic. There is no X-Party-Id trusted-header shortcut, no business-role enum, and no fail-open switch. Identity comes only from a JWT whose signature, expiry, and (optionally) issuer and audience are verified in-process; authorization defaults to deny. This closes the historical gaps the platform refactor was designed to eliminate — unverified local token parsing, gateway-trusted headers, and Spring Boot's silently-leaked HTTP-Basic default chain.
The whole module is one @AutoConfiguration class (ResourceServerAutoConfiguration) plus a properties class and a JWT-to-principal converter. Every bean it contributes is @ConditionalOnMissingBean, so an application can override any single piece — the decoder, the authority mapping, the policy engine, the filter chain — without forking the module.
The security platform is layered hexagonally; dependencies point inward, and providers attach as outboard adapters:
security-api → security-spi → security-core → security-webflux → security-resource-server → adapters
(ports + (driven (neutral (reactive (this module: (Vault, KMS,
domain) ports) engine) Spring Security Spring Security 6 OPA, Keycloak,
bindings) resource-server wiring) internal-db, …)
security-apidefines the domain (SecurityPrincipal,Decision,SigningKey) and driving ports.security-spidefines the driven ports this module consumes:KeyManagementPort,AuthorityMappingPort,PolicyDecisionPort,AuditEventPort,SecurityContextPort.security-coresupplies the framework-neutral default implementations (InMemoryKeyManagementAdapter,ConfigurableAuthorityMapper,PrincipalFactory,EmbeddedPolicyDecisionAdapter,LoggingAuditEventAdapter).security-webfluxsupplies the reactive Spring Security glue (FireflyAuthenticationToken,PolicyAuthorizationManager,ReactorSecurityContextAdapter).- This module binds all of the above into a concrete
SecurityWebFilterChainand is delivered to applications transitively via the application starter. - Adapters (key management, policy, idp providers) replace the in-process defaults by simply contributing their own port beans.
This module depends only on security-webflux (which transitively brings security-core, -spi, -api) and the Spring Security OAuth2 resource-server / JOSE artifacts. It imports no vendor SDK.
ResourceServerAutoConfiguration contributes, each gated by @ConditionalOnMissingBean:
- A verifying JWT decoder.
NimbusReactiveJwtDecoder.withPublicKey(...)built from the active RSASigningKeyresolved throughKeyManagementPort.activeSigningKey(), pinned toRS256. If no RSA key resolves, the context fails to start (fail-closed) rather than booting an unprotected server. - A validator chain. A
DelegatingOAuth2TokenValidator<Jwt>always enforcingJwtTimestampValidator(exp/nbf with skew), plusJwtIssuerValidatorwhenissueris configured and a custom audience validator whenaudiencesis non-empty. - Claim-to-authority mapping. An
AuthorityMappingPort(defaultConfigurableAuthorityMapper) driven by configurable role/scope claim paths and authority prefix, normalizing provider-specific claims (Keycloak/Cognito/Entra) into a uniform authority/scope set. - A rich principal.
JwtToFireflyPrincipalConverterreplaces Spring's defaultJwtAuthenticationTokenwith aFireflyAuthenticationTokenwhose principal is aSecurityPrincipal(built byPrincipalFactory.fromClaims(...)), so@AuthenticationPrincipal SecurityPrincipalworks out of the box. - A default-deny filter chain.
fireflySecurityWebFilterChaindisables CSRF, HTTP-Basic, form-login, and logout; permits only the explicitly configuredpermitMatchers; and routesanyExchange()through aPolicyAuthorizationManagerbacked byPolicyDecisionPort. Unconfigured routes are denied. - Hardened response headers on every response:
X-Frame-Options: DENY,Referrer-Policy: no-referrer, a strictContent-Security-Policy(default-src 'none'; frame-ancestors 'none'), and HSTS (includeSubDomains, 365-day max-age). - Framework defaults for the remaining ports —
EmbeddedPolicyDecisionAdapter(collecting anyPolicyRulebeans),LoggingAuditEventAdapter, andReactorSecurityContextAdapter— all overridable.
| Type | Role |
|---|---|
ResourceServerAutoConfiguration |
@AutoConfiguration @EnableWebFluxSecurity entry point; builds the decoder, converter, and SecurityWebFilterChain. |
ResourceServerProperties |
@ConfigurationProperties("firefly.security.resource-server") — permit matchers, issuer, audiences, authority prefix, role/scope claim paths. |
JwtToFireflyPrincipalConverter |
Converter<Jwt, Mono<AbstractAuthenticationToken>> mapping a validated Jwt to a FireflyAuthenticationToken carrying a SecurityPrincipal. |
Ports consumed (from security-spi): KeyManagementPort, AuthorityMappingPort, PolicyDecisionPort, AuditEventPort, SecurityContextPort. Domain types (from security-api): SecurityPrincipal, Decision, SigningKey.
The auto-configuration is registered via META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports.
- Java 21+
- Spring Boot 3.x, Spring Security 6.x
- A reactive web stack (Spring WebFlux)
- A
KeyManagementPortthat can resolve an active RSA signing key (the bundledInMemoryKeyManagementAdaptersuffices for dev; production deployments contribute a Vault/AWS-KMS/Azure-Key-Vault adapter)
The version is managed by the Firefly parent/BOM, so you can usually omit it. In a Firefly application this module is pulled in transitively by the application starter; depend on it directly only when binding a resource server standalone:
<dependency>
<groupId>org.fireflyframework</groupId>
<artifactId>fireflyframework-security-resource-server</artifactId>
</dependency>If you are not inheriting the Firefly parent, pin the version explicitly:
<dependency>
<groupId>org.fireflyframework</groupId>
<artifactId>fireflyframework-security-resource-server</artifactId>
<version>26.06.01</version>
</dependency>With the module on the classpath, the resource server is active with zero code — every route is locked down. Mark public routes explicitly and read the validated principal directly:
firefly:
security:
resource-server:
permit-matchers:
- /actuator/health
- /public/**
issuer: https://idp.example.com/realms/firefly
audiences:
- firefly-apiimport org.fireflyframework.security.api.domain.SecurityPrincipal;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
@RestController
class MeController {
@GetMapping("/api/me")
Mono<String> me(@AuthenticationPrincipal SecurityPrincipal principal) {
return Mono.just(principal.subject());
}
}Add ABAC/ReBAC decisions by contributing one or more PolicyRule beans; they are collected by the default EmbeddedPolicyDecisionAdapter and consulted on every non-permitted exchange, fail-closed:
@Bean
PolicyRule denyReports() {
return (principal, action, resource, context) ->
resource.startsWith("/api/reports") && !principal.scopes().contains("reports.read")
? Mono.just(Decision.deny("reports.read scope required"))
: Mono.just(Decision.permit());
}All keys live under firefly.security.resource-server:
| Property | Default | Description |
|---|---|---|
permit-matchers |
(empty) | Ant-style path patterns served without authentication. The only opt-out from default-deny. |
issuer |
(none) | Expected iss; when set, a JwtIssuerValidator is enforced. |
audiences |
(empty) | Acceptable aud values; when non-empty, an audience validator is enforced. |
authority-prefix |
"" |
Prefix applied to mapped authorities (e.g. ROLE_). |
role-claim-paths |
(empty) | Dot-path claims inspected for roles/groups (defaults cover Keycloak/Cognito/Entra). |
scope-claim-paths |
(empty) | Dot-path claims inspected for OAuth2 scopes. |
With no configuration at all, every route requires a validated bearer token and timestamp validation is always on.
Bearer token → fireflySecurityWebFilterChain
→ NimbusReactiveJwtDecoder (RS256, key from KeyManagementPort)
→ DelegatingOAuth2TokenValidator (timestamp [+ issuer] [+ audience])
→ JwtToFireflyPrincipalConverter → FireflyAuthenticationToken(SecurityPrincipal)
→ authorizeExchange: permitMatchers → permitAll, else PolicyAuthorizationManager (PolicyDecisionPort, default-deny)
→ controller / handler
A missing or malformed token, a failed signature, an expired token, or a wrong issuer/audience yields 401. A validated token whose policy decision is DENY (or whose route is not permitted) yields 403. Only a validated token on an allowed route reaches the handler with 200.
The module ships a @SpringBootTest(webEnvironment = RANDOM_PORT) integration test, ResourceServerIntegrationTest, that boots a real reactive server with the real auto-configuration and exercises the full filter chain over HTTP with WebTestClient. It signs JWTs in-test with Nimbus (RSASSASigner) using the active SigningKey from the wired KeyManagementPort, and asserts the secure-by-default behavior end to end:
- 200 — a public route (
/public/**) is reachable without a token; a protected route returns theSecurityPrincipal.subject()for a valid signed JWT. - 401 — a protected route with no token; an expired token; a forged token signed by an unknown key (a fresh
InMemoryKeyManagementAdapterthe server doesn't trust). - 403 — a route a contributed
PolicyRuledenies, even with a valid token (proving default-deny / policy enforcement is independent of authentication). - Headers —
X-Frame-Options: DENY,Referrer-Policy: no-referrer, andContent-Security-Policyare present on responses.
These mirror the platform's negative-path verification strategy: expired, forged-signature-rejected, and denied-policy paths are proven, not assumed.
Copyright 2024-2026 Firefly Software Foundation.
Licensed under the Apache License, Version 2.0. See LICENSE for details.