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