Skip to content

fix: let an assertion be presented only once - #44

Open
shreemaan-abhishek wants to merge 1 commit into
fix/assertion-in-response-tofrom
fix/assertion-replay-cache
Open

fix: let an assertion be presented only once#44
shreemaan-abhishek wants to merge 1 commit into
fix/assertion-in-response-tofrom
fix/assertion-replay-cache

Conversation

@shreemaan-abhishek

Copy link
Copy Markdown
Contributor

Closes #37, item 5 of its suggested scope. Stacked on #43, which is stacked on #42; the base branch retargets as each merges.

What was wrong

An assertion could be posted back as many times as its window allowed. #42 bounds that window and #43 ties the assertion to one AuthnRequest, which together shrink the opening a great deal, but neither makes an assertion single-use, and single-use is what "bearer" means: whoever holds it is the subject.

What it does now

login_callback remembers the ID of every assertion it accepts and refuses a response carrying one it has seen. The store is an lua_shared_dict the deployment names through the new replay_dict option, because a library cannot declare one and the entry has to be shared across workers. Unset leaves assertions untracked, which is today's behaviour; a name that no lua_shared_dict matches fails loudly at new() rather than quietly not tracking anything.

How long an entry lives is taken from the assertion rather than from configuration: Conditions/@NotOnOrAfter plus the clock_skew allowance is the last moment the checks in #42 would still accept it, so the cache holds exactly what is still replayable and no more. An assertion that names no expiry has nothing to derive from and is remembered for replay_ttl, 600 seconds by default.

Two smaller points:

  • the key carries sp_issuer, so several SP instances sharing one dict do not collide.
  • lua_shared_dict evicts under pressure. An eviction weakens replay protection silently, so a forcible insert logs a warning naming the dict as full.

Tests

TESTs 21 to 23 in t/assertion-conditions.t. TEST 23 reads the entry's TTL back out of the dict, covering both the derived window and the replay_ttl fallback.

Full run on this branch, t/assertion-conditions.t and t/signed-response.t, 120 subtests, all pass. Rebuilt against #43's lua/ with the new tests kept, the two that should fail do and only those:

Failed 5/69 subtests     # TESTs 21 and 23

TEST 22 passes on both, which is the point of it.

Nothing stopped the same assertion being posted back a second time inside
its validity window. Its ID is remembered now, in an lua_shared_dict the
deployment names, and a second presentation is refused.

The entry lives as long as the assertion's own Conditions leave it usable,
so the cache holds exactly what could still be replayed. An assertion that
names no expiry is remembered for replay_ttl, since nothing in the
assertion says when to stop.

Unset replay_dict leaves assertions untracked, which is what deployments
with no shared dict to spare get today.
@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a588b139-cc1d-4a58-8332-fd7ff2399c7d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

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