272 lines
4.6 KiB
Markdown
272 lines
4.6 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
|
|
- Tamper detection
|
|
- Strong random key generation
|
|
|
|
### 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);
|
|
}
|
|
};
|
|
```
|
|
|
|
# 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)
|
|
});
|
|
});
|
|
```
|
|
|
|
# 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. |