Skip to content

feat(jwt,core): stateful access tokens + user-deletion fan-out#13

Merged
wolpert merged 1 commit into
mainfrom
feature/stateful-access-tokens
May 17, 2026
Merged

feat(jwt,core): stateful access tokens + user-deletion fan-out#13
wolpert merged 1 commit into
mainfrom
feature/stateful-access-tokens

Conversation

@wolpert
Copy link
Copy Markdown
Contributor

@wolpert wolpert commented May 17, 2026

Summary

Two related primitives for the 1.1.0 train.

Feature 3 — AccessTokenStore SPI in pk-auth-jwt. PkAuthJwtIssuer.issue() always calls store.record(jti, …) before returning; PkAuthJwtValidator.validate() calls store.exists(jti) alongside RevocationCheck. The default AccessTokenStore.noop() preserves stateless JWT behaviour — hosts wire JdbiAccessTokenStore or DynamoDbAccessTokenStore (both shipped) to make logout / admin-revoke take effect before exp. No TokenMode enum: presence of a real bean is the signal. ADR 0015.

Cross-cutting — UserDeletionService in pk-auth-core. Single fan-out point that runs every registered UserDeletionListener and returns a typed UserDeletionResult. Listeners shipped for credentials, backup codes, OTPs, and access tokens. Each adapter wires them through its native DI mechanism (Spring auto-collection, Dagger @IntoSet, Micronaut Collection<T>). Sequential + best-effort rather than motif's single-shared-transaction model because pk-auth's persistence SPIs span JDBI / DynamoDB / in-memory with no shared substrate. ADR 0016.

Breaking SPI additions

  • CredentialRepository.deleteByUserHandle(UserHandle) -> int
  • OtpRepository.deleteByUserHandle(UserHandle) -> int

All shipped implementations (in-memory, JDBI, DynamoDB) are updated. Documented in CHANGELOG.md.

Schema

  • Flyway V8 adds access_tokens (jti, user_handle, audience, device_id, issued_at, expires_at). PkAuthJdbiSchema.CURRENT_SCHEMA_VERSION bumped to 8.
  • DynamoDB: two-item layout on PkAuthCore (primary jti-keyed + user-indexed pointer) with native ttl for async expiry.

Test plan

  • `./gradlew :pk-auth-jwt:test` — passes including 5 new AccessTokenStore integration tests (noop, recorded jti, absent jti rejected, present jti accepted, deletion listener forwarding).
  • `./gradlew :pk-auth-testkit:test` — InMemoryAccessTokenStore driven by AccessTokenStoreScenarios (5 cases).
  • `./gradlew :pk-auth-persistence-jdbi:test` — JdbiAccessTokenStoreIntegrationTest passes against Testcontainers Postgres (5 parity scenarios, schema bumped from V6 → V8).
  • `./gradlew :pk-auth-persistence-dynamodb:test` — DynamoDbAccessTokenStoreIntegrationTest passes against DynamoDB Local (5 parity scenarios, both primary + user-index items written).
  • `./gradlew :pk-auth-core:test` — UserDeletionServiceTest covers happy path, failing-listener-doesn't-stop-others, and each shipped listener's forwarding.
  • `./gradlew check` — 159-task full build green across every module + every example demo. JaCoCo coverage thresholds met on pk-auth-jwt (80%) and pk-auth-core (80%) after adding the integration test classes.

🤖 Generated with Claude Code

Introduces two related primitives for 1.1.0:

1. AccessTokenStore SPI in pk-auth-jwt. PkAuthJwtIssuer.issue() always
   calls store.record(jti, ...) before returning; PkAuthJwtValidator
   calls store.exists(jti) after signature/audience checks. The default
   AccessTokenStore.noop() preserves stateless JWT behaviour; hosts wire
   JdbiAccessTokenStore or DynamoDbAccessTokenStore (both shipped) to
   make logout / admin-revoke take effect before exp. RevocationCheck
   stays as the deny-list escape hatch. ADR 0015.

2. UserDeletionService + UserDeletionListener SPI in pk-auth-core. One
   call (deleteUser) fans out to every registered listener; failures
   are logged and counted, the service returns a UserDeletionResult.
   Listeners shipped for credentials, backup codes, OTPs, and access
   tokens; adapters auto-register them (Spring @bean collection, Dagger
   @IntoSet, Micronaut Collection<T> injection). Sequential + best-effort
   rather than motif's single-transaction model because pk-auth's
   persistence SPIs span JDBI / DynamoDB / in-memory. ADR 0016.

Breaking SPI additions: CredentialRepository.deleteByUserHandle and
OtpRepository.deleteByUserHandle. All three persistence variants
updated. Flyway V8 ships the new access_tokens table.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@wolpert wolpert merged commit c6b86fa into main May 17, 2026
1 of 2 checks passed
@wolpert wolpert deleted the feature/stateful-access-tokens branch May 17, 2026 03: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