Reference

IRefreshTokenStore

Package Trax.Api.Auth.Jwt.Cognito.IssuerNamespace Trax.Api.Auth.Jwt.Cognito.IssuerSource IRefreshTokenStore.cs, InMemoryRefreshTokenStore.cs

NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible for securing systems that use it. See API Security.

The operations a Cognito-compatible auth server needs for opaque refresh tokens: issue, validate, rotate and revoke. Tokens belong to rotation chains bound to a (sub, clientId) pair. InMemoryRefreshTokenStore implements it in process memory.

Signature

public interface IRefreshTokenStore
{
    Task<RefreshTokenHandle> IssueAsync(Guid sub, string clientId, TimeSpan lifetime, CancellationToken ct);
    Task<RefreshTokenClaims?> ValidateAsync(string token, CancellationToken ct);
    Task<RefreshTokenHandle?> RotateAsync(string oldToken, CancellationToken ct);
    Task RevokeAsync(string token, CancellationToken ct);
    Task RevokeAllAsync(Guid sub, string clientId, CancellationToken ct);
}
 
public sealed record RefreshTokenHandle(string Token, DateTimeOffset ExpiresAt);
 
public sealed record RefreshTokenClaims(Guid Sub, string ClientId, DateTimeOffset ExpiresAt);
 
public sealed class InMemoryRefreshTokenStore : IRefreshTokenStore
{
    public InMemoryRefreshTokenStore(TimeProvider? clock = null);
}

Members

MemberDescription
IssueAsync(sub, clientId, lifetime, ct)Starts a new rotation chain and returns its first token.
ValidateAsync(token, ct)The token's RefreshTokenClaims when it is active; null when it is expired, revoked, consumed by rotation, or unknown.
RotateAsync(oldToken, ct)Consumes oldToken and returns a new token in the same chain, with the chain's original expiry. null when oldToken is not active.
RevokeAsync(token, ct)Revokes the whole chain the token belongs to.
RevokeAllAsync(sub, clientId, ct)Revokes every chain of the pair: global sign-out.
RefreshTokenHandle.Token, .ExpiresAtThe opaque token and when its chain expires.
RefreshTokenClaims.Sub, .ClientId, .ExpiresAtWhat the token is bound to, and when it expires.

Returns

See each member above.

Behavior

  • Rotation does not extend a session: every token in a chain expires when the first one does.
  • Presenting a token that rotation already consumed is reuse. RotateAsync revokes the whole chain and returns null, so the token the earlier rotation issued stops working too: the store cannot tell the client from someone replaying a copy, so the session ends for both. An implementation of the interface must do the same.
  • Concurrent rotations of one token: one caller wins, every other gets null. The losers presented a spent token, so they count as reuse and the chain, the winner's new token included, is revoked.
  • InMemoryRefreshTokenStore issues 256-bit random tokens, holds them in process memory, and loses them on restart.

Example

var store = new InMemoryRefreshTokenStore();
var userId = Guid.NewGuid();
 
var refresh = await store.IssueAsync(userId, "mobile-app-client-id", TimeSpan.FromDays(30), ct);
 
var next = await store.RotateAsync(refresh.Token, ct);
if (next is null)
{
    // Rotated already, revoked or expired: the client signs in again.
}

See also