Changelog
Modules

JWT

JWT access tokens with an HttpOnly refresh cookie, hashed storage, and reuse detection.

JWT sign-in: short-lived access tokens plus an HttpOnly refresh cookie. Pair with Identity management for register/manage.

DI

builder.Services.AddJwtEndpoints<AppUser, AppDbContext>(o =>
{
    o.Issuer = "https://example.com";
    o.Audience = "https://example.com";
    o.SigningOptions.SymmetricKey = builder.Configuration["Jwt:SymmetricKey"];
});

AddJwtEndpoints returns a SimpleJwtBuilder for replacing services:

  • AddAccessTokenGenerator<T>
  • AddRefreshTokenService<T>
  • AddErrorDescriber<T>

EF mapping

using AuthEndpoints.Jwt;

protected override void OnModelCreating(ModelBuilder builder)
{
    base.OnModelCreating(builder);
    builder.UseRefreshToken(); // schema AuthEndpoints, table AuthEndpointsRefreshTokens
}

Refresh tokens are stored as hashes only. Family reuse detection revokes the family on reuse.

Map

app.MapGroup("/auth").MapJwtAuthEndpoints<AppUser>();

Routes

Relative to the group prefix (facade default /auth):

MethodPathNotes
POST/createLogin; rate-limited; returns access token
POST/refreshRefresh cookie + CSRF; rate-limited
GET/verify[Authorize] JWT Bearer → 204
POST/logoutCSRF; revokes refresh family
GET/csrfTokenFor refresh/logout
PropertyValue
NameAuthEndpoints.Jwt.RefreshToken
FlagsHttpOnly, SameSite=Strict, Secure when HTTPS, Path=/
Default TTL14 days

Options

PropertyDefaultNotes
IssuerJwtRejected in Production
AudienceJwtAudienceRejected in Production
AccessTokenLifetime15 minutes
SigningOptionsSymmetricSee Signing for Symmetric, RSA, ECDSA, and X509
TokenValidationParametersnullSee Advanced — custom ASP.NET Core JWT Bearer validation

Signing

Configure SimpleJwtOptions.SigningOptions (SimpleJwtSigningOptions). Default algorithm is Symmetric (HS256).

AlgorithmRequired propertyDefault JWT algNotes
Symmetric (default)SymmetricKeyHS256≥ 32 UTF-8 bytes
RsaRsaKeyRS256Or use X509 for cert-backed RSA
EcdsaEcdsaKeyES256
X509CertificateRS256Certificate must include a private key

Use AlgorithmOverride to pick a different JWT algorithm string (for example SecurityAlgorithms.RsaSha512, EcdsaSha384, or HmacSha512). The options validator checks key material against the effective algorithm (HS*, RS*, ES*, PS*).

The same SigningOptions shape works on the facade via o.Jwt.Configure = jwt => { … } or on compose via AddJwtEndpoints as shown below.

Symmetric

builder.Services.AddJwtEndpoints<AppUser, AppDbContext>(o =>
{
    o.Issuer = "https://example.com";
    o.Audience = "https://example.com";
    o.SigningOptions.Algorithm = SimpleJwtSigningOptions.SigningAlgorithm.Symmetric; // default
    o.SigningOptions.SymmetricKey = builder.Configuration["Jwt:SymmetricKey"];
});

RSA

using System.Security.Cryptography;

var rsa = RSA.Create();
rsa.ImportFromPem(File.ReadAllText("keys/rsa-private.pem"));

builder.Services.AddJwtEndpoints<AppUser, AppDbContext>(o =>
{
    o.Issuer = "https://example.com";
    o.Audience = "https://example.com";
    o.SigningOptions.Algorithm = SimpleJwtSigningOptions.SigningAlgorithm.Rsa;
    o.SigningOptions.RsaKey = rsa;
});

ECDSA

using System.Security.Cryptography;

var ecdsa = ECDsa.Create();
ecdsa.ImportFromPem(File.ReadAllText("keys/ecdsa-private.pem"));

builder.Services.AddJwtEndpoints<AppUser, AppDbContext>(o =>
{
    o.Issuer = "https://example.com";
    o.Audience = "https://example.com";
    o.SigningOptions.Algorithm = SimpleJwtSigningOptions.SigningAlgorithm.Ecdsa;
    o.SigningOptions.EcdsaKey = ecdsa;
});

X509

using System.Security.Cryptography.X509Certificates;

