Files
EonaCat.SecureToken/README.md
T
2026-06-20 06:27:57 +02:00

348 lines
7.4 KiB
Markdown

# EonaCat.SecureToken
**Secure, modern token library for .NET with key rotation, signing isolation, validation rules, and .NET Standard support.**
EonaCat.SecureToken provides a safer alternative to rolling your own authentication tokens. It focuses on:
- Strong cryptographic signing
- Versioned key management
- Token lifecycle validation
- Refresh/access token separation
- Extensible claims
- API-friendly validation results
- `.NET Standard 2.0` compatibility
## Features
### Cryptographic protection
- HMAC based token signing
- HKDF derived context-specific keys
- 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
Rotate signing keys without invalidating existing tokens.
Example:
```csharp
var store = SigningKeyStore.CreateNew();
var service = new TokenService(store);
var oldToken = service.Issue(
TokenDescriptor.Create()
.ForSubject("user-123")
.IssuedBy("my-api")
.ForAudience("mobile")
);
// Rotate keys
store.Rotate();
// New tokens use the new key
var newToken = service.Issue(
TokenDescriptor.Create()
.ForSubject("user-456")
.IssuedBy("my-api")
.ForAudience("mobile")
);
// Old token still validates
service.Validate(
oldToken,
TokenValidationOptions.AccessToken("my-api", "mobile")
);
```
## Installation
Install from NuGet:
```bash
dotnet add package EonaCat.SecureToken
```
# Quick Start
## Create a token service
```csharp
using EonaCat.SecureToken.Core;
using EonaCat.SecureToken.Cryptography;
var keys = SigningKeyStore.CreateNew();
var tokens = new TokenService(keys);
```
## Issue an access token
```csharp
var token = tokens.Issue(
TokenDescriptor.Create()
.ForSubject("user-123")
.IssuedBy("my-service")
.ForAudience("api")
.WithRole("admin")
.WithClaim("email", "user@example.com")
);
```
The token contains:
- Subject
- Issuer
- Audience
- Roles
- Custom claims
- Token ID
- Expiration
- Key generation information
## Validate a token
```csharp
var result = tokens.Validate(
token,
TokenValidationOptions.AccessToken(
issuer: "my-service",
audience: "api"
)
);
if (result.IsSuccess)
{
var claims = result.UnwrapClaims();
Console.WriteLine(claims.Subject);
}
```
# Token expiration
```csharp
var token = tokens.Issue(
TokenDescriptor.Create()
.ForSubject("user-1")
.IssuedBy("api")
.ForAudience("mobile")
.WithLifetime(TimeSpan.FromMinutes(15))
);
```
Expired tokens are automatically rejected.
# Refresh tokens
Create a refresh token:
```csharp
var pair = tokens.IssueTokenPair(
"user-1",
"api",
"mobile"
);
Console.WriteLine(pair.AccessToken);
Console.WriteLine(pair.RefreshToken);
```
Validate separately:
```csharp
tokens.Validate(
pair.RefreshToken,
TokenValidationOptions.RefreshToken("api")
);
```
Refresh tokens cannot be used as access tokens.
# Token binding
Bind tokens to a context such as a device or session:
```csharp
var token = tokens.Issue(
TokenDescriptor.Create()
.ForSubject("user-1")
.IssuedBy("api")
.ForAudience("web")
.BoundTo("device-identifier")
);
```
Validation:
```csharp
new TokenValidationOptions
{
ValidIssuer = "api",
ValidAudience = "web",
BindingContext = "device-identifier"
};
```
# Revocation
You can integrate your own revocation storage:
```csharp
var options = new TokenValidationOptions
{
ValidIssuer = "api",
ValidAudience = "web",
RevocationCheck = async (tokenId, cancellationToken) =>
{
return await database.IsRevoked(tokenId);
}
};
```
# 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
builder.Services.AddSecureTokens();
```
or provide your own key store:
```csharp
builder.Services.AddSecureTokens(
store =>
{
return SigningKeyStore.FromKeys(
new[]
{
(1, secretKeyBytes)
});
});
```
To enable replay protection via DI:
```csharp
builder.Services.AddSecureTokenReplayProtection();
```
# Security design
The library separates cryptographic purposes:
```
Master Key
|
+-- Signing Key
|
+-- Encryption Key
|
+-- Context-specific keys
```
This prevents accidental key reuse between operations.
# Supported frameworks
- .NET Standard 2.0
- .NET Standard 2.1
- .NET Framework 4.8
- .NET 8+
# When to use
Good fit for:
APIs
Microservices
Internal authentication
Service-to-service tokens
Applications needing key rotation
# When not to use
Do not store secrets directly in source code.
Use:
- Environment variables
- Secret managers
- Hardware-backed key storage where required
# License
Apache License.