Skip to content

Jafly. Security WebFlux Binding

Security WebFlux Binding

Bind the framework principal and authorization policy to reactive Spring Security/WebFlux.

Use the principal projection and URL policy enforcement components in a reactive security chain. Configure security-core and the relevant token validation module; this binding alone is not a complete identity provider deployment.

Maven artifact Packaging Source project
org.fireflyframework:fireflyframework-security-webflux jar modules/security-webflux

Part of Security and identity.

In the onboarding service

Project the authenticated reviewer into the framework principal and connect URL policy decisions to WebFlux. Token signature validation is configured in the resource-server module.

Add the component

Installation and version availability explains source builds and published releases.

<dependency>
    <groupId>org.fireflyframework</groupId>
    <artifactId>fireflyframework-security-webflux</artifactId>
    <version>26.10.20-SNAPSHOT</version>
</dependency>

Quick start

The Java examples in this section are API fragments for application code. Use the stated imports and supply the application-owned collaborators described alongside them.

Wire the PEP into a SecurityWebFilterChain (normally done for you by security-autoconfigure):

import org.fireflyframework.security.spi.PolicyDecisionPort;
import org.fireflyframework.security.webflux.authz.PolicyAuthorizationManager;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;

@Bean
SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, PolicyDecisionPort pdp) {
    return http
            .authorizeExchange(ex -> ex
                    .pathMatchers("/actuator/health").permitAll()
                    // every other exchange goes through the policy decision point (fail-closed)
                    .anyExchange().access(new PolicyAuthorizationManager(pdp)))
            .build();
}

Read the current principal from a service through the port. No Spring Security import required:

import org.fireflyframework.security.api.domain.SecurityPrincipal;
import org.fireflyframework.security.spi.SecurityContextPort;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

@Service
public class GreetingService {

    private final SecurityContextPort securityContext;

    public GreetingService(SecurityContextPort securityContext) {
        this.securityContext = securityContext; // ReactorSecurityContextAdapter at runtime
    }

    public Mono<String> greet() {
        return securityContext.currentPrincipal()
                .map(SecurityPrincipal::subject)
                .map(subject -> "Hello, " + subject)
                .defaultIfEmpty("Hello, anonymous");
    }
}

When you mint a principal yourself (e.g. In a custom authentication converter), wrap it so Spring authorities and the rich principal stay in sync:

SecurityPrincipal principal = SecurityPrincipal.builder()
        .subject("u1")
        .authorities(java.util.Set.of("ROLE_ADMIN"))
        .build();
Authentication auth = new FireflyAuthenticationToken(principal);

Configuration and wiring

The firefly.security.webflux.header-bridge namespace is bound by PrincipalHeaderBridgeProperties.java. Nested configuration is declared in the linked class. Defaults are the field initializers.

Setting Java type Source default
firefly.security.webflux.header-bridge.enabled boolean false
firefly.security.webflux.header-bridge.tenant-header String "X-Tenant-Id"
firefly.security.webflux.header-bridge.user-header String "X-User-Id"
firefly.security.webflux.header-bridge.roles-header String "X-User-Roles"
firefly.security.webflux.header-bridge.tenant-required boolean true
firefly.security.webflux.header-bridge.tenant-claim-paths List<String> new ArrayList<>(List.of("tenant_ids"))

Property-based wiring conditions found in this module:

Public API and extension points

Type Contract and responsibility
PrincipalHeaderBridgeProperties Configuration for the principal→header bridge.
FireflyAuthenticationToken Spring Security org.springframework.security.core.Authentication whose principal is the framework's SecurityPrincipal.
PrincipalSupport Recovers a SecurityPrincipal from any Spring Authentication.
PolicyAuthorizationManager URL policy enforcement point.
PrincipalHeaderBridgeWebFilter Derives trusted internal request headers from the validated token , never from the client.
Related component Why it belongs nearby
Security API Define product-neutral principals, authorization decisions, driving use cases, and the Secure annotation.
Security Core Evaluate the framework security model with authority mapping, policy decisions, introspection caching, and principal projection.

Build and test

Use Java 25. From the repository root, build this component with its required dependencies:

git clone https://github.com/fireflyframework/fireflyframework-jafly.git
cd fireflyframework-jafly
./mvnw -pl :fireflyframework-security-webflux -am verify
./mvnw -pl :fireflyframework-security-webflux -am install

verify runs the selected projects' tests. install makes the current build available in your local Maven repository.

Representative test sources:

Test fixture

The overwritesTrustedHeadersFromToken test demonstrates one concrete behavior. Excerpt from PrincipalHeaderBridgeWebFilterTest.java. The test class supplies its fixtures and imports.

@Test
void overwritesTrustedHeadersFromToken() {
    SecurityPrincipal principal = SecurityPrincipal.builder()
            .subject("user-1")
            .authorities(Set.of("idp-admin"))
            .claims(Map.of("tenant_ids", List.of("tenant-a")))
            .build();
    MockServerWebExchange exchange = MockServerWebExchange.from(
            MockServerHttpRequest.get("/api/x").header("X-Tenant-Id", "evil")); // client spoof
    CapturingChain chain = new CapturingChain();

    StepVerifier.create(filter(principalPort(principal)).filter(exchange, chain)).verifyComplete();

    assertThat(chain.calls).hasValue(1);
    HttpHeaders headers = chain.captured.get().getRequest().getHeaders();
    assertThat(headers.getFirst("X-Tenant-Id")).isEqualTo("tenant-a"); // spoof overwritten
    assertThat(headers.getFirst("X-User-Id")).isEqualTo("user-1");
    assertThat(headers.getFirst("X-User-Roles")).isEqualTo("idp-admin");
}

