ValkyrieJwt.Redis 0.1.1

dotnet add package ValkyrieJwt.Redis --version 0.1.1
                    
NuGet\Install-Package ValkyrieJwt.Redis -Version 0.1.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ValkyrieJwt.Redis" Version="0.1.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ValkyrieJwt.Redis" Version="0.1.1" />
                    
Directory.Packages.props
<PackageReference Include="ValkyrieJwt.Redis" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add ValkyrieJwt.Redis --version 0.1.1
                    
#r "nuget: ValkyrieJwt.Redis, 0.1.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package ValkyrieJwt.Redis@0.1.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=ValkyrieJwt.Redis&version=0.1.1
                    
Install as a Cake Addin
#tool nuget:?package=ValkyrieJwt.Redis&version=0.1.1
                    
Install as a Cake Tool

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:

  • ITenantSigningKeyStore
  • IRefreshTokenStore
  • ITokenBlacklist
  • IValkyrieAuditSink

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 as iss.
  • Subject, serialized as sub.
  • TenantId, serialized as tid.
  • TokenId, serialized as jti.
  • Audiences, serialized as aud.
  • Roles, serialized as roles.
  • Permissions, serialized as permissions.
  • IssuedAt, NotBefore and ExpiresAt, serialized as iat, nbf and exp.
  • FingerprintHash, serialized as fp.
  • Signature, serialized as signature.
  • Encryption, serialized as encryption.

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.Hs256 and Encryption = "": signed JWT.
  • Signature = "" and Encryption = ValkyrieEncryptionAlgorithms.A256Gcm: encrypted JWT.
  • Signature = ValkyrieAlgorithms.Hs512 and Encryption = 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 using dir and A256GCM; 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:

  • ITenantSigningKeyStore for persistent tenant key rings or KMS-backed keys.
  • IRefreshTokenStore for database-backed refresh sessions.
  • ITokenBlacklist for distributed revocation.
  • IValkyrieAuditSink for structured logs, SIEM, OpenTelemetry or audit tables.
  • IValkyrieClock for 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 like v0.1.0 is pushed, or from manual dispatch.

Required repository secrets:

  • NUGET_API_KEY: API key for https://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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.1 141 5/6/2026
0.1.0 117 5/6/2026

Security disclaimer: ValkyrieJwt.Redis 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.