UserSession
One refresh-token session. Created at login and registration, rotated on refresh, and revoked by logout, a password change, or reuse detection.
DANGER
UserSession is a database entity. Never return it from an endpoint: it carries the current and previous refresh-token hashes and the surrogate keys. Map it to a DTO that exposes only the fields the client needs, such as Device, Browser, Os, and LastActiveAt for a session list.
Usage
using Microsoft.EntityFrameworkCore;
using AlmightyShogun.AspNet.Auth.Credentials;
public sealed class SessionAuditService(AppDbContext database)
{
public Task<List<UserSession>> GetActiveSessionsAsync(int userId)
=> database.UserSessions
.Where(session => !session.IsRevoked)
.Where(session => session.UserId == userId)
.Where(session => session.ExpiresAt > DateTimeOffset.UtcNow)
.ToListAsync();
}Fields
Id: int
The surrogate key. Never leaves the server; the refresh token is the only handle a client has on a session.
UserId: int
The owning user. Sessions cascade with the user, so deleting an account takes its sessions with it.
RefreshTokenHash: string
Hash of the current refresh token, uniquely indexed. The token itself is never stored, so a database copy cannot be used to refresh.
PreviousRefreshTokenHash: string?
Hash of the token this one replaced. Presenting it again after the grace period is what identifies a stolen token and revokes every session for the user. Cleared as that fires, so one retired hash triggers detection once.
Default: null
App: string?
The application audience this session belongs to. Refresh only matches sessions for the currently resolved application, so a token from one app cannot refresh another.
Default: null
ExpiresAt: DateTimeOffset
When the session stops refreshing. Set a refresh window ahead of the moment the session opens and set the same way again on each refresh rather than accumulating, and never past the ceiling set by AbsoluteSessionLifetimeDays.
CreatedAt: DateTimeOffset
When the session began. The absolute lifetime cap is measured from here, not from the last refresh.
LastActiveAt: DateTimeOffset
When the session last refreshed. Also what the reuse grace period is measured against.
IsRevoked: bool
Whether the session was ended deliberately, by logout, a password change, or reuse detection. Revoked rows are kept rather than deleted.
Default: false
IpAddress: string?
The address of the most recent request on this session, truncated to 45 characters, which holds any IPv6 address in its longest plain form but not one carrying a scope id.
Default: null
UserAgent: string?
The raw User-Agent header, kept alongside the parsed values because parsing loses detail that matters when auditing.
Default: null
Device: string?
The device parsed from the User-Agent, for showing a user their own session list.
Default: null
Browser: string?
The browser parsed from the User-Agent, for showing a user their own session list.
Default: null
Os: string?
The operating system parsed from the User-Agent, for showing a user their own session list.
Default: null
IsExpired: bool
Whether the expiry has passed. Computed, not mapped, so it cannot be used in a query.
IsActive: bool
Whether the session would still refresh, meaning neither revoked nor expired. Computed, not mapped.