Skip to content

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 ​

csharp
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.

All packages are released under the MIT License.