Move replay protection into Abblix.JWT, one store for every JWT profile - #337
Merged
Conversation
… profile Two implementations of the same idea had grown up in two packages: the server reserved jti values for DPoP proofs and client assertions, the security-events receiver reserved them for Security Event Tokens, and neither could reach the other because a receiver must not depend on the OpenID Connect server. The primitive now lives one layer below both, as Abblix.JWT's IReplayCache and its distributed-cache implementation, so a host running both shares a single store instead of keeping two. Three things the move had to preserve rather than rebuild. The cache key prefix is a plain literal now: it used to be derived from the namespace it lived in, and this is the second relocation of that namespace, each of which would have silently orphaned the entries of a running deployment and left a rolling upgrade with no replay protection until they aged out. The server's clock skew and its two log event ids stay in the server, applied by decoration, so an operator's runbook still keys off the numbers it knows. And the retention that a Security Event Token receiver remembers an identifier for moves onto the validation profile, beside the freshness window it has to outlive, where raising one without the other is visible instead of split across two files. The contract shipped in 2.3 is marked obsolete and keeps working: it still resolves, and its implementation now stores through the moved one, so the deprecated and current spellings cannot disagree about whether an identifier has been seen. A host that implemented it rather than merely resolving it keeps deciding where replay state lives - the registration detects that case and bridges to it, because a security control that lapses while its registration still looks healthy is the worst way for one to fail.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Two implementations of one idea had grown up in two packages: the server reserved
jtivalues for DPoP proofs and client assertions, the security-events receiver reserved them for Security Event Tokens, and neither could reach the other because a receiver must not depend on the OpenID Connect server. The primitive now lives one layer below both, asAbblix.Jwt.ReplayPrevention.IReplayCachewith a distributed-cache implementation, so a host running both shares one store instead of keeping two.What the move had to preserve rather than rebuild
The 2.3 contract
Features.ReplayPrevention.IJwtReplayCacheand its implementation are marked obsolete and keep working: they still resolve, and the implementation now stores through the moved one, so the deprecated and current spellings cannot disagree about whether an identifier has been seen. A host that implemented the contract rather than merely resolving it keeps deciding where replay state lives - the registration detects that case and bridges to it, because a security control that lapses while its registration still looks healthy is the worst way for one to fail.The three duplicate registrations collapse into one
AddReplayPrevention(), which is idempotent and applies the server's policy by decoration, so which package registered the store first cannot decide whether that policy applies.Coverage
The moved implementation carries its unit tests across (first reservation, repeat, prefix isolation, the lifetime handed to the store, a floored expiry already in the past, an empty identifier). Two new cases in
ServiceCollectionOverrideTestspin the parts a reader would otherwise have to trust: a host implementation of the deprecated contract stays in charge, and repeated feature registrations produce exactly one cache. 3722 tests pass across ten suites.