Table of Contents

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

store IRefreshTokenStore

Where refresh tokens are kept.

time TimeProvider

The clock.

events IEventService

Where a replay is reported.

sessions IEnumerable<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

store IRefreshTokenStore

Where refresh tokens are kept.

time TimeProvider

The clock.

events IEventService

Where a replay is reported.

sessions IEnumerable<ISessionCoordinationService>

Asked whether a token's session is still there, when one is registered.

options IOptions<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

store IRefreshTokenStore

Where refresh tokens are kept.

time TimeProvider

The clock.

events IEventService

Where a replay is reported.

sessions IEnumerable<ISessionCoordinationService>

Asked whether a token's session is still there, when one is registered.

options IOptions<IdentityServerOptions>

The server's options, for DeleteOneTimeOnlyRefreshTokensOnUse and ConsumedRefreshTokenGracePeriod. Null reads as the defaults.

logger ILogger<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

IRefreshTokenStore

TimeProvider

The clock. Duende's.

protected TimeProvider TimeProvider { get; }

Property Value

TimeProvider

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

refreshToken RefreshToken

Returns

Task<bool>

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

subject ClaimsPrincipal

The user the token is issued for; its subject id and every claim are stored with it.

client Client

The client the token is issued to; only that client may redeem it, and its lifetime policy applies.

scopes IEnumerable<string>

The scopes granted, which every later token request on this refresh token is held to.

isOpenId bool

Whether the grant included openid, so a refresh can issue an identity token.

dpopKeyThumbprint string

The JWK thumbprint of the DPoP key the client proved, binding the token to it; null when unbound.

authorizedResourceIndicators IEnumerable<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.

sessionId string

The user's session id (sid), so ending the session can find and revoke the token.

accessToken Token

The access token issued alongside, stored with the refresh token for the resource it was issued for.

requestedResourceIndicator string

The resource indicator the access token was requested for, if the request named one.

description string

What the user or client called this grant, shown in the user's grant list.

cancellationToken CancellationToken

Cancels the store's write.

Returns

Task<string>

The handle the client presents to redeem the token.

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

handle string
token RefreshToken
client Client
cancellationToken CancellationToken

Returns

Task<string>

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

handle string

The handle the client presented.

token RefreshToken

The token ValidateRefreshTokenAsync(string, Client, CancellationToken) returned for it.

client Client

The client redeeming it.

cancellationToken CancellationToken

Cancels the store's write.

Returns

Task<bool>

True when this request may be answered with tokens; false when it must be refused.

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

handle string
client Client
cancellationToken CancellationToken

Returns

Task<RefreshToken>

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.