RevocationCutoffChecker Class
Refuses a token issued before a revocation cutoff recorded against its subject or its session.
public class RevocationCutoffChecker : Abblix.Oidc.Server.Features.Tokens.Revocation.IRevocationCutoffCheckerInheritance System.Object → RevocationCutoffChecker
Implements IRevocationCutoffChecker
Remarks
Applies only to tokens this server minted. A cutoff names a principal in this server's own namespace,
while the validator that consults this also sees tokens minted elsewhere - a client's
private_key_jwt assertion (RFC 7523 Section 3, where sub is the client_id), an
assertion from a federated issuer under the jwt-bearer grant, a request object, a software statement.
Their subjects are strings from other namespaces, so matching one against our cutoff would refuse a
stranger's token for a revocation that has nothing to do with it - and under client_credentials
this server's own sub is a client_id too, so the two namespaces genuinely collide.
Measured against iat rather than auth_time. Both are OPTIONAL in general (RFC 7519
Section 4.1.6), but this server issues iat on every token it mints - RFC 9068 Section 2.2 makes it
REQUIRED for the at+jwt profile its access tokens use - while auth_time is REQUIRED only
when max_age was requested or it was asked for as an essential claim (OpenID Connect Core 1.0
Section 2). A check built on the second would pass silently for most tokens, which is worse than no
check. A token arriving without iat is left alone: there is nothing to measure, and refusing it
would revoke on the strength of a claim that was never there.
A cutoff therefore has two sides, and the token side alone would be a control that can be walked
around: iat is stamped afresh by every authorization, so a browser session the revocation never
reached would mint a replacement past the cutoff on the first attempt and on every attempt after.
IsSessionRefusedAsync(AuthSession) is the other side, consulted wherever a session is about to be used:
by the authorization endpoint before it reuses one, and by the token endpoint before it redeems a grant
authorized from one.
The comparison is against the whole second the token declares. A JWT's iat is a whole number of
seconds, so a token minted in the same second as a revocation reads as older than it and is refused. That
errs towards refusing a token the revocation did not mean to catch, which is the direction to err in.
Clock differences between instances run the other way and are not bounded by the token, which is what
RevocationCutoffSkew answers - on this side only. The session side takes no
tolerance, because there the same widening would refuse the fresh sign-in a user answers the refusal
with, and the retry lands in the same window.
Constructors
RevocationCutoffChecker(ILogger<RevocationCutoffChecker>, IRevocationCutoffRegistry, IIssuerProvider, IOptions<OidcOptions>, IClientInfoProvider, ISubjectTypeConverter) Constructor
Refuses a token issued before a revocation cutoff recorded against its subject or its session.
public RevocationCutoffChecker(Microsoft.Extensions.Logging.ILogger<Abblix.Oidc.Server.Features.Tokens.Revocation.RevocationCutoffChecker> logger, Abblix.Oidc.Server.Features.Storages.IRevocationCutoffRegistry cutoffRegistry, Abblix.Oidc.Server.Features.Issuer.IIssuerProvider issuerProvider, Microsoft.Extensions.Options.IOptions<Abblix.Oidc.Server.Common.Configuration.OidcOptions> options, Abblix.Oidc.Server.Features.ClientInformation.IClientInfoProvider clientInfoProvider, Abblix.Oidc.Server.Features.PairwiseIdentifiers.ISubjectTypeConverter subjectTypeConverter);Parameters
logger Microsoft.Extensions.Logging.ILogger<RevocationCutoffChecker>
Records a refusal, so a revoked token is distinguishable from an expired one.
cutoffRegistry IRevocationCutoffRegistry
The registry of subject- and session-level revocation cutoffs.
issuerProvider IIssuerProvider
Names this server, so a cutoff is only measured against tokens it minted.
options Microsoft.Extensions.Options.IOptions<OidcOptions>
Carries the tolerance the comparison allows for clock differences.
clientInfoProvider IClientInfoProvider
Resolves the client a token names, so a pairwise pseudonym can be opened back into the subject a host would revoke.
subjectTypeConverter ISubjectTypeConverter
Opens that pseudonym.
Remarks
Applies only to tokens this server minted. A cutoff names a principal in this server's own namespace,
while the validator that consults this also sees tokens minted elsewhere - a client's
private_key_jwt assertion (RFC 7523 Section 3, where sub is the client_id), an
assertion from a federated issuer under the jwt-bearer grant, a request object, a software statement.
Their subjects are strings from other namespaces, so matching one against our cutoff would refuse a
stranger's token for a revocation that has nothing to do with it - and under client_credentials
this server's own sub is a client_id too, so the two namespaces genuinely collide.
Measured against iat rather than auth_time. Both are OPTIONAL in general (RFC 7519
Section 4.1.6), but this server issues iat on every token it mints - RFC 9068 Section 2.2 makes it
REQUIRED for the at+jwt profile its access tokens use - while auth_time is REQUIRED only
when max_age was requested or it was asked for as an essential claim (OpenID Connect Core 1.0
Section 2). A check built on the second would pass silently for most tokens, which is worse than no
check. A token arriving without iat is left alone: there is nothing to measure, and refusing it
would revoke on the strength of a claim that was never there.
A cutoff therefore has two sides, and the token side alone would be a control that can be walked
around: iat is stamped afresh by every authorization, so a browser session the revocation never
reached would mint a replacement past the cutoff on the first attempt and on every attempt after.
IsSessionRefusedAsync(AuthSession) is the other side, consulted wherever a session is about to be used:
by the authorization endpoint before it reuses one, and by the token endpoint before it redeems a grant
authorized from one.
The comparison is against the whole second the token declares. A JWT's iat is a whole number of
seconds, so a token minted in the same second as a revocation reads as older than it and is refused. That
errs towards refusing a token the revocation did not mean to catch, which is the direction to err in.
Clock differences between instances run the other way and are not bounded by the token, which is what
RevocationCutoffSkew answers - on this side only. The session side takes no
tolerance, because there the same widening would refuse the fresh sign-in a user answers the refusal
with, and the retry lands in the same window.
Methods
RevocationCutoffChecker.CheckAsync(JsonWebTokenPayload) Method
The refusal a cutoff calls for on this token, or null when no cutoff reaches it.
public System.Threading.Tasks.Task<Abblix.Jwt.JwtValidationError?> CheckAsync(Abblix.Jwt.JsonWebTokenPayload payload);Parameters
payload JsonWebTokenPayload
The payload of a token that has already passed signature and lifetime checks.
Implements CheckAsync(JsonWebTokenPayload)
Returns
System.Threading.Tasks.Task<JwtValidationError>
A validation error when the token is refused, otherwise null.
RevocationCutoffChecker.IsSessionRefusedAsync(AuthSession) Method
Whether a cutoff refuses this authentication session, so nothing may mint against it.
public System.Threading.Tasks.Task<bool> IsSessionRefusedAsync(Abblix.Oidc.Server.Features.UserAuthentication.AuthSession session);Parameters
session AuthSession
A session about to be used: one the authorization endpoint is considering reusing, or the one a grant presented at the token endpoint was authorized from.
Implements IsSessionRefusedAsync(AuthSession)
Returns
System.Threading.Tasks.Task<System.Boolean>
true when the session must not be used.
Remarks
Without this the token side alone is half a control. A cutoff refuses tokens already issued, and
iat is stamped afresh by every new authorization, so a browser session the revocation never
touched can mint a replacement that clears the cutoff on the first try - and keeps doing so.
Measured against AuthenticationTime, not against a flag. A sign-in after the suspension is lifted produces a later authentication time and passes, which is the same property that lets a revoked subject sign in again with nothing to clean up. A boolean would refuse the fresh session too, and since the host's new session carries the same subject the request would loop back here for as long as the record is kept.