var certificate = X509CertificateLoader.LoadPkcs12FromFile(
    "certs/signing.pfx",
    builder.Configuration["Jwt:CertificatePassword"]);

builder.Services.AddJwtEndpoints<AppUser, AppDbContext>(o =>
{
    o.Issuer = "https://example.com";
    o.Audience = "https://example.com";
    o.SigningOptions.Algorithm = SimpleJwtSigningOptions.SigningAlgorithm.X509;
    o.SigningOptions.Certificate = certificate;
});

PEM with private key is also fine when you build an X509Certificate2 that includes the key (for example X509Certificate2.CreateFromPemFile).

AlgorithmOverride

using Microsoft.IdentityModel.Tokens;

builder.Services.AddJwtEndpoints<AppUser, AppDbContext>(o =>
{
    o.Issuer = "https://example.com";
    o.Audience = "https://example.com";
    o.SigningOptions.Algorithm = SimpleJwtSigningOptions.SigningAlgorithm.Rsa;
    o.SigningOptions.RsaKey = rsa;
    o.SigningOptions.AlgorithmOverride = SecurityAlgorithms.RsaSha512;
});

Validation notes

  • Symmetric / HS*: SymmetricKey is required and must be at least 256 bits (32 UTF-8 bytes)
  • RS*: RsaKey or Certificate is required
  • ES*: EcdsaKey is required
  • PS*: RsaKey is required
  • Production still rejects default issuer/audience — see Production

Advanced

TokenValidationParameters

Access-token validation always goes through ASP.NET Core JWT Bearer and TokenValidationParameters. Changing the signing algorithm does not bypass that path — it only changes which SecurityKey is used.

When SimpleJwtOptions.TokenValidationParameters is null (the default), AddJwtEndpoints builds parameters like this and assigns them to JwtBearerOptions.TokenValidationParameters:

new TokenValidationParameters
{
    ValidateIssuer = true,
    ValidIssuer = options.Issuer,
    ValidateAudience = true,
    ValidAudience = options.Audience,
    ValidateLifetime = true,
    ValidateIssuerSigningKey = true,
    IssuerSigningKey = options.SigningOptions.ToSecurityKey()
}

ToSecurityKey() returns the key type for the configured algorithm:

AlgorithmIssuerSigningKey type
SymmetricSymmetricSecurityKey
RsaRsaSecurityKey
EcdsaECDsaSecurityKey
X509X509SecurityKey

Signing (token creation) uses SigningOptions.ToSigningCredentials() (HS256 / RS256 / ES256 / etc.). Validation checks the JWT signature against IssuerSigningKey.

Custom TokenValidationParameters

If you set TokenValidationParameters yourself, that object is used as-is. AuthEndpoints does not merge SigningOptions into it — you must set IssuerSigningKey (and issuer/audience rules) on your override:

using Microsoft.IdentityModel.Tokens;

builder.Services.AddJwtEndpoints<AppUser, AppDbContext>(o =>
{
    o.Issuer = "https://example.com";
    o.Audience = "https://example.com";
    o.SigningOptions.Algorithm = SimpleJwtSigningOptions.SigningAlgorithm.Rsa;
    o.SigningOptions.RsaKey = rsa;

    // Used for token creation (SigningOptions) AND you must wire validation yourself:
    o.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidIssuer = "https://example.com",
        ValidateAudience = true,
        ValidAudience = "https://example.com",
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = o.SigningOptions.ToSecurityKey()
    };
});

JwtBearerOptions callback

AddJwtEndpoints also accepts an optional Action<JwtBearerOptions> to tweak the bearer handler after TokenValidationParameters are assigned (events, metadata address, etc.):

builder.Services.AddJwtEndpoints<AppUser, AppDbContext>(
    o =>
    {
        o.Issuer = "https://example.com";
        o.Audience = "https://example.com";
        o.SigningOptions.SymmetricKey = builder.Configuration["Jwt:SymmetricKey"];
    },
    jwt =>
    {
        jwt.MapInboundClaims = false;
    });

SPA notes

  1. Store/send access token as Authorization: Bearer ….
  2. Refresh/logout need credentials (cookie) + RequestVerificationToken from GET /auth/csrfToken.

Gotchas

  • Passkey login does not issue JWT — call /create separately if needed.
  • Migrate after UseRefreshToken(); recreate the table when upgrading from plaintext storage.
  • Call UseRateLimiter() and UseAntiforgery().