JWT
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):
| Method | Path | Notes |
|---|---|---|
POST | /create | Login; rate-limited; returns access token |
POST | /refresh | Refresh cookie + CSRF; rate-limited |
GET | /verify | [Authorize] JWT Bearer → 204 |
POST | /logout | CSRF; revokes refresh family |
GET | /csrfToken | For refresh/logout |
Refresh cookie
| Property | Value |
|---|---|
| Name | AuthEndpoints.Jwt.RefreshToken |
| Flags | HttpOnly, SameSite=Strict, Secure when HTTPS, Path=/ |
| Default TTL | 14 days |
Options
| Property | Default | Notes |
|---|---|---|
Issuer | Jwt | Rejected in Production |
Audience | JwtAudience | Rejected in Production |
AccessTokenLifetime | 15 minutes | |
SigningOptions | Symmetric | See Signing for Symmetric, RSA, ECDSA, and X509 |
TokenValidationParameters | null | See Advanced — custom ASP.NET Core JWT Bearer validation |
Signing
Configure SimpleJwtOptions.SigningOptions (SimpleJwtSigningOptions). Default algorithm is Symmetric (HS256).
Algorithm | Required property | Default JWT alg | Notes |
|---|---|---|---|
Symmetric (default) | SymmetricKey | HS256 | ≥ 32 UTF-8 bytes |
Rsa | RsaKey | RS256 | Or use X509 for cert-backed RSA |
Ecdsa | EcdsaKey | ES256 | |
X509 | Certificate | RS256 | Certificate 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*:SymmetricKeyis required and must be at least 256 bits (32 UTF-8 bytes) RS*:RsaKeyorCertificateis requiredES*:EcdsaKeyis requiredPS*:RsaKeyis 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:
| Algorithm | IssuerSigningKey type |
|---|---|
| Symmetric | SymmetricSecurityKey |
| Rsa | RsaSecurityKey |
| Ecdsa | ECDsaSecurityKey |
| X509 | X509SecurityKey |
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
- Store/send access token as
Authorization: Bearer …. - Refresh/logout need credentials (cookie) +
RequestVerificationTokenfromGET /auth/csrfToken.
Gotchas
- Passkey login does not issue JWT — call
/createseparately if needed. - Migrate after
UseRefreshToken(); recreate the table when upgrading from plaintext storage. - Call
UseRateLimiter()andUseAntiforgery().