PasswordResetToken
A user's password reset, at most one row per account. Requesting another rewrites this row rather than adding a second, so a fresh link invalidates the previous one.
Normal flows go through IAuthPasswordService, which issues, redeems, and invalidates these consistently; read the entity directly for audit views and cleanup jobs.
DANGER
PasswordResetToken is a database entity. Never return it from an endpoint: it carries the token hash, the requesting address, and the surrogate keys. Map it to a DTO that exposes only the fields the client needs.
Usage
using Microsoft.EntityFrameworkCore;
using AlmightyShogun.AspNet.Auth.Credentials;
public sealed class PasswordResetCleanup(AppDbContext database)
{
public async Task DeleteSpentAsync()
{
DateTimeOffset now = DateTimeOffset.UtcNow;
await database.PasswordResetTokens
.Where(token => token.UsedAt != null || token.ExpiresAt <= now)
.ExecuteDeleteAsync();
}
}Fields
Id: int
The surrogate key. Never leaves the server; the emailed token is the only handle a caller has on this row.
UserId: int
The user the reset was issued for, uniquely indexed so an account cannot hold two reset tokens at once. Cascades with the user.
TokenHash: string
Hash of the token that was emailed, uniquely indexed. The emailed value cannot be recovered from the database.
CreatedAt: DateTimeOffset
When the reset now held was requested. Rewritten each time the user asks for another link, so it dates the current one rather than the first ever issued.
ExpiresAt: DateTimeOffset
When the token stops being usable, set at issue from PasswordResetMinutes.
UsedAt: DateTimeOffset?
When the token was spent, or null while it is still usable. Set instead of deleting the row, so a second attempt reads as a replay; it returns to null when the row is reused for a new request.
Default: null
RequestedIpAddress: string?
The address the reset was requested from, when the caller passed one. Kept for auditing a reset the account owner did not ask for, and truncated to 45 characters, which holds any IPv6 address in its longest plain form but not one carrying a scope id.
Default: null
IsActive: bool
Whether the token would still be accepted, meaning unspent and not past its expiry. Computed, not mapped, so it cannot be used in a query.