This commit is contained in:
2026-06-20 06:27:57 +02:00
parent 6cc78245be
commit 52b9be6495
14 changed files with 654 additions and 47 deletions
+77 -1
View File
@@ -18,9 +18,20 @@ EonaCat.SecureToken provides a safer alternative to rolling your own authenticat
- HMAC based token signing
- HKDF derived context-specific keys
- Constant-time signature verification
- Constant-time signature verification (including binding-context comparison)
- Tamper detection
- Strong random key generation
- Bounded decoding: oversized or field-flooded tokens are rejected before signature
verification, so malformed input cannot be used to exhaust memory
### Validation
- Issuer, audience (single or multi-audience accept-list), and token-type checks
- Expiry, not-before, and an independent `MaxTokenAge` backstop measured from issuance
- Token binding (IP, device fingerprint, TLS channel hash, etc.)
- Pluggable revocation check
- Pluggable replay-cache for one-time-use tokens (password reset, email verification, invitations)
- `OnValidated` audit hook fired for every validation attempt
### Key rotation
@@ -205,6 +216,64 @@ var options = new TokenValidationOptions
};
```
# Replay protection for one-time-use tokens
Revocation answers "has someone explicitly blocked this token?" Replay protection
answers a different question: "has this exact token already been used once?" Use it
for password-reset links, email-verification links, and invitations - anything that
should only ever be redeemed a single time, even before it expires.
```csharp
var replayCache = new InMemoryReplayCache(); // register as a singleton in DI
var options = TokenValidationOptions.OneTimeUse(
issuer: "api",
tokenType: TokenTypeConstants.PasswordReset,
maxAge: TimeSpan.FromMinutes(15));
options.ReplayCache = replayCache;
var result = await tokens.ValidateAsync(token, options);
// Second call with the same token returns TokenResult.Replayed instead of Success.
```
`InMemoryReplayCache` is process-local and fine for a single instance. For multi-instance
deployments, implement `IReplayCache` against a shared store (Redis, or a database table
with a unique constraint on the token ID) so replay detection works across all instances.
Replay consumption only happens through `ValidateAsync`, since it has a side effect
(recording the token as used) - the synchronous `Validate` never touches the replay cache.
# Multiple audiences
Use `ValidAudiences` when the same access token needs to be accepted by more than one
downstream service:
```csharp
var options = TokenValidationOptions.AccessToken(
issuer: "api",
audiences: new[] { "service-a", "service-b", "service-c" });
```
The token is accepted if any one of its own audiences matches any one of the configured set.
# Audit and introspection hook
`OnValidated` is invoked for every validation attempt, success or failure, and is intended
for audit logging or metrics - not for authorization decisions:
```csharp
var options = TokenValidationOptions.AccessToken("api", "web");
options.OnValidated = e =>
{
logger.LogInformation("Token validation: {Result} sub={Subject}", e.Result, e.Subject);
};
```
`Subject` and `TokenId` on the event are only populated when the result is a success,
since claims are never trusted before the signature has verified.
# ASP.NET Core dependency injection
```csharp
@@ -225,6 +294,13 @@ builder.Services.AddSecureTokens(
});
```
To enable replay protection via DI:
```csharp
builder.Services.AddSecureTokenReplayProtection();
```
# Security design
The library separates cryptographic purposes: