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.0compatibility
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
MaxTokenAgebackstop 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)
OnValidatedaudit hook fired for every validation attempt
Key rotation
Rotate signing keys without invalidating existing tokens.
Example:
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:
dotnet add package EonaCat.SecureToken
Quick Start
Create a token service
using EonaCat.SecureToken.Core;
using EonaCat.SecureToken.Cryptography;
var keys = SigningKeyStore.CreateNew();
var tokens = new TokenService(keys);
Issue an access token
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
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
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:
var pair = tokens.IssueTokenPair(
"user-1",
"api",
"mobile"
);
Console.WriteLine(pair.AccessToken);
Console.WriteLine(pair.RefreshToken);
Validate separately:
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:
var token = tokens.Issue(
TokenDescriptor.Create()
.ForSubject("user-1")
.IssuedBy("api")
.ForAudience("web")
.BoundTo("device-identifier")
);
Validation:
new TokenValidationOptions
{
ValidIssuer = "api",
ValidAudience = "web",
BindingContext = "device-identifier"
};
Revocation
You can integrate your own revocation storage:
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.
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:
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:
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
builder.Services.AddSecureTokens();
or provide your own key store:
builder.Services.AddSecureTokens(
store =>
{
return SigningKeyStore.FromKeys(
new[]
{
(1, secretKeyBytes)
});
});
To enable replay protection via DI:
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.