Skip to content

Validate docs/ADRs against code + fix DynamoDB refresh-token forensic-retention TTL#37

Merged
wolpert merged 4 commits into
mainfrom
docs/validate-against-code
Jun 6, 2026
Merged

Validate docs/ADRs against code + fix DynamoDB refresh-token forensic-retention TTL#37
wolpert merged 4 commits into
mainfrom
docs/validate-against-code

Conversation

@wolpert
Copy link
Copy Markdown
Contributor

@wolpert wolpert commented Jun 6, 2026

Summary

Validated the project docs and all 16 ADRs against the actual code, fixed the inaccuracies, and corrected one latent behavior bug uncovered during validation. Most of the diff is documentation; the only production code change is in the DynamoDB refresh-token repository.

./gradlew check passes (full Spotless lint + tests + JaCoCo coverage gates). A focused security review of the code change vs v1.3.1 found no security regression.

Documentation fixes

  • README / operator-guide / transactional-semantics: DynamoDB uses two tables (PkAuthCore + PkAuthUsers), not one; native TTL attribute is ttl (not expiresAt) and there is no MagicLink item (magic links are never persisted); OTP hashing is HMAC-SHA256(pepper, code), not Argon2id; migration list extended through V10; backup/OTP consume atomicity wording corrected (id-keyed mark-used; the hash compare is a deliberately racy service-layer step).
  • ADRs (8 corrected, decisions all still hold): 0003 runtime deps (HikariCP + slf4j-api); 0004 Dropwizard wiring (class names, 6 provision methods, admin now Dagger-wired via PkAuthFullComponent); 0005 revocation + refresh tokens shipped in 1.1.0 (cross-ref 0013/0015); 0008 "Three"→"Four" attribute definitions; 0009/0010/0012 version drift; 0015 delete(UserHandle, String) signature.

Code fix — DynamoDB refresh-token forensic-retention TTL

DynamoDbRefreshTokenRepository set the native ttl to plain expiresAt, so DynamoDB's background sweep pruned used/revoked refresh rows at expiry — ~30 days earlier than the JDBI backend and contradicting both RefreshTokenItem's Javadoc and the documented cleanupRetention window.

ttl was also doing double duty as the rotateAtomically freshness/replay check, so it couldn't simply be extended without risking acceptance of expired tokens. The fix separates the two concerns:

  • New expiresAtEpoch attribute (= expiresAt.epochSecond, the value the old ttl carried) drives the freshness/expiry check — semantics unchanged.
  • ttl now carries expiresAt + cleanupRetention for native pruning only, restoring JDBI parity and making the Javadoc true.
  • deleteExpiredBefore filters on expiresAtEpoch to keep expires_at < cutoff semantics.
  • Backward-compatible constructor overload takes the retention Duration; the existing 2-arg constructor delegates with the documented 30-day default.

Pre-existing rows lacking expiresAtEpoch fail the freshness check (fail-closed → family scorch → re-auth); no migration concern beyond a one-time re-auth window.

Testing

  • All 10 DynamoDB refresh-token integration tests pass against DynamoDB Local, including the concurrent-rotation race and expiry tests, plus a new nativeTtlExtendsExpiryByCleanupRetention assertion.
  • ./gradlew check green across all modules.

Security review

Reviewed the code diff vs v1.3.1. The expiry/replay guarantees are preserved exactly (freshness check compares the same value it always did); the only behavioral change is that native TTL prunes 30 days later, which keeps used/revoked rows around longer — fail-safe for replay detection and consistent with the JDBI backend. No findings.

🤖 Generated with Claude Code

wolpert and others added 4 commits June 6, 2026 11:45
…ity claims

Validated README.md, docs/operator-guide.md, and docs/transactional-semantics.md
against the code. Fixes for claims that did not match the implementation:

- DynamoDB persistence uses TWO tables (PkAuthCore + PkAuthUsers per ADR 0008
  and PkAuthDynamoTables), not a single physical table. The operator guide told
  operators to provision one table; corrected to provision both.
- DynamoDB-native TTL attribute is `ttl` (epoch seconds), not `expiresAt`, and is
  set on Challenge / OneTimePasscode (and the 1.1.0 token rows) — there is no
  MagicLink item; magic-link tokens are never persisted.
