Driven ports (service-provider interfaces) for the Firefly security platform. Pure reactive contracts — no Spring web, no provider SDK. Providers implement these as adapters; the core engine programs against them.
- Overview
- Where it sits
- Ports
- Design contract
- Requirements
- Installation
- Usage
- Dependencies
- Testing
- Documentation
- Contributing
- License
fireflyframework-security-spi is the outbound (driven) side of the Firefly security platform's hexagon. It declares the interfaces the framework needs from the outside world — token validation, authority mapping, policy decisions, key management, secrets, audit, caching, revocation, issuer registry, tenant resolution and principal enrichment — and nothing else. There are no implementations here, no auto-configuration, and no transitive dependency on Spring Security or any vendor SDK.
The module exists to enforce the hexagonal inversion described in the security design: the framework-neutral engine (fireflyframework-security-core) and its reactive binding (fireflyframework-security-webflux) depend only on these interfaces, while concrete behavior — Nimbus JWT decoding, OPA/Cerbos policy engines, Vault/KMS key stores, Redis caches, Keycloak/Cognito/Entra issuer metadata — lives in adapter modules that implement them. Swapping a provider is a dependency swap, not a code change.
Two principles from the platform design shape every contract in this module:
- Secure-by-default, hard. Validating ports (
TokenValidationPort,PolicyDecisionPort,RelationshipPort) are fail-closed by contract: any error must surface as a denial or aTokenValidationException, never as a silent permit. The Javadoc on each port states the fail-closed expectation that adapters and contract tests enforce. - Product-agnostic, always. The ports speak only in generic primitives —
subject,tenantId,Set<String>authorities/scopes,Map<String, Object>claims/attributes. Noparty,contract,productor business-role enum appears. Products re-introduce their own domain through thePrincipalAttributeContributorPortextension seam, without the framework ever knowing about it.
The platform is built strictly in dependency order; this module is layer 3.
security-bom version/dependency pins (Spring Security 6.x, Nimbus, provider SDKs)
security-api driving ports + domain model (SecurityPrincipal, Decision, BearerToken, ...)
security-spi ◀── this driven ports (interfaces only)
security-core framework-neutral engine; programs against the SPI
security-webflux reactive bindings (ServerHttpSecurity, ReactiveSecurityContextHolder → SecurityContextPort)
*-resource-server-starter JWT + opaque resource server
adapters concrete implementations of these ports (opa, vault, redis, keycloak, internal-db, ...)
security-spi depends only on security-api (for the shared domain model) plus reactor-core and slf4j-api. It is the single seam through which security-core reaches every external system, and it is the join between the two platform tiers: the IdP tier and the security tier connect through exactly one of these ports — TokenValidationPort.
All ports live in org.fireflyframework.security.spi. Every method that performs I/O returns a reactive type (Mono/Flux); the one pure mapping port (AuthorityMappingPort) returns plain collections.
| Port | Signature(s) | Role | Typical adapters |
|---|---|---|---|
TokenValidationPort |
Mono<SecurityPrincipal> validate(BearerToken) |
The single seam between the IdP and security tiers. Validates a bearer token (JWT signature/issuer/audience/expiry, or RFC 7662 introspection for opaque tokens) and projects it into a SecurityPrincipal. Throws TokenValidationException on any failure. |
Nimbus JWT decoder, idp introspection bridge |
AuthorityMappingPort |
Set<String> mapAuthorities(Map<String,Object>), Set<String> mapScopes(Map<String,Object>) |
Normalizes provider-specific claims into framework authorities/scopes (realm_access.roles, cognito:groups, roles, …). Pure, synchronous. |
per-issuer claim mappers |
PolicyDecisionPort |
Mono<Decision> authorize(SecurityPrincipal, String action, String resource, Map<String,Object> context) |
Externalized ABAC decision point. Fail-closed: any error yields denial/indeterminate. | embedded engine, OPA, Cerbos |
RelationshipPort |
Mono<Boolean> check(String subject, String relation, String object) |
Zanzibar-style ReBAC relationship check. Fail-closed. | OpenFGA, SpiceDB |
KeyManagementPort |
Mono<SigningKey> activeSigningKey(), Flux<SigningKey> verificationKeys(), Mono<String> jwkSetJson(), Mono<Void> rotate() |
Manages first-party signing keys and the published JWKS, with rotation-with-overlap so in-flight tokens stay verifiable. | in-memory dev, Vault Transit, AWS/Azure KMS |
SecretsPort |
Mono<String> getSecret(String name) |
Resolves named secrets from an external store; fail-closed at startup. | Vault, AWS Secrets Manager, Azure Key Vault |
AuditEventPort |
Mono<Void> emit(SecurityAuditEvent) |
Sink for security audit events. Must never block the request path or throw. | log, JDBC, Kafka, SIEM |
TokenIntrospectionCachePort |
Mono<SecurityPrincipal> get(String), Mono<Void> put(String, SecurityPrincipal, Duration), Mono<Void> evict(String) |
Caches opaque-token introspection results, TTL bounded by the token's exp. |
Caffeine, Redis |
RevocationPort |
Mono<Boolean> isRevoked(String tokenId), Mono<Void> revoke(String tokenId, Instant expiresAt) |
Tracks revoked tokens/sessions so an unexpired, validly-signed token can still be rejected. | Redis |
IssuerRegistryPort |
Mono<Boolean> isTrusted(String), Mono<TrustedIssuer> findByIssuer(String), Flux<TrustedIssuer> issuers() |
Registry of trusted issuers for a multi-tenant resource server; allow-lists iss and selects the per-issuer decoder. |
static config, Keycloak/Cognito/Entra metadata |
TenantResolverPort |
Mono<String> resolveTenant(SecurityPrincipal) |
Resolves the generic tenant discriminator (from issuer, a claim, or host mapping). | claim/host resolvers |
SecurityContextPort |
Mono<SecurityPrincipal> currentPrincipal() |
Stack-neutral accessor for the current validated principal; empty when unauthenticated. | webflux ReactiveSecurityContextHolder binding |
PrincipalAttributeContributorPort |
Mono<Map<String,Object>> contribute(SecurityPrincipal) |
Product extension seam. Enriches a validated principal with domain-specific attributes; merged into SecurityPrincipal.attributes. The framework contributes nothing here. |
product-supplied enrichers |
These ports reference the domain model owned by fireflyframework-security-api: BearerToken, SecurityPrincipal, Decision, SigningKey, TrustedIssuer and SecurityAuditEvent, plus the TokenValidationException raised by TokenValidationPort.
- Interfaces only. This module defines no classes, no beans, no auto-configuration. Wiring happens in
security-core/security-webfluxand the starters; implementations live in adapters. - Reactive-first. Every I/O method returns
Mono/Flux. The lone exception isAuthorityMappingPort, which is a pure, in-memory claim transformation and intentionally synchronous. - Fail-closed. Validating ports must treat errors as denials, never as permits — this is the heart of the secure-by-default posture.
- No domain leakage. Contracts carry only generic primitives (
String,Set<String>,Map<String, Object>,Duration,Instant) and api-owned domain types. Product concepts enter only viaPrincipalAttributeContributorPort. - Interface Segregation. Ports are narrow and capability-scoped; an adapter implements only the seams it can satisfy, rather than one fat aggregate.
- Java 21+
- Project Reactor 3.x (
reactor-core) - Maven 3.9+
Most consumers depend on security-core/security-webflux (which pull this in transitively). Adapter authors depend on it directly. Versions are managed by the Firefly parent/BOM, so the version can be omitted:
<dependency>
<groupId>org.fireflyframework</groupId>
<artifactId>fireflyframework-security-spi</artifactId>
</dependency>If you are not inheriting the Firefly parent, pin the version explicitly:
<dependency>
<groupId>org.fireflyframework</groupId>
<artifactId>fireflyframework-security-spi</artifactId>
<version>26.06.01</version>
</dependency>You implement these ports in an adapter module and let the platform's auto-configuration discover your bean. A minimal PolicyDecisionPort adapter, fail-closed by contract:
package com.example.security;
import org.fireflyframework.security.api.domain.Decision;
import org.fireflyframework.security.api.domain.SecurityPrincipal;
import org.fireflyframework.security.spi.PolicyDecisionPort;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.Map;
@Component
public class OpaPolicyDecisionAdapter implements PolicyDecisionPort {
private final OpaClient opa; // your engine binding
public OpaPolicyDecisionAdapter(OpaClient opa) {
this.opa = opa;
}
@Override
public Mono<Decision> authorize(SecurityPrincipal principal, String action,
String resource, Map<String, Object> context) {
return opa.evaluate(principal.subject(), action, resource, context)
.map(allowed -> allowed ? Decision.permit() : Decision.deny())
// fail-closed: any error becomes a denial, never a permit
.onErrorReturn(Decision.deny());
}
}Conversely, the core engine consumes the ports — it never sees your adapter type, only the interface:
import org.fireflyframework.security.spi.TokenValidationPort;
import org.fireflyframework.security.spi.PolicyDecisionPort;
public class AuthorizationFilter {
private final TokenValidationPort tokenValidation;
private final PolicyDecisionPort policy;
public AuthorizationFilter(TokenValidationPort tokenValidation, PolicyDecisionPort policy) {
this.tokenValidation = tokenValidation;
this.policy = policy;
}
public Mono<Decision> authorize(BearerToken token, String action, String resource) {
return tokenValidation.validate(token) // → SecurityPrincipal (or TokenValidationException)
.flatMap(p -> policy.authorize(p, action, resource, Map.of()));
}
}Deliberately minimal — this is a contracts module:
| Dependency | Scope | Why |
|---|---|---|
fireflyframework-security-api |
compile | shared domain model (SecurityPrincipal, Decision, BearerToken, SigningKey, TrustedIssuer, SecurityAuditEvent) and TokenValidationException |
reactor-core |
compile | Mono/Flux return types |
slf4j-api |
compile | logging facade for adapters; no binding shipped |
junit-jupiter, assertj-core |
test | contract guard test |
There is no dependency on Spring, Spring Security, Spring Web, or any provider SDK — those belong to the binding and adapter modules downstream.
The module ships one guard test, SpiContractTest, that reflectively enforces the SPI's structural contract so it cannot drift:
allPortsAreInterfaces— every port (includingAuthorityMappingPort) is declared as an interface, never an abstract class.reactivePortMethodsReturnPublishers— every method on every reactive port returns aMonoorFlux. The pure mapping port (AuthorityMappingPort) is explicitly exempted and listed separately, documenting why it is allowed to be synchronous.
Behavioral conformance is not tested here — adapters are verified against the per-SPI contract-test base classes and negative-path suites (expired / wrong-aud / wrong-iss / unknown-kid / forged-signature-rejected / denied-policy) provided by fireflyframework-security-test. This module proves only that the contracts themselves stay reactive, interface-only and fail-closed by shape.
Run the guard test with:
mvn -pl fireflyframework-security-spi test- Firefly Framework documentation hub and module catalog: github.com/fireflyframework
- Security platform design:
fireflyframework-securityhexagonal architecture (api → spi → core → webflux → resource-server → adapters)
Contributions are welcome. Please read the CONTRIBUTING.md guide for details on our code of conduct, development process, and how to submit pull requests. When adding or changing a port, keep it interface-only, reactive (or document why it is not), fail-closed, and free of product-domain types — and update SpiContractTest accordingly.
Copyright 2024-2026 Firefly Software Foundation.
Licensed under the Apache License, Version 2.0. See LICENSE for details.