Class DefaultRefreshTokenService
- Namespace
- Fdyay.IdentityServer.Services.Tokens
- Assembly
- Fdyay.IdentityServer.Core.dll
Supports all classes in the .NET class hierarchy and provides low-level services to derived classes. This is the ultimate base class of all .NET classes; it is the root of the type hierarchy.
public class DefaultRefreshTokenService : IRefreshTokenService
- Inheritance
-
DefaultRefreshTokenService
- Implements
- Inherited Members
Remarks
Duende 8.0.1's DefaultRefreshTokenService, measured, for its one protected virtual step:
AcceptConsumedTokenAsync(RefreshToken), asked when a token already rotated away is presented again.
Where ours differs: the methods that create, validate and rotate are not virtual, and a consumed token the hook does not accept revokes the lineage it belongs to, where Duende's only refuses it — an override can accept, but cannot refuse without revoking. The hook is asked only for a token issued to the client presenting it. The default hook accepts within ConsumedRefreshTokenGracePeriod, zero unless set (0.167.0), where Duende's answers false.
Constructors
DefaultRefreshTokenService(IRefreshTokenStore, TimeProvider, IEventService, IEnumerable<ISessionCoordinationService>?)
public DefaultRefreshTokenService(IRefreshTokenStore store, TimeProvider time, IEventService events, IEnumerable<ISessionCoordinationService>? sessions = null)
Parameters
storeIRefreshTokenStoreWhere refresh tokens are kept.
timeTimeProviderThe clock.
eventsIEventServiceWhere a replay is reported.
sessionsIEnumerable<ISessionCoordinationService>Asked whether a token's session is still there, when one is registered —
AddServerSideSessions()registers it. Enumerated rather than required, so a host without server-side sessions resolves this service as before and nothing is asked, as nothing is in Duende without a session store.
Remarks
The constructor this had before 0.150.0, kept: the options read as the defaults.
DefaultRefreshTokenService(IRefreshTokenStore, TimeProvider, IEventService, IEnumerable<ISessionCoordinationService>?, IOptions<IdentityServerOptions>?)
public DefaultRefreshTokenService(IRefreshTokenStore store, TimeProvider time, IEventService events, IEnumerable<ISessionCoordinationService>? sessions, IOptions<IdentityServerOptions>? options)
Parameters
storeIRefreshTokenStoreWhere refresh tokens are kept.
timeTimeProviderThe clock.
eventsIEventServiceWhere a replay is reported.
sessionsIEnumerable<ISessionCoordinationService>Asked whether a token's session is still there, when one is registered.
optionsIOptions<IdentityServerOptions>The server's options, for DeleteOneTimeOnlyRefreshTokensOnUse. Null reads as the defaults.
Remarks
New in 0.150.0, an overload rather than a parameter added to the one above, so a host compiled against that one still binds. Logs nothing: the constructor below takes the logger.
DefaultRefreshTokenService(IRefreshTokenStore, TimeProvider, IEventService, IEnumerable<ISessionCoordinationService>?, IOptions<IdentityServerOptions>?, ILogger<DefaultRefreshTokenService>)
[ActivatorUtilitiesConstructor]
public DefaultRefreshTokenService(IRefreshTokenStore store, TimeProvider time, IEventService events, IEnumerable<ISessionCoordinationService>? sessions, IOptions<IdentityServerOptions>? options, ILogger<DefaultRefreshTokenService> logger)
Parameters
storeIRefreshTokenStoreWhere refresh tokens are kept.
timeTimeProviderThe clock.
eventsIEventServiceWhere a replay is reported.
sessionsIEnumerable<ISessionCoordinationService>Asked whether a token's session is still there, when one is registered.
optionsIOptions<IdentityServerOptions>The server's options, for DeleteOneTimeOnlyRefreshTokensOnUse and ConsumedRefreshTokenGracePeriod. Null reads as the defaults.
loggerILogger<DefaultRefreshTokenService>Where a consumed token presented again is logged, accepted or refused. Duende's.
Remarks
New in 0.167.0, an overload as the one above was. The one the container builds, as the longest and as marked for
ActivatorUtilities.
Properties
RefreshTokenStore
Where refresh tokens are kept. Duende's.
protected IRefreshTokenStore RefreshTokenStore { get; }
Property Value
TimeProvider
The clock. Duende's.
protected TimeProvider TimeProvider { get; }
Property Value
Methods
AcceptConsumedTokenAsync(RefreshToken)
Whether a refresh token already consumed — rotated away under OneTimeOnly — is accepted when presented again.
Duende's hook. By default, only within ConsumedRefreshTokenGracePeriod of its
consumption, which is zero unless the host sets it: otherwise a consumed token presented again is a replay, refused,
and its lineage revoked.
protected virtual Task<bool> AcceptConsumedTokenAsync(RefreshToken refreshToken)
Parameters
refreshTokenRefreshToken
Returns
Remarks
Accepting weakens replay detection (RFC 9700 §4.14): a consumed token that is accepted is redeemed again and rotated into a second successor, so a stolen handle used within whatever this accepts goes unnoticed. A host that needs it — a grace period for a client that lost the response carrying its new token, or races itself — keeps it narrow. Refusing here always revokes the lineage. Duende's answers false; the grace period is ours (0.167.0).
CreateRefreshTokenAsync(ClaimsPrincipal, Client, IEnumerable<string>, bool, string?, IEnumerable<string>?, string?, Token?, string?, string?, CancellationToken)
Issues a refresh token, bound to a DPoP key when the client proved it holds one.
public Task<string> CreateRefreshTokenAsync(ClaimsPrincipal subject, Client client, IEnumerable<string> scopes, bool isOpenId, string? dpopKeyThumbprint = null, IEnumerable<string>? authorizedResourceIndicators = null, string? sessionId = null, Token? accessToken = null, string? requestedResourceIndicator = null, string? description = null, CancellationToken cancellationToken = default)
Parameters
subjectClaimsPrincipalThe user the token is issued for; its subject id and every claim are stored with it.
clientClientThe client the token is issued to; only that client may redeem it, and its lifetime policy applies.
scopesIEnumerable<string>The scopes granted, which every later token request on this refresh token is held to.
isOpenIdboolWhether the grant included
openid, so a refresh can issue an identity token.dpopKeyThumbprintstringThe JWK thumbprint of the DPoP key the client proved, binding the token to it; null when unbound.
authorizedResourceIndicatorsIEnumerable<string>The APIs the authorization named by resource indicator (RFC 8707), which every later token request on this refresh token is held to. Null or empty for a grant whose authorization named none.
sessionIdstringThe user's session id (
sid), so ending the session can find and revoke the token.accessTokenTokenThe access token issued alongside, stored with the refresh token for the resource it was issued for.
requestedResourceIndicatorstringThe resource indicator the access token was requested for, if the request named one.
descriptionstringWhat the user or client called this grant, shown in the user's grant list.
cancellationTokenCancellationTokenCancels the store's write.
Returns
RotateAsync(string, RefreshToken, Client, CancellationToken)
Rotates the refresh token per the client's usage policy, returning the handle to return.
public Task<string> RotateAsync(string handle, RefreshToken token, Client client, CancellationToken cancellationToken = default)
Parameters
handlestringtokenRefreshTokenclientClientcancellationTokenCancellationToken
Returns
Remarks
Under OneTimeOnly the predecessor is consumed rather than deleted, so a later presentation of it
is recognisable as a replay — unless PersistentGrantOptions.DeleteOneTimeOnlyRefreshTokensOnUse is on. Under ReUse nothing is consumed and the same handle comes back: a
client on that policy is meant to present it repeatedly, and treating the second use as a replay would
revoke a working session. Under either, a Sliding token's lifetime is extended from its use, never past
AbsoluteRefreshTokenLifetime when that is above zero — the reused one in place, under its handle (0.174.0).
A token TryRedeemAsync(string, RefreshToken, Client, CancellationToken) has taken is already consumed, or deleted, and is not consumed again.
TryRedeemAsync(string, RefreshToken, Client, CancellationToken)
Takes a validated refresh token for the one request about to be answered with it: false when another request took it first, and the request is then refused as a replay would be.
public Task<bool> TryRedeemAsync(string handle, RefreshToken token, Client client, CancellationToken cancellationToken = default)
Parameters
handlestringThe handle the client presented.
tokenRefreshTokenThe token ValidateRefreshTokenAsync(string, Client, CancellationToken) returned for it.
clientClientThe client redeeming it.
cancellationTokenCancellationTokenCancels the store's write.
Returns
Remarks
Ours (security), 0.176.0. Validating a token reads it; until this, rotating it consumed it in a second step, so two
requests presenting one OneTimeOnly token at the same moment both read it unconsumed and were both answered
with a successor — two live lineages from one token, and no replay detected. The token endpoint asks this once
every check on the request has passed and before any token is issued. Duende reads and consumes in two steps, as
this did. This default answers true, so a refresh token service of a host's own behaves as before.
ValidateRefreshTokenAsync(string, Client, CancellationToken)
The token behind a handle, or null if it cannot be used.
public Task<RefreshToken?> ValidateRefreshTokenAsync(string handle, Client client, CancellationToken cancellationToken = default)
Parameters
handlestringclientClientcancellationTokenCancellationToken
Returns
Remarks
Null covers absent, expired, issued to another client, and replayed. The caller answers all four the
same way — invalid_grant — and deliberately learns no more than that: which one it was tells an
attacker whether a handle was ever real, and the client cannot act differently on any of them.
A replay is not only refused here; it revokes the lineage it belongs to. A consumed token presented within
PersistentGrantOptions.ConsumedRefreshTokenGracePeriod of its consumption is not a replay — off by default.