Detailed reference

Where it sits

The Firefly security platform is hexagonal; modules depend strictly inward:

security-api      driving ports + domain (SecurityPrincipal, Decision, Obligation, use-cases)
   ▲
security-spi      driven ports (PolicyDecisionPort, SecurityContextPort, TokenValidationPort, ...)
   ▲
security-core     framework-neutral engine (EmbeddedPolicyDecisionAdapter, authority normalization, authz engine)
   ▲
security-webflux  ◀── THIS MODULE — Spring Security WebFlux binding
   ▲
security-resource-server-starter   JWT resource server, explicit signing-key configuration, hardened headers
   ▲
adapters          OPA / Cerbos / OpenFGA · Vault / KMS · idp providers (keycloak, cognito, azure-ad, internal-db)

This module depends only on security-api and security-core (which transitively pulls security-spi) plus Spring Security itself. It defines no provider, no transport and no token format of its own. It is pure glue. The bearer-validation pipeline, multi-tenant issuer resolution, security headers and auto-configuration live one layer up in security-resource-server-starter and security-autoconfigure, which wire the beans this module supplies into a SecurityWebFilterChain. This module ships no AutoConfiguration.imports and no runnable application.yaml; its types are constructed and wired by those higher layers.

Key types

Type Package Role
FireflyAuthenticationToken ...webflux.authentication A Spring AbstractAuthenticationToken whose principal is a SecurityPrincipal. Authorities are derived from principal.authorities() as SimpleGrantedAuthoritys; getName() returns principal.subject(). Exposes securityPrincipal() to recover the rich principal. Marked authenticated on construction (it is only ever built from an already-validated principal).
PrincipalSupport ...webflux.authentication Stateless helper that recovers a SecurityPrincipal from any Spring Authentication. Prefers the principal carried by a FireflyAuthenticationToken; falls back to a principal already set as getPrincipal(); otherwise builds a minimal SecurityPrincipal from the authentication's name and granted authorities (so third-party / JWT Authentications still project cleanly). Null-safe.
ReactorSecurityContextAdapter ...webflux.context Implements SecurityContextPort (SPI). Reads ReactiveSecurityContextHolder.getContext(), keeps only an authenticated Authentication, and projects it via PrincipalSupport::extract. Emits an empty Mono when there is no authenticated context.
PolicyAuthorizationManager ...webflux.authz Implements Spring's ReactiveAuthorizationManager<AuthorizationContext>. the URL PEP. Denies unauthenticated requests; for authenticated requests it calls PolicyDecisionPort.authorize(...) with the HTTP method as the action, the request path as the resource, and a {method, path} context map, mapping Decision.granted() to an AuthorizationDecision.

Ports

  • Implements org.fireflyframework.security.spi.SecurityContextPort (via ReactorSecurityContextAdapter).
  • Consumes org.fireflyframework.security.spi.PolicyDecisionPort (injected into PolicyAuthorizationManager; satisfied by security-core's EmbeddedPolicyDecisionAdapter by default, or by an OPA/Cerbos/OpenFGA adapter).
  • Projects org.fireflyframework.security.api.domain.SecurityPrincipal and reads org.fireflyframework.security.api.domain.Decision.

Secure-by-default behavior

PolicyAuthorizationManager.check(...) is deny-biased at every branch:

  • No authentication / unauthenticated → AuthorizationDecision(false) (the defaultIfEmpty and isAuthenticated() filter close the gap).
  • Authenticated, policy abstains → permit. With security-core's EmbeddedPolicyDecisionAdapter and no rules registered, ABAC is not in use and the decision defers to RBAC at the PEP; configured rules apply deny-overrides.
  • Authenticated, policy denies → AuthorizationDecision(false).
  • Policy errors / PDP unreachable → onErrorReturn(new AuthorizationDecision(false)). fail-closed, never permit-on-error.

This matches design decision D4 (secure-by-default-hard): default-deny, validated bearer upstream, fail-closed everywhere.

Dependencies

Provided by the Firefly parent/BOM (org.fireflyframework:fireflyframework-parent), so versions are managed for you:

  • fireflyframework-security-api. SecurityPrincipal, Decision.
  • fireflyframework-security-core. neutral engine, incl. EmbeddedPolicyDecisionAdapter (transitively brings fireflyframework-security-spi).
  • spring-security-core, spring-security-web, spring-webflux. the binding surface this module exists to adapt.
  • slf4j-api, lombok (provided).

It has no Spring Boot, provider-SDK or auto-configuration dependency; that wiring belongs to the starters above it.

<dependency>
    <groupId>org.fireflyframework</groupId>
    <artifactId>fireflyframework-security-webflux</artifactId>
</dependency>

Testing

Unit-tested with JUnit 5, AssertJ, reactor-test (StepVerifier), spring-security-test and spring-test mocks. No running server, no Spring context:

  • PolicyAuthorizationManagerTest drives the PEP through MockServerWebExchange / AuthorizationContext and asserts all four decision branches: denies unauthenticated requests (Mono.empty()), permits an authenticated request when the EmbeddedPolicyDecisionAdapter abstains, denies when a PolicyDecisionPort returns Decision.deny(...), and fails closed when the port emits an error.
  • ReactorSecurityContextAdapterTest verifies the principal is projected out of the reactive context (seeded via ReactiveSecurityContextHolder.withAuthentication(...)) and that currentPrincipal() completes empty when no security context is present.

Run them with:

./mvnw -pl :fireflyframework-security-webflux -am test

Continue the application

Use the Security Resource Server guide for the next part of this integration. The runnable submission/query service is in Getting started.

Source and further reading