Security R2DBC Adapter¶
Persist token revocation records through a relational R2DBC adapter.
Provide DatabaseClient, apply the documented schema, and wire R2dbcRevocationAdapter into the security engine. Verify expiry and revocation queries against the same database dialect and clock behavior used in production.
| Maven artifact | Packaging | Source project |
|---|---|---|
org.fireflyframework:fireflyframework-security-adapter-r2dbc |
jar |
modules/security-adapter-r2dbc |
Part of Security and identity.
In the onboarding service¶
Store revoked token IDs when the application uses PostgreSQL for that security state. Explicitly connect the revocation port to the JWT validation policy; registering its bean alone does not reject a token.
Add the component¶
Installation and version availability explains source builds and published releases.
<dependency>
<groupId>org.fireflyframework</groupId>
<artifactId>fireflyframework-security-adapter-r2dbc</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.
R2dbcRevocationAdapter implements RevocationPort only. It does not implement TokenIntrospectionCachePort, and the default resource-server JWT decoder does not consult it automatically. Connect the port explicitly to the application's token-validation policy before relying on revocation.
Provide the adapter as the application's RevocationPort bean, wired from a Spring R2DBC DatabaseClient:
import org.fireflyframework.security.adapter.r2dbc.R2dbcRevocationAdapter;
import org.fireflyframework.security.spi.RevocationPort;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.r2dbc.core.DatabaseClient;
@Configuration
class SecurityRevocationConfig {
@Bean
RevocationPort revocationPort(DatabaseClient databaseClient) {
return new R2dbcRevocationAdapter(databaseClient);
}
}
Application code (or the resource server) then uses the port:
// Revoke a token until its own expiry (e.g. on logout)
revocationPort.revoke(jti, accessTokenExpiry).then(...);
// Fail-closed check during bearer validation
revocationPort.isRevoked(jti)
.flatMap(revoked -> revoked ? Mono.error(new SecurityException("Token is revoked")) : continueChain());
With spring.r2dbc.* configured and the security_revoked_token table present, no further setup is needed.
Configuration and wiring¶
This module declares no @ConfigurationProperties class of its own. Use the constructor/port contracts above and the configuration of the host or related core module.
Public API and extension points¶
| Type | Contract and responsibility |
|---|---|
R2dbcRevocationAdapter |
Relational-database RevocationPort backed by R2DBC. |
Related components¶
| Related component | Why it belongs nearby |
|---|---|
| Security API | Define product-neutral principals, authorization decisions, driving use cases, and the Secure annotation. |
| Security SPI | Define driven ports for policy, relationships, revocation, keys, secrets, and other external security services. |
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-adapter-r2dbc -am verify
./mvnw -pl :fireflyframework-security-adapter-r2dbc -am install
verify runs the selected projects' tests. install makes the current build available in your local Maven repository.
Representative test sources:
Some tests use Testcontainers. A working Docker-compatible container runtime is required for those fixtures; inspect their annotations and the build output to distinguish executed tests from environment-dependent skips.
Mock, local-container, and application-context tests do not establish acceptance against a live provider account. Validate the intended provider deployment and credentials separately.
Test fixture¶
The revokeIsIdempotentUpsert test demonstrates one concrete behavior. Excerpt from R2dbcRevocationAdapterIntegrationTest.java. The test class supplies its fixtures and imports.
@Test
void revokeIsIdempotentUpsert() {
revocationPort.revoke("token-2", Instant.now().plusSeconds(120)).block();
revocationPort.revoke("token-2", Instant.now().plusSeconds(600)).block();
StepVerifier.create(revocationPort.isRevoked("token-2")).expectNext(true).verifyComplete();
}
Detailed reference¶
Where it sits in the hexagonal security platform¶
| Layer | Module | Role |
|---|---|---|
api |
fireflyframework-security-api |
Driving ports + neutral domain (SecurityPrincipal, decisions, …). |
spi |
fireflyframework-security-spi |
Driven ports, including RevocationPort. |
core |
fireflyframework-security-core |
Vendor-neutral engine; depends on ports, never on adapters. |
webflux |
fireflyframework-security-webflux |
Reactive binding of the core to Spring WebFlux. |
resource-server |
fireflyframework-security-resource-server |
JWT validation and hardened response headers; revocation integration is application-owned. |
adapters |
fireflyframework-security-adapter-r2dbc (this module) |
Concrete RevocationPort (and sibling-port) implementations. |
The shipped resource-server decoder does not automatically call RevocationPort. Connect isRevoked(tokenId) to an application-owned reactive JWT validation/authorization path, reject revoked tokens, and choose fail-closed handling for storage errors. Adding this adapter alone does not invalidate bearer tokens. This adapter depends only on fireflyframework-security-api and fireflyframework-security-spi. It never reaches back into the core or webflux layers, preserving the hexagonal dependency direction.
What it provides¶
R2dbcRevocationAdapter (implements org.fireflyframework.security.spi.RevocationPort)¶
A reactive, R2DBC-backed revocation list. Constructed with a single Spring DatabaseClient:
It implements the two RevocationPort operations:
| Method | Behavior |
|---|---|
Mono<Void> revoke(String tokenId, Instant expiresAt) |
Idempotent upsert into security_revoked_token (INSERT ... ON CONFLICT (token_id) DO UPDATE). expiresAt may be null (bound as a SQL NULL), meaning the entry never self-expires; otherwise it is stored as a TIMESTAMPTZ at UTC. |
Mono<Boolean> isRevoked(String tokenId) |
Returns true only for an entry that exists and is still active (expires_at IS NULL OR expires_at > now()). An unknown token returns false via defaultIfEmpty(false). |
Self-expiring entries. Because isRevoked ignores rows whose expires_at has passed, an expired revocation no longer counts as revoked. The token has expired anyway. This decouples correctness from cleanup: a scheduled job can prune the table at leisure (the idx_security_revoked_token_expires_at index supports this) without any race against active checks.
Schema¶
The adapter targets a single table. A reference PostgreSQL DDL ships at
src/main/resources/org/fireflyframework/security/adapter/r2dbc/schema-postgresql.sql:
CREATE TABLE IF NOT EXISTS security_revoked_token (
token_id VARCHAR(512) PRIMARY KEY,
expires_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_security_revoked_token_expires_at
ON security_revoked_token (expires_at);
Apply it the way your application manages schema. Flyway, Liquibase, or spring.sql.init.*. The adapter does not create or migrate the table itself.
Dependencies¶
Compile scope (kept deliberately minimal. One capability, no vendor lock-in beyond R2DBC):
fireflyframework-security-api. neutral security domain.fireflyframework-security-spi. definesRevocationPort.spring-r2dbc. providesDatabaseClient.slf4j-api,lombok(provided).
Test scope only: spring-boot-starter-data-r2dbc, spring-boot-starter-test, r2dbc-postgresql, testcontainers (postgresql + junit-jupiter), and reactor-test.
Testing¶
R2dbcRevocationAdapterIntegrationTest is a real end-to-end integration test. No mocks, no in-memory stand-in. It boots a @SpringBootTest against a live PostgreSQL 16 instance started by Testcontainers (postgres:16-alpine, Docker required), wires spring.r2dbc.* from the container via @DynamicPropertySource, and loads the schema through spring.sql.init.mode=always.
The test exercises the full contract of the adapter:
- a revoked, unexpired token reports
isRevoked == true; - an unknown token reports
false(the not-revoked default); - an entry whose
expires_atis in the past no longer counts as revoked; revokeis an idempotent upsert (revoking the sametoken_idtwice updates, not duplicates).
It is named *Test so it runs under Surefire. The module does not bind the Failsafe plugin. Running the suite requires a working Docker environment.
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.