ValkyrieJwt.Mongo
0.1.1
dotnet add package ValkyrieJwt.Mongo --version 0.1.1
NuGet\Install-Package ValkyrieJwt.Mongo -Version 0.1.1
<PackageReference Include="ValkyrieJwt.Mongo" Version="0.1.1" />
<PackageVersion Include="ValkyrieJwt.Mongo" Version="0.1.1" />
<PackageReference Include="ValkyrieJwt.Mongo" />
paket add ValkyrieJwt.Mongo --version 0.1.1
#r "nuget: ValkyrieJwt.Mongo, 0.1.1"
#:package ValkyrieJwt.Mongo@0.1.1
#addin nuget:?package=ValkyrieJwt.Mongo&version=0.1.1
#tool nuget:?package=ValkyrieJwt.Mongo&version=0.1.1
ValkyrieJwt
ValkyrieJwt is a multistack JWT toolkit for applications that need more than a token string: tenant-aware signing keys, key rotation, refresh token rotation, blacklist/revocation, fingerprint binding, RBAC helpers and audit events.
The .NET runtime is the NuGet package ValkyrieJwt. Optional NuGet packages ValkyrieJwt.Redis and ValkyrieJwt.Mongo add persistent stores. The Node.js/TypeScript runtime is the npm package valkyrie-jwt.
Status: early personal project, currently
0.1.0. The code is intentionally simple and interface-driven so persistence, asymmetric keys, DI integration and framework adapters can evolve cleanly.
Features
| Capability | Included now |
|---|---|
| Create JWTs | HMAC-signed access tokens with issuer, tenant, subject, audience, roles, permissions and custom claims |
| Encrypt JWTs | Compact JWE access tokens with direct key management and AES-256-GCM content encryption |
| Rotate keys | Tenant-scoped key ring with active key rotation and old-key validation |
| Refresh tokens | Random refresh tokens stored hashed, rotated on use |
| Blacklist | Access-token revocation by jti until token expiration |
| Fingerprint binding | SHA-256 fingerprint hash stored in fp to reduce stolen-token replay |
| Multi-tenant | Tenant id stored as tid; signing keys are isolated per tenant |
| RBAC | Role and permission checks via ValkyrieAuthorizationService |
| Audit | Pluggable audit sink with issue, validation, rejection, revocation, refresh and key events |
| Typed body | Use ValkyrieJwtBody or a derived class to decide exactly what goes into the token |
| Persistent stores | Optional Redis and MongoDB packages for shared key rings, refresh tokens, blacklist and audit events |
Install
dotnet add package ValkyrieJwt
Optional persistent stores:
dotnet add package ValkyrieJwt.Redis
dotnet add package ValkyrieJwt.Mongo
Node.js/TypeScript backend runtime:
npm install valkyrie-jwt
From Install to Token
NuGet with C#/.NET
Create an API and install the package:
dotnet new web -n MinhaApi
cd MinhaApi
dotnet add package ValkyrieJwt
Use it in Program.cs:
using ValkyrieJwt;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddValkyrieJwt("minha-api");
var app = builder.Build();
app.MapPost("/login", async (
LoginRequest login,
HttpContext http,
ValkyrieJwtService jwt) =>
{
// Validate email/password with your user store before generating tokens.
string tenantId = string.IsNullOrWhiteSpace(login.TenantId) ? "default" : login.TenantId;
string userId = "user-1";
string fingerprint = http.Request.Headers["X-Valkyrie-Fingerprint"].ToString();
var body = new ValkyrieJwtBody
{
Audiences = new[] { "minha-api" },
Roles = new[] { "user" },
Permissions = new[] { "profile:read" },
Fingerprint = fingerprint,
Signature = ValkyrieAlgorithms.Hs256,
Encryption = string.Empty
};
ValkyrieTokenPair tokens = await jwt.GenerateAsync(tenantId, userId, body);
return Results.Ok(tokens);
});
app.Run();
public sealed record LoginRequest(string Email, string Password, string? TenantId);
Call the endpoint and use accessToken as your JWT:
curl -X POST http://localhost:5000/login \
-H "Content-Type: application/json" \
-H "X-Valkyrie-Fingerprint: device-cookie-value" \
-d "{\"email\":\"dev@example.com\",\"password\":\"secret\",\"tenantId\":\"default\"}"
The response contains:
{
"accessToken": "eyJ...",
"accessTokenId": "2d6...",
"accessTokenExpiresAt": "2026-05-06T13:15:00+00:00",
"refreshToken": "opaque-random-refresh-token",
"refreshTokenId": "c92...",
"refreshTokenExpiresAt": "2026-06-05T13:00:00+00:00",
"tenantId": "default",
"subject": "user-1",
"keyId": "vk_..."
}
npm with TypeScript/Node
Create a Node backend/script and install the package:
npm init -y
npm install valkyrie-jwt
To run this example directly as TypeScript:
npm install -D typescript tsx @types/node
Create generate-token.ts:
import {
ValkyrieAlgorithms,
ValkyrieJwtBody,
ValkyrieJwtService
} from "valkyrie-jwt";
const jwt = new ValkyrieJwtService("minha-api");
const body = new ValkyrieJwtBody({
audiences: ["minha-api"],
roles: ["admin"],
permissions: ["users:read"],
signature: ValkyrieAlgorithms.Hs256,
encryption: "",
email: "dev@example.com"
});
const tokens = await jwt.generate("default", "user-1", body);
const validation = await jwt.validate(tokens.accessToken, {
expectedTenantId: "default",
expectedAudience: "minha-api"
});
console.log(tokens.accessToken);
console.log(validation.succeeded);
console.log(validation.body?.email);
Run:
npx tsx generate-token.ts
The npm package is for trusted Node.js backends. Do not generate signed or encrypted JWTs in a browser, because that would expose your signing/encryption key material.
Quick Start
using ValkyrieJwt;
var jwt = new ValkyrieJwtService("https://auth.example.com");
string fingerprint = "value-from-secure-device-cookie";
ValkyrieTokenPair pair = await jwt.GenerateAsync(new ValkyrieTokenRequest("tenant-a", "user-123")
{
Audiences = new[] { "orders-api" },
Roles = new[] { "admin" },
Permissions = new[] { "orders:read", "orders:write" },
Fingerprint = fingerprint,
Claims = new Dictionary<string, object?>
{
["email"] = "dev@example.com"
}
});
ValkyrieValidationResult validation = await jwt.ValidateAsync(
pair.AccessToken,
new ValkyrieTokenValidationParameters
{
ExpectedTenantId = "tenant-a",
ExpectedAudience = "orders-api",
Fingerprint = fingerprint
});
if (!validation.Succeeded)
{
throw new InvalidOperationException(validation.FailureReason);
}
var authorization = new ValkyrieAuthorizationService();
bool canWriteOrders = authorization.Authorize(validation, new ValkyrieRequirement
{
TenantId = "tenant-a",
AllPermissions = new[] { "orders:write" }
});
The issuer is the API/auth-service identifier that created the token. A URL is a good stable value, but a simple name like "minha-api" also works as long as validation uses the same issuer.
To override only specific defaults:
var jwt = new ValkyrieJwtService(
"https://auth.example.com",
accessTokenLifetime: TimeSpan.FromMinutes(5),
tokenProtection: ValkyrieTokenProtection.Encrypted);
The full ValkyrieJwtOptions object is still available when you prefer explicit configuration.
ASP.NET Core DI
For APIs, register ValkyrieJwtService as a singleton through dependency injection. With no extra store package, the service uses in-memory stores:
using ValkyrieJwt;
builder.Services.AddValkyrieJwt("minha-api");
Then inject it where you need it:
app.MapPost("/login", async (ValkyrieJwtService jwt) =>
{
ValkyrieTokenPair tokens = await jwt.GenerateAsync("default", "user-123", new ValkyrieJwtBody
{
Audiences = new[] { "minha-api" },
Roles = new[] { "user" }
});
return Results.Ok(tokens);
});
Redis Persistence
Install ValkyrieJwt.Redis, then choose one of these forms.
Use an existing Redis connection registered by your API:
using StackExchange.Redis;
using ValkyrieJwt;
builder.Services.AddSingleton<IConnectionMultiplexer>(
_ => ConnectionMultiplexer.Connect("localhost:6379"));
builder.Services
.AddValkyrieJwt("minha-api")
.UseRedis();
Or let ValkyrieJwt register the connection:
builder.Services
.AddValkyrieJwt("minha-api")
.UseRedis("localhost:6379");
UseRedis() replaces the in-memory implementations for:
ITenantSigningKeyStoreIRefreshTokenStoreITokenBlacklistIValkyrieAuditSink
Default Redis keys use the valkyrie:jwt prefix. You can change it:
builder.Services
.AddValkyrieJwt("minha-api")
.UseRedis("localhost:6379", options =>
{
options.KeyPrefix = "minha-api:auth";
options.Database = 1;
});
MongoDB Persistence
Install ValkyrieJwt.Mongo, then choose one of these forms.
Use an existing Mongo client registered by your API:
using MongoDB.Driver;
using ValkyrieJwt;
builder.Services.AddSingleton<IMongoClient>(
_ => new MongoClient("mongodb://localhost:27017"));
builder.Services
.AddValkyrieJwt("minha-api")
.UseMongo("valkyrie_jwt");
Or let ValkyrieJwt register the Mongo client:
builder.Services
.AddValkyrieJwt("minha-api")
.UseMongo("mongodb://localhost:27017", "valkyrie_jwt");
"valkyrie_jwt" is the MongoDB database name. By default, ValkyrieJwt uses these collections inside it:
valkyrie_signing_keys
valkyrie_refresh_tokens
valkyrie_blacklist
valkyrie_audit_events
You can customize the database and collection names:
builder.Services
.AddValkyrieJwt("minha-api")
.UseMongo("mongodb://localhost:27017", "minha_api_auth", options =>
{
options.RefreshTokensCollectionName = "auth_refresh_tokens";
options.BlacklistCollectionName = "auth_blacklist";
});
Typed JWT Body
For the newer body-first flow, create a ValkyrieJwtBody, fill the values you want, then issue the token with tenant and subject:
var body = new ValkyrieJwtBody
{
Audiences = new[] { "orders-api" },
Roles = new[] { "admin" },
Permissions = new[] { "orders:write" },
Signature = ValkyrieAlgorithms.Hs512,
Encryption = string.Empty
};
ValkyrieTokenPair pair = await jwt.GenerateAsync("tenant-a", "user-123", body);
ValkyrieJwtBody contains the standard payload fields:
Issuer, serialized asiss.Subject, serialized assub.TenantId, serialized astid.TokenId, serialized asjti.Audiences, serialized asaud.Roles, serialized asroles.Permissions, serialized aspermissions.IssuedAt,NotBeforeandExpiresAt, serialized asiat,nbfandexp.FingerprintHash, serialized asfp.Signature, serialized assignature.Encryption, serialized asencryption.
The service fills issuer, tenant, subject, token id and timestamps during issue. You control roles, permissions, audiences, signature, encryption and custom fields.
To add your own fields, inherit from ValkyrieJwtBody:
public sealed class BillingJwtBody : ValkyrieJwtBody
{
public string Document { get; set; } = string.Empty;
public int AccessLevel { get; set; }
}
var body = new BillingJwtBody
{
Audiences = new[] { "billing-api" },
Signature = string.Empty,
Encryption = ValkyrieEncryptionAlgorithms.A256Gcm,
Document = "sensitive-value",
AccessLevel = 7
};
ValkyrieTokenPair pair = await jwt.GenerateAsync("tenant-a", "user-123", body);
Protection is decided by the body:
Signature = ValkyrieAlgorithms.Hs256andEncryption = "": signed JWT.Signature = ""andEncryption = ValkyrieEncryptionAlgorithms.A256Gcm: encrypted JWT.Signature = ValkyrieAlgorithms.Hs512andEncryption = ValkyrieEncryptionAlgorithms.A256Gcm: signed and encrypted JWT.
Unprotected JWTs are intentionally rejected. At least Signature or Encryption must be set.
Validate and materialize the body back into your type:
ValkyrieBodyValidationResult<BillingJwtBody> result =
await jwt.ValidateAsync<BillingJwtBody>(pair.AccessToken);
if (result.Succeeded)
{
BillingJwtBody validatedBody = result.Body!;
}
For encrypted tokens, you can also read the decrypted body directly:
BillingJwtBody decrypted = await jwt.DecryptBodyAsync<BillingJwtBody>(pair.AccessToken);
If you are managing the key material outside the built-in key store, pass it directly:
BillingJwtBody decrypted = await jwt.DecryptBodyAsync<BillingJwtBody>(
pair.AccessToken,
keyMaterial,
tenantId: "tenant-a",
keyId: "external-key",
algorithm: ValkyrieAlgorithms.Hs256);
Refresh Token Rotation
Refresh tokens are opaque random values. ValkyrieJwt stores only a SHA-256 hash and marks a refresh token as consumed when it is exchanged. Reusing a consumed refresh token is treated as replay and revokes the whole token family.
ValkyrieRefreshResult refreshed = await jwt.RefreshAsync(pair.RefreshToken!, fingerprint);
if (refreshed.Succeeded)
{
pair = refreshed.Tokens!;
}
Revocation and Blacklist
await jwt.RevokeAccessTokenAsync(pair.AccessToken, fingerprint, reason: "logout");
await jwt.RevokeRefreshTokenAsync(pair.RefreshToken!, reason: "logout");
Access-token blacklist entries are kept until the token expires. The default implementation is in-memory; production systems can use ValkyrieJwt.Redis, ValkyrieJwt.Mongo or a custom ITokenBlacklist.
Key Rotation
ValkyrieSigningKey newKey = await jwt.RotateSigningKeyAsync("tenant-a");
New tokens use the new kid; older tokens remain valid while their signing key remains available. Production systems can use Redis, MongoDB or a custom ITenantSigningKeyStore with durable storage and an operational retirement policy.
Encrypted JWTs
Signed JWTs protect integrity, but their payload is readable by anyone who has the token. When claims need confidentiality, issue compact JWE tokens:
var jwt = new ValkyrieJwtService(new ValkyrieJwtOptions
{
Issuer = "https://auth.example.com",
TokenProtection = ValkyrieTokenProtection.Encrypted
});
ValkyrieTokenPair pair = await jwt.GenerateAsync(new ValkyrieTokenRequest("tenant-a", "user-123")
{
Claims = new Dictionary<string, object?>
{
["document"] = "sensitive-value"
}
});
Available protection modes:
Signed: regular compact JWS, default mode.Encrypted: compact JWE usingdirandA256GCM; payload claims are encrypted and authenticated.SignedAndEncrypted: creates an inner signed JWT and encrypts it as a nested JWE.
The encrypted token still exposes a small protected header with kid, tid, alg and enc so the validator can find the tenant key. Keep highly sensitive records in your backend when possible and put only the minimum necessary claims in tokens.
TypeScript/Node Runtime
import {
ValkyrieEncryptionAlgorithms,
ValkyrieJwtBody,
ValkyrieJwtService,
createRandomFingerprint
} from "valkyrie-jwt";
const jwt = new ValkyrieJwtService("minha-api");
const fingerprint = createRandomFingerprint();
const tokens = await jwt.generate("default", "user-1", new ValkyrieJwtBody({
audiences: ["minha-api"],
fingerprint,
signature: "",
encryption: ValkyrieEncryptionAlgorithms.A256Gcm,
document: "sensitive-value"
}));
const validation = await jwt.validateBody(tokens.accessToken, {
expectedTenantId: "default",
expectedAudience: "minha-api",
fingerprint
});
The npm runtime supports signed JWTs, encrypted JWTs, refresh token rotation, blacklist, fingerprint binding, multi-tenant keys, RBAC helpers and audit sinks for Node.js backends. It still exports createFingerprint, createRandomFingerprint, decodeToken and assertTokenShape for tests and tooling.
For shared persistence in TypeScript backends, pass the store client object your API already uses.
createRedisStores(redis) receives a connected Redis client, not a connection string. It can be a redis/node-redis client created with createClient(...) and await redis.connect(), or a compatible Redis client that exposes commands such as get, set, exists, expire, sAdd/sadd, sMembers/smembers and xAdd/xadd.
import { createClient } from "redis";
import { createRedisStores, ValkyrieJwtService } from "valkyrie-jwt";
const redis = createClient({ url: "redis://localhost:6379" });
await redis.connect();
const jwt = new ValkyrieJwtService("minha-api", {
stores: createRedisStores(redis, { keyPrefix: "minha-api:auth" })
});
createMongoStores(db) receives a MongoDB Db object, not the MongoClient and not a connection string. With the official MongoDB driver, that object is returned by mongo.db("valkyrie_jwt").
import { MongoClient } from "mongodb";
import { createMongoStores, ValkyrieJwtService } from "valkyrie-jwt";
const mongo = new MongoClient("mongodb://localhost:27017");
await mongo.connect();
const jwt = new ValkyrieJwtService("minha-api", {
stores: createMongoStores(mongo.db("valkyrie_jwt"))
});
Extensibility Points
Replace these interfaces as the project grows:
ITenantSigningKeyStorefor persistent tenant key rings or KMS-backed keys.IRefreshTokenStorefor database-backed refresh sessions.ITokenBlacklistfor distributed revocation.IValkyrieAuditSinkfor structured logs, SIEM, OpenTelemetry or audit tables.IValkyrieClockfor deterministic tests and controlled time.
Security Disclaimer
ValkyrieJwt is provided as-is under the MIT license. Security depends on how the application configures, stores, deploys and operates its keys, tokens, Redis/Mongo/database stores, cookies, clocks, secrets, network boundaries and authorization rules.
Using this library is the responsibility and risk of the application owner. The library owner/author is not responsible for misuse, insecure configuration, operational failures, application vulnerabilities, data loss, security incidents or breaches created by the way this library is used.
Review the code, test your threat model, keep dependencies updated, protect signing/encryption keys, use durable stores in production and run your own security validation before relying on ValkyrieJwt in a real system.
CI/CD
The repository includes GitHub Actions:
ci.yml: restores, builds, tests, packs all NuGet packages, tests the npm runtime package and runs C# ↔ TypeScript interoperability tests on push/PR.release.yml: publishes the NuGet packages and npm package when a tag likev0.1.0is pushed, or from manual dispatch.
Required repository secrets:
NUGET_API_KEY: API key forhttps://api.nuget.org.NODE_AUTH_TOKEN: npm automation token with publish permission.
Release example:
git tag v0.1.0
git push origin v0.1.0
Roadmap
- ASP.NET Core authentication/authorization adapters.
- RSA/ECDSA and KMS-backed signing providers.
- PostgreSQL persistence package.
- OpenTelemetry audit exporter.
- JWKS endpoint support.
- More property-based and interoperability tests.
License
MIT. See LICENSE.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- MongoDB.Driver (>= 3.7.1)
- ValkyrieJwt (>= 0.1.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Security disclaimer: ValkyrieJwt.Mongo is provided as-is. Use is the responsibility and risk of the application owner; the library owner/author is not responsible for misuse, insecure configuration, operational failures, vulnerabilities, data loss, incidents or breaches created by how the library is used.