Self-issued JWT bearer tokens and their refresh tokens, from Regira.Security.Authentication. For tokens issued by an external identity provider, see External Identity Providers.
| Property | Type | Default | Description |
|---|---|---|---|
Secret |
string |
(required) | HMAC signing key — HS256 needs ≥ 32 bytes, HS384 ≥ 48, the HS512 default ≥ 64. Enforced at registration |
Algorithm |
string? |
null |
Signing algorithm as a JWA id; HS512 when unset (applied as SecurityAlgorithms.HmacSha512Signature, the XML-dsig URI spelling) |
ValidateSecretLength |
bool |
true |
Whether registration rejects a Secret too short for Algorithm |
AuthenticationScheme |
string |
"Bearer" |
Name of the JwtBearer scheme |
Authority |
string? |
null |
Token issuer |
Audience |
string? |
null |
Single audience |
Audiences |
ICollection<string>? |
null |
Multiple audiences |
LifeSpan |
int |
7200 |
Token lifetime in seconds |
IncludeIssuedDate |
bool |
true |
Include an iat claim in created tokens |
NameClaimType |
string |
"name" |
Claim used as user name |
RoleClaimType |
string |
"role" |
Claim used as role |
UseJwtClaimTypes |
bool |
true |
Configure the token handlers’ claim-type maps to the short JWT spellings (sub, name, email) instead of the WS-2008 URIs |
string Create(IEnumerable<Claim> claims, string? audience = null, int? lifeSpan = null);
Task<bool> Validate(string token);
var services = new ServiceCollection();
IConfiguration configuration = new ConfigurationManager();
services.AddJwtAuthentication(o => configuration.GetSection(AuthenticationSections.Jwt).Bind(o));
// Registers ITokenHelper as transient and configures the JwtBearer scheme.
// or explicitly
services.AddJwtAuthentication(options =>
{
options.Secret = configuration["Authentication:Jwt:Secret"]!;
options.Authority = configuration["Authentication:Jwt:Authority"];
options.Audience = configuration["Authentication:Jwt:Audience"];
options.LifeSpan = 3600;
});
Namespace Regira.Security.Authentication.Jwt.Extensions — historical; these apply to every scheme.
string? userId = User.FindUserId(); // NameIdentifier / sub
string? name = User.FindUserName(); // Identity.Name, then Name / name
string? email = User.FindEmail(); // Email / email
IReadOnlyList<string> roles = User.FindRoles(); // every role, across all three spellings
bool canRead = User.HasScope("api.read"); // splits the space-delimited scp / scope value
FindRoles() and HasScope() exist because the naive read is wrong. Roles reach a principal as role, roles
(Entra app roles) or the ClaimTypes.Role URI (API key, ASP.NET Identity), so reading one spelling answers empty
for the schemes using another. Scopes arrive as one space-delimited string rather than one claim each, so
User.HasClaim("scp", "api.read") is false against a token that plainly grants it.
ClaimsNormalizer.Normalize(claims, authenticationType) returns an identity carrying the canonical sub / name
/ email / role spellings (RegiraClaimTypes) alongside whatever the provider emitted. It is additive —
every source claim survives and a canonical copy is added only when missing, so Entra’s roles keeps working for
anyone reading it directly while [Authorize(Roles = …)], User.IsInRole and RequireClaim("role", …) all start
agreeing.
Source claim types per canonical claim are configurable via ClaimNormalizationOptions; the defaults cover oid,
preferred_username, upn, unique_name, roles and the ClaimTypes URIs. Note that oid, not sub, is the
stable user id on an Entra token — Entra’s sub is pairwise per application.
AuthenticationSections holds the Authentication: root paths — Jwt, Bearer, ApiKeys, Cookie, Oidc,
EntraId:
var services = new ServiceCollection();
IConfiguration configuration = new ConfigurationManager();
services.AddJwtAuthentication(o => configuration.GetSection(AuthenticationSections.Jwt).Bind(o));
Opt-in, chained off AddJwtAuthentication. Closes the gap a SPA on the JWT scheme hits: a 2-hour access token with no
way to renew it.
var services = new ServiceCollection();
IConfiguration configuration = new ConfigurationManager();
services.AddJwtAuthentication(o => configuration.GetSection("Authentication:Jwt").Bind(o))
.AddRefreshTokens(); // in-memory store — development only
Nothing changes for a host that does not call it: POST auth returns the same body it always did (the refreshToken
and expiresAt fields are absent, not null) and POST auth/refresh-token answers 404.
| Endpoint | Anon | Needs | Use |
|---|---|---|---|
POST auth/refresh |
a still-valid bearer token | Pick up role changes mid-session | |
POST auth/refresh-token |
✅ | the refresh token | Renew after the access token expired |
auth/refresh cannot help at the one moment renewal matters — once the access token has expired it answers 401.
auth/refresh-token is anonymous by necessity, because the refresh token is the credential; rate-limit it.
| Property | Type | Default | Description |
|---|---|---|---|
LifeSpan |
int (s) |
14 days | One token’s validity |
AbsoluteLifeSpan |
int (s) |
90 days | Ceiling on the whole rotation chain |
Rotate |
bool |
true |
Issue a new token on every use, revoking the presented one |
RevokeFamilyOnReuse |
bool |
true |
End the chain when an already-rotated token is presented again |
TokenByteLength |
int |
32 |
Entropy of the generated token |
HashStoredTokens |
bool |
true |
Persist a hash rather than the token |
IRefreshTokenService.Refresh so it cannot be skipped — replaying sign-in claims would keep a removed role in force
and a disabled account working. Returning null refuses the refresh and revokes the chain.AbsoluteLifeSpan caps the chain; without it a frequently-used token never expires.IHasher there.AddRefreshTokens() falls back to InMemoryRefreshTokenStore when no store is registered. It loses every session on
restart and is per-process, so behind a load balancer a refresh lands on an instance that has never heard of the token
and users are signed out at random. Neither shows up on one developer machine.
Implement IRefreshTokenStore over your own DbContext — five methods, of which only TryRevoke needs care — and
register it first:
services.AddJwtAuthentication(…)
.AddRefreshTokenStore<MyEfRefreshTokenStore>()
.AddRefreshTokens();
RevokeFamily must reach already-revoked tokens as well as active ones: it ends a chain, and a replayed token is by
definition one that was already revoked.
TryRevoke must be atomic — it is a test-and-set, revoking only if the token is not already revoked and
returning whether this caller did it. Two concurrent refreshes of one token would otherwise both succeed and split
the family into two live chains with no replay detected. Implement it as a conditional write: ExecuteUpdate
filtered on RevokedAt == null with a row-count check, or UPDATE … WHERE TokenKey = @k AND RevokedAt IS NULL.
Call RevokeAllForUser on a password change or when an account is disabled — nothing does it automatically.
Apache License 2.0 — this package contains no license validation and no runtime limits. See LICENSE. A few companion packages are commercially licensed with a free tier; see the licensing overview.