Clywell.Core.Security
2.1.1
dotnet add package Clywell.Core.Security --version 2.1.1
NuGet\Install-Package Clywell.Core.Security -Version 2.1.1
<PackageReference Include="Clywell.Core.Security" Version="2.1.1" />
<PackageVersion Include="Clywell.Core.Security" Version="2.1.1" />
<PackageReference Include="Clywell.Core.Security" />
paket add Clywell.Core.Security --version 2.1.1
#r "nuget: Clywell.Core.Security, 2.1.1"
#:package Clywell.Core.Security@2.1.1
#addin nuget:?package=Clywell.Core.Security&version=2.1.1
#tool nuget:?package=Clywell.Core.Security&version=2.1.1
Clywell.Core.Security
Security primitives for .NET — JWT bearer configuration, user context resolution, permission-based authorization, and security headers middleware.
Installation
dotnet add package Clywell.Core.Security
Table of Contents
- Quick Start
- JWT Authentication
- Current User
- Permission-Based Authorization
- Step-Up Authentication
- Custom User Context Resolver
- Custom Claim Mapping
- Security Headers
- API Reference
- Dependencies
Quick Start
1. Register services
builder.Services.AddSecurity(options =>
{
options.AddJwtBearer()
.WithOidcProvider("https://your-identity-provider.com", audience: "your-api");
});
2. Configure the middleware pipeline
app.UseAuthentication();
app.UseUserContext(); // Populates ICurrentUser from the resolved identity
app.UseAuthorization();
app.UseSecurityHeaders(); // Adds OWASP-recommended response headers
3. Inject ICurrentUser
public class MyService(ICurrentUser currentUser)
{
public void DoWork()
{
if (!currentUser.IsAuthenticated)
return;
Console.WriteLine(currentUser.UserId);
Console.WriteLine(currentUser.Email);
Console.WriteLine(currentUser.IpAddress);
if (currentUser.IsInRole("Admin")) { /* ... */ }
if (currentUser.HasPermission("articles.edit")) { /* ... */ }
}
}
JWT Authentication
AddJwtBearer() returns a JwtBearerBuilder. Pick your token source first, then optionally chain transport and advanced settings.
OIDC / External Identity Provider
Use WithOidcProvider when tokens are issued by an external provider (Auth0, Azure AD, Keycloak, etc.). Signing keys are discovered automatically.
options.AddJwtBearer()
.WithOidcProvider("https://login.example.com", audience: "my-api");
Advanced settings chain naturally:
options.AddJwtBearer()
.WithOidcProvider("https://login.example.com", audience: "my-api")
.WithClockSkew(TimeSpan.FromSeconds(30))
.DisableAudienceValidation(); // only if your IdP omits aud
Self-Hosted JWT (Symmetric Key)
Use WithSymmetricKey when your own service issues JWTs with no external OIDC provider.
Security: The signing key must be at least 32 characters. Read it from a secret manager or environment variable — never hard-code it.
options.AddJwtBearer()
.WithSymmetricKey(
signingKey: builder.Configuration["Jwt:SigningKey"], // min 32 chars
issuer: "https://my-service.example.com",
audience: "my-api");
Self-Hosted JWT (Asymmetric Key / RSA)
Use WithSigningKey when your own service issues JWTs signed with an asymmetric key (RSA, ECDSA). Pass the public SecurityKey derived from your signing key pair. This is the recommended approach for production JWT issuers.
Security: Never include the private key in token validation. Extract only the public parameters before registering — see the example below.
// Load the RSA private key from configuration (secret manager / env var - never hard-code)
using var rsa = RSA.Create();
rsa.ImportFromPem(configuration["Jwt:RsaPrivateKey"]);
var publicKey = new RsaSecurityKey(RSA.Create(rsa.ExportParameters(includePrivateParameters: false)));
builder.Services.AddSecurity(options =>
{
options.AddJwtBearer()
.WithSigningKey(publicKey, issuer: "https://my-service.example.com")
.DisableAudienceValidation(); // omit if you set an audience
});
Token from Cookie or Query String
For transports that cannot send an Authorization header (SignalR WebSockets, SSE), chain WithTokenCookie and/or WithTokenQueryParam. Cookie takes priority over query string when both are present.
Security: The cookie should be
HttpOnlyandSecure. Query string tokens may appear in server logs — prefer cookies where possible.
options.AddJwtBearer()
.WithOidcProvider("https://login.example.com", audience: "my-api")
.WithTokenCookie("access_token") // HttpOnly, Secure cookie
.WithTokenQueryParam("access_token"); // fallback when cookie absent
Current User
ICurrentUser is a scoped service populated per-request by UseUserContext(). It exposes:
| Member | Type | Description |
|---|---|---|
UserId |
string? |
Primary subject identifier (sub claim by default) |
Email |
string? |
User email address |
DisplayName |
string? |
Display / full name |
Acr |
string? |
Authentication Context Class Reference; "step-up" for step-up tokens (see AcrValues) |
OperationContext |
string? |
Operation context from step-up proof tokens; identifies the sensitive operation |
IsAuthenticated |
bool |
true when a valid identity was resolved |
IpAddress |
string? |
Remote IP from HttpContext.Connection |
Roles |
IReadOnlySet<string> |
Case-insensitive role set |
Permissions |
IReadOnlySet<string> |
Case-insensitive permission set |
Principal |
ClaimsPrincipal? |
Underlying ASP.NET Core principal |
IsInRole(role) |
bool |
Role membership check |
HasPermission(perm) |
bool |
Permission check |
GetProperty<T>(key) |
T? |
Read a custom property stored in UserInfo.Properties |
Custom properties on UserInfo
UserInfo accepts an optional ImmutableDictionary<string, object> for arbitrary per-request data (e.g. tenant metadata resolved from the token). Access it via GetProperty<T>():
var userInfo = new UserInfo(
userId,
email,
displayName,
roles,
permissions,
Properties: ImmutableDictionary<string, object>.Empty
.Add("tenantId", "tenant-abc")
.Add("plan", "pro"));
// Later, in any service:
var tenantId = currentUser.GetProperty<string>("tenantId");
Permission-Based Authorization
Decorate controllers or actions with [HasPermission]. Multiple attributes require all permissions (AND semantics).
[HasPermission("articles.edit")]
public IActionResult EditArticle(int id) { ... }
[HasPermission("articles.delete")]
[HasPermission("articles.edit")] // user must have BOTH
public IActionResult DeleteArticle(int id) { ... }
Policies are resolved dynamically by PermissionPolicyProvider — no manual policy registration required.
Step-Up Authentication
Step-up authentication is a re-authentication event: the user proves their identity again for a specific sensitive operation without replacing their existing session token.
Calls continue to send the normal Authorization: Bearer ... header. The step-up proof is sent separately in X-Step-Up-Proof.
Static step-up (endpoint-declared)
Use RequireStepUp() on minimal API endpoints that always require elevated assurance:
app.MapDelete("/account", DeleteAccountHandler)
.RequireStepUp("delete_account");
The caller must send a valid proof token (issued by the IAM service's step-up verify endpoint) in X-Step-Up-Proof.
operationContext is optional, but recommended. It scopes the proof token to exactly this operation and helps prevent replay against other step-up endpoints.
Dynamic step-up (handler-determined)
Inject IStepUpProofValidator into command handlers or services when step-up depends on runtime conditions:
public class TransferFundsHandler(IStepUpProofValidator stepUpValidator)
{
public Result Handle(TransferFundsCommand command)
{
if (command.Amount > 10_000)
{
var proof = stepUpValidator.Validate("high_value_transfer");
if (proof != StepUpProofValidationResult.Valid)
return Result.Failure("Step-up required for high-value transfers.");
}
// ... proceed
}
}
StepUpProofValidationResult
| Value | Meaning |
|---|---|
Valid |
Proof token is valid, has acr=step-up, and required operation context (if any) matches |
Missing |
X-Step-Up-Proof header is missing |
Invalid |
Token is malformed, failed validation, or is not a step-up token |
ContextMismatch |
Token operation_context does not match the required value |
Expired |
Proof token is expired |
Header constant
Use SecurityHeaderNames.StepUpProof ("X-Step-Up-Proof") when constructing client requests instead of hardcoding the header name.
Custom User Context Resolver
The default ClaimsUserContextResolver reads identity data straight from JWT claims. To load roles or permissions from a database (or any other source), implement IUserContextResolver.
Type-Parameter Registration
public class DatabaseUserContextResolver(
IUserRepository userRepo) : IUserContextResolver
{
public async Task<UserInfo?> ResolveAsync(HttpContext context)
{
if (context.User.Identity?.IsAuthenticated != true)
return null;
var userId = context.User.FindFirstValue("sub");
if (userId is null) return null;
var user = await userRepo.GetByIdAsync(userId);
if (user is null) return null;
return new UserInfo(
userId,
user.Email,
user.DisplayName,
user.Roles.ToHashSet(),
user.Permissions.ToHashSet());
}
}
// Registration
builder.Services.AddSecurity(options =>
options.UseResolver<DatabaseUserContextResolver>());
Factory Registration
Use the factory overload when the resolver depends on services not available at configuration time:
builder.Services.AddSecurity(options =>
options.UseResolver(sp =>
{
var repo = sp.GetRequiredService<IUserRepository>();
return new DatabaseUserContextResolver(repo);
}));
Custom Claim Mapping
ClaimsUserContextResolver reads claims using the names defined in UserClaimMapping. Override any of them via ConfigureClaimMapping() if your identity provider uses non-standard claim types:
builder.Services.AddSecurity(options =>
{
options.AddJwtBearer(jwt => { /* ... */ });
options.ConfigureClaimMapping(mapping =>
{
mapping.UserId = "oid"; // Azure AD object ID
mapping.Email = "preferred_username";
mapping.DisplayName = "name"; // default — shown for clarity
mapping.Roles = "roles"; // Azure AD app roles
mapping.Permissions = "scp"; // OAuth 2 scopes as permissions
});
});
Default claim type mapping:
| Property | Default claim type |
|---|---|
UserId |
sub |
Email |
email |
DisplayName |
name |
Roles |
role |
Permissions |
permission |
Security Headers
UseSecurityHeaders() adds OWASP-recommended response headers and strips server-identifying headers. Call it with no arguments to apply the defaults, or supply a configuration action to customise any aspect.
Default headers
| Header | Default value |
|---|---|
X-Content-Type-Options |
nosniff |
X-Frame-Options |
DENY |
Referrer-Policy |
strict-origin-when-cross-origin |
Permissions-Policy |
Disables accelerometer, camera, geolocation, gyroscope, magnetometer, microphone, USB |
Content-Security-Policy |
default-src 'self'; frame-ancestors 'none' |
Server and X-Powered-By are removed from every response.
Default usage (no configuration needed)
app.UseSecurityHeaders();
Customising individual headers
Pass an Action<SecurityHeadersOptions> to override any value. Set a property to null to suppress that header entirely.
app.UseSecurityHeaders(options =>
{
options.FrameOptions = "SAMEORIGIN"; // relax framing restriction
options.ReferrerPolicy = "no-referrer";
options.PermissionsPolicy = null; // suppress the header
});
Configuring Content-Security-Policy
Use the fluent CspBuilder or supply a raw string:
app.UseSecurityHeaders(options =>
{
options.WithContentSecurityPolicy(csp => csp
.Default("'self'")
.Script("'self'")
.Style("'self'")
.Image("'self'", "data:")
.Font("'self'")
.Connect("'self'")
.FrameAncestors("'none'"));
});
Route-specific CSP overrides
Apps that serve a developer UI (e.g. Scalar API reference) at a specific path can register a per-route policy that overrides the global one only for requests under that prefix:
app.UseSecurityHeaders(options =>
{
// Scalar injects inline scripts/styles — allow them only on that route
options.AddRouteContentSecurityPolicy("/scalar", csp => csp
.Default("'self'")
.Script("'self'", "'unsafe-inline'")
.Style("'self'", "'unsafe-inline'")
.Image("'self'", "data:", "https:")
.Font("'self'", "data:")
.Connect("'self'")
.FrameAncestors("'none'"));
});
Development-only overrides
Use IWebHostEnvironment at the call site to apply settings that should only apply in development:
app.UseSecurityHeaders(options =>
{
var connectSrc = app.Environment.IsDevelopment()
? ["'self'", "ws://localhost:*", "wss://localhost:*"] // allow browser-refresh WebSocket
: ["'self'"];
options.WithContentSecurityPolicy(csp => csp
.Default("'self'")
.Connect(connectSrc)
.FrameAncestors("'none'"));
});
Adding and removing custom headers
app.UseSecurityHeaders(options =>
{
options.AddHeader("X-App-Version", "2.1.0"); // add a custom header
options.RemoveHeader("X-AspNet-Version"); // strip an additional header
});
API Reference
SecurityOptions
| Method | Description |
|---|---|
AddJwtBearer() |
Returns a JwtBearerBuilder to configure JWT bearer authentication |
UseResolver<TResolver>() |
Register a custom IUserContextResolver by type |
UseResolver(Func<IServiceProvider, IUserContextResolver>) |
Register a custom resolver via factory |
ConfigureClaimMapping(Action<UserClaimMapping>) |
Override claim type names read by ClaimsUserContextResolver |
JwtBearerBuilder
| Method | Description |
|---|---|
WithOidcProvider(authority, audience?) |
Validate tokens from an external OIDC provider |
WithSymmetricKey(signingKey, issuer, audience?) |
Validate locally-issued tokens with a symmetric key |
WithSigningKey(signingKey, issuer, audience?) |
Validate tokens signed with a pre-built SecurityKey (RSA, ECDSA, etc.). Pass the public key for asymmetric schemes. |
WithTokenCookie(cookieName) |
Read bearer token from an HttpOnly cookie (SignalR / SSE) |
WithTokenQueryParam(parameterName) |
Fallback: read bearer token from a query string parameter |
DisableHttpsMetadataRequirement() |
Allow HTTP for OIDC discovery. Never in production. |
DisableIssuerValidation() |
Skip iss claim validation |
DisableAudienceValidation() |
Skip aud claim validation |
DisableLifetimeValidation() |
Skip token expiry check. Never in production. |
WithClockSkew(TimeSpan) |
Override clock skew tolerance (default: 1 minute) |
PreserveInboundClaimTypes() |
Keep WS-Federation claim type URIs instead of mapping to short names |
ICurrentUser
Scoped service available after UseUserContext() runs in the pipeline. See the Current User section for the full member table.
SecurityHeadersOptions
Configure via app.UseSecurityHeaders(options => { ... }).
| Member | Description |
|---|---|
ContentTypeOptions |
X-Content-Type-Options value; null suppresses the header (default: nosniff) |
FrameOptions |
X-Frame-Options value; null suppresses (default: DENY) |
ReferrerPolicy |
Referrer-Policy value; null suppresses (default: strict-origin-when-cross-origin) |
PermissionsPolicy |
Permissions-Policy value; null suppresses |
WithContentSecurityPolicy(string?) |
Set a raw CSP string, or null to suppress |
WithContentSecurityPolicy(Action<CspBuilder>) |
Build a CSP using the fluent builder |
AddRouteContentSecurityPolicy(string, string) |
Override CSP for requests under a path prefix |
AddRouteContentSecurityPolicy(string, Action<CspBuilder>) |
Same, using the builder |
AddHeader(name, value) |
Inject an additional response header |
RemoveHeader(name) |
Remove a response header (in addition to Server / X-Powered-By) |
CspBuilder
| Method | CSP directive |
|---|---|
Default(sources) |
default-src |
Script(sources) |
script-src |
Style(sources) |
style-src |
Image(sources) |
img-src |
Font(sources) |
font-src |
Connect(sources) |
connect-src |
FrameAncestors(sources) |
frame-ancestors |
Media(sources) |
media-src |
Object(sources) |
object-src |
Worker(sources) |
worker-src |
FormAction(sources) |
form-action |
Build() |
Returns the assembled CSP string |
UserInfo
public sealed record UserInfo(
string UserId,
string? Email = null,
string? DisplayName = null,
IReadOnlySet<string>? Roles = null,
IReadOnlySet<string>? Permissions = null,
ImmutableDictionary<string, object>? Properties = null,
string? Acr = null,
string? OperationContext = null);
Returned by IUserContextResolver.ResolveAsync() to describe the resolved identity for the current request.
SecurityClaimTypes
Constants for common JWT claim type names:
SecurityClaimTypes.Subject // "sub"
SecurityClaimTypes.Email // "email"
SecurityClaimTypes.Name // "name"
SecurityClaimTypes.Role // "role"
SecurityClaimTypes.Permission // "permission"
SecurityClaimTypes.Acr // "acr"
SecurityClaimTypes.OperationContext // "operation_context"
AcrValues
Well-known Authentication Context Class Reference (acr) values:
AcrValues.Password // "pwd"
AcrValues.Mfa // "mfa"
AcrValues.StepUp // "step-up"
AcrValues.Social // "social"
AcrValues.ApiKey // "api_key"
SecurityHeaderNames
Header name constants used by the security infrastructure:
SecurityHeaderNames.StepUpProof // "X-Step-Up-Proof"
Dependencies
Microsoft.AspNetCore.App(framework reference — no extra NuGet download)Microsoft.AspNetCore.Authentication.JwtBearer
License
MIT — see LICENSE.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. 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. |
-
net10.0
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 10.0.8)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.