- OTP codes are hashed with HMAC-SHA256(pepper, code), not Argon2id; relabel the
  secrets-table row accordingly (backup codes do use Argon2id, with no pepper).
- Operator-guide migration list now covers V8/V9/V10 (access_tokens,
  refresh_tokens, refresh_tokens amr column), not just V1-V7.
- transactional-semantics: backup/OTP `consume` atomicity is the id-keyed
  mark-used step (the hash compare is a deliberately racy service-layer step);
  clarified which contracts have concurrency parity tests; fixed "Two surfaces"
  -> "Three surfaces".

Verified-correct and left unchanged: JWT HS256/ES256 support, 32-byte secret
hard-fail at boot, TokenTtlPolicy, AccessTokenStore, ceremony-finish ordering,
rotateAtomically across all three backends, the
concurrentRotationExactlyOneSucceedsFamilyRevoked parity test, UserDeletionService
fan-out, and the admin-module wiring matrix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e TTL

DynamoDbRefreshTokenRepository set the native `ttl` attribute to
expiresAt.epochSecond, so DynamoDB's background sweep pruned used/revoked
refresh rows at expiry — ~30 days earlier than the JDBI backend, which keeps
them for the documented cleanupRetention window. RefreshTokenItem's own Javadoc
already claimed ttl = expiresAt + cleanupRetention, so the code contradicted both
the comment and JDBI parity.

The `ttl` attribute was also doing double duty as the rotateAtomically freshness
check (`#ttl > :nowEpoch`), so it could not simply be extended without treating
expired tokens as fresh (a replay-defense regression). Split the two concerns:

- New `expiresAtEpoch` attribute (= expiresAt.epochSecond) drives the freshness
  condition; `ttl` now carries expiresAt + cleanupRetention for native pruning.
- deleteExpiredBefore filters on `expiresAtEpoch` to keep `expires_at < cutoff`
  semantics matching the JDBI cleanup SQL.
- Add a backward-compatible constructor overload taking the retention Duration;
  the existing 2-arg constructor delegates with the documented 30-day default.

Pre-release (1.1.0-SNAPSHOT), so no item-schema migration concern. Verified
against DynamoDB Local: all 10 refresh-token integration tests pass, including
the concurrent-rotation race and expiry tests, plus a new
nativeTtlExtendsExpiryByCleanupRetention assertion. operator-guide.md updated to
describe the per-type ttl semantics.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Validated all 16 ADRs against the current code. Decisions all still hold; fixed
the claims that no longer match:

- 0003: runtime deps are JDBI 3 + Postgres driver + HikariCP + slf4j-api, not
  "JDBI 3 + Postgres driver only".
- 0004: PersistenceBindings is a class (not a record); module types are
  PkAuthCeremonyResource / PkAuthDropwizardAuthenticator (not Passkey*);
  PkAuthComponent now has six provision methods (added userDeletionService,
  refreshHandler); admin is now Dagger-wired via PkAuthFullComponent
  (PkAuthModule + AltFlowsModule) with PkAuthAdminResource, direct instantiation
  only for a host-supplied AdminService.
- 0005: token revocation and refresh tokens, listed as deferred non-goals,
  shipped in 1.1.0 (AccessTokenStore / ADR 0015, pk-auth-refresh-tokens / ADR
  0013); the stateless-JWT default still stands. Annotated and marked resolved.
- 0008: "Three attribute definitions" -> "Four" (pk, sk, gsi1pk, gsi1sk).
- 0009: Jackson databind 3.1.3 -> 3.1.4; annotations 2.21 -> 2.22.
- 0010: Dropwizard 5.0.1 -> 5.0.2 (current catalog pin).
- 0012: Micronaut 4.10.23 -> 4.10.25 (current catalog pin).
- 0015: AccessTokenStore.delete(String) -> delete(UserHandle, String) (IDOR
  defense), in both the interface snippet and the logout example.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cosmetic line-wrapping only (no semantic change) so :check's spotlessJavaCheck
passes. Follows up the TTL fix in 23da547.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@wolpert wolpert enabled auto-merge (rebase) June 6, 2026 19:31
@wolpert wolpert merged commit 18a2f4c into main Jun 6, 2026
3 checks passed
@wolpert wolpert deleted the docs/validate-against-code branch June 6, 2026 19:33
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