Skip to content

Let a Logout Token the sink failed on be delivered again - #566

Merged
kirill-abblix merged 3 commits into
developfrom
fix/logout-replay-release
Sep 23, 2026
Merged

kirill-abblix merged 3 commits into
developfrom
fix/logout-replay-release

Conversation

@kirill-abblix

Copy link
Copy Markdown
Member

Closes #565.

A back-channel logout receiver reserves the Logout Token's jti while validating it, before the notification sink runs. If the sink returned a description, threw, or the request was cancelled, that reservation stayed in place. The provider's retransmission was then refused as a replay, and the session never ended.

What changes:

  • IReplayCache gets ReleaseAsync. ReplayCacheBase gets an abstract RemoveAsync, which DistributedReplayCache implements with IDistributedCache.RemoveAsync. The member is required, so a host with its own store gets a build error on upgrade and adds one delete. A default that did nothing would compile and keep the bug in every store we don't ship.
  • The server's ConfiguredReplayCache passes the release on to the store it wraps and logs it at Debug, next to the reservation events it already writes. A host that runs both the server and the receiver shares that cache.
  • ILogoutNotificationSink now says that a refusal gives the token back, so the provider's retransmission reaches the sink again. DistributedReplayCache notes that a release makes its existing race between instances wider.
  • BackChannelLogoutHandler releases the reservation whenever the sink did not end the sessions. The release runs with CancellationToken.None, because a cancelled request is one of the cases it handles. If the release fails, it is logged under a new event id (ReservationKept) and the provider still gets the sink's outcome.
  • The replay cache is optional on the handler, as it already is on PushDeliveryHandler, so a host that builds the handler itself around a validator that reserves nothing still works.
  • PushDeliveryHandler still records only after its sink. Its remarks no longer say the cache cannot release.

2.4 is not patched. Hosts on 2.4 keep the old behavior until they move to 3.0.

Validating a Logout Token reserves its jti in the replay cache, and that
happens before the notification sink runs. When the sink returned a
description, threw, or the request was cancelled, the reservation stood,
so the provider's retransmission was refused as a replay and the session
was never ended.

IReplayCache gains ReleaseAsync, and ReplayCacheBase an abstract
RemoveAsync that DistributedReplayCache implements over
IDistributedCache.RemoveAsync. The member is required: a default that did
nothing would compile and keep this behavior in every store the library
does not ship. The server's ConfiguredReplayCache forwards the release.

BackChannelLogoutHandler gives the reservation back whenever the sink did
not end the sessions. The release is not cancellable, since a cancelled
request is one of the cases it answers, and a failed release is logged
under a new event id instead of replacing the sink's outcome. The replay
cache is optional on the handler, as it is on PushDeliveryHandler, for a
host whose validator reserves nothing.

PushDeliveryHandler keeps recording after its sink, and its remarks no
longer say the cache cannot release.

Ref #565
A replay refused at validation, a notification carrying no jti, and a
release on ReplayCacheBase each get a test row, and the retry theory
asserts a throw where it expects one instead of catching whatever the
first request raised.

ConfiguredReplayCache records a release at Debug under its own event id,
so a debug log no longer shows one identifier reserved twice with
nothing between.

JwtAssertionAuthenticatorBase no longer says a reservation cannot be
given back. ILogoutNotificationSink says a refusal returns the token to
the replay guard, and DistributedReplayCache says what a release does to
the race it already has between instances. The handler's log message
says the sink did not report success rather than that nothing was done.
Spellings in the new text are American.

Ref #565
)

After a release, only the next presentation of the token passes as
fresh: it writes the entry back, and later ones are refused again. The
DistributedReplayCache remark said the token passed until it expired.

The release row in ConfiguredReplayCacheTests releases an identifier the
message template does not contain, so it fails when the decorator logs a
different identifier.

Ref #565
@sonarqubecloud

Copy link
Copy Markdown

@kirill-abblix
kirill-abblix merged commit 4ab31de into develop Sep 23, 2026
26 checks passed
@kirill-abblix
kirill-abblix deleted the fix/logout-replay-release branch September 23, 2026 11:50
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.

Back-channel logout: when the sink fails, the Logout Token can never be delivered again (jti is reserved before the sink runs)

1 participant