MesAuth.Authorizer
10.23.0
dotnet add package MesAuth.Authorizer --version 10.23.0
NuGet\Install-Package MesAuth.Authorizer -Version 10.23.0
<PackageReference Include="MesAuth.Authorizer" Version="10.23.0" />
<PackageVersion Include="MesAuth.Authorizer" Version="10.23.0" />
<PackageReference Include="MesAuth.Authorizer" />
paket add MesAuth.Authorizer --version 10.23.0
#r "nuget: MesAuth.Authorizer, 10.23.0"
#:package MesAuth.Authorizer@10.23.0
#addin nuget:?package=MesAuth.Authorizer&version=10.23.0
#tool nuget:?package=MesAuth.Authorizer&version=10.23.0
MesAuth.Authorizer — Integration Guide
Package:
MesAuth.Authorizer· Current version: 10.21.0
Targets: .NET 8 / .NET 9 / .NET 10
Audience: .NET developer adding auth to a new consumer API or troubleshooting an existing integration
Table of Contents
- Quick-start checklist
- Basic auth setup
- AI schema discovery
- Cluster / shared cache (Main.Api)
- IClientCacheService — full reference
- Concurrency primitives — semaphores & work queues
- appsettings.json reference
- Program.cs reference
- HttpContext extension methods
- Common pitfalls
1. Quick-start checklist
- Add
MesAuth.AuthorizerNuGet reference - Set
AppId,AppKey,Issuer,WellknowConfigUriinappsettings.json - Call
await builder.Services.AddMesAuthAsync(opt => { ... })inProgram.cs - Call
app.UseCors()beforeapp.UseMesAuth()— order matters - Confirm
AutoRegisterEndpoints = trueif you want endpoints auto-published to MesAuth
2. Basic auth setup
2.1 Service registration
await builder.Services.AddMesAuthAsync(opt =>
{
opt.AppId = builder.Configuration["MesAuth:AppId"]!;
opt.AppKey = builder.Configuration["MesAuth:AppKey"]!;
opt.WellknowConfigUri = builder.Configuration["MesAuth:WellknowConfigUri"]!;
opt.Issuer = builder.Configuration["MesAuth:Issuer"]!;
});
AddMesAuthAsync is an async method — you must await it. It fetches the OpenID Connect discovery document at startup to obtain JWKS, audience, and endpoint URLs. If the auth server is unreachable at startup the app still starts, logging a warning; auth auto-heals on the first request once the server is back.
2.2 Middleware order
app.UseRouting(); // must be first
app.UseCors(); // must be before UseMesAuth
app.UseMesAuth(); // = UseAuthentication + UseAuthorization + PermMiddleware
app.MapControllers();
UseMesAuth() registers three things in order:
- A response header hook that appends
Access-Control-Expose-Headers: X-MA-PERM, MesAuthbefore each response (needed for CORS) UseAuthentication()+UseAuthorization()UseMiddleware<PermMiddleware>()— fetches and evaluates per-request permissions
If you need to register middleware between UseAuthorization and PermMiddleware, call each separately instead of UseMesAuth().
2.3 Authentication schemes
| Scheme constant | Value | Used for |
|---|---|---|
AuthenServiceExtension.JwtScheme |
"Bearer" |
Browser/SPA users (cookie or header) |
AuthenServiceExtension.AppKeyScheme |
"AppKey" |
Service-to-service via X-APP-ID / X-APP-KEY |
Policy "JwtOrAppKey" |
either scheme | Default — accepts both |
Policy "RequireAppKey" |
AppKey only | Internal/cluster endpoints |
Use [Authorize(Policy = "RequireAppKey")] on endpoints that should only accept app credentials.
2.4 Token extraction priority
For JWT Bearer, the middleware reads tokens in this order per request:
- Cookie — name comes from discovery (
mes_auth_tokenby default) Authorization: Bearer <token>header?access_token=query parameter — only for paths starting with/hub(SignalR)
2.5 Bypassing permission checks
Two mechanisms skip PermMiddleware:
// Option A: standard ASP.NET [AllowAnonymous]
app.MapGet("/public", () => "hello").AllowAnonymous();
// Option B: MesAuth-specific bypass (keeps auth cookies but skips permission lookup)
app.MapGet("/webhook", WebhookHandler).MesAuthException();
// Option C: [MesAuthExceptionAttribute] on a controller action
[MesAuthException]
public IActionResult Callback() { ... }
MesAuthException is useful for callbacks that receive valid JWTs but whose permission code isn't registered in MesAuth (e.g., OAuth callbacks, approval webhooks from MesAuth itself).
2.6 Key rotation
When MesAuth rotates its RS256 signing key, the JwtBearer OnAuthenticationFailed event catches SecurityTokenSignatureKeyNotFoundException, calls DiscoveryService.RefreshAsync(), fetches the new JWKS, and retries validation in the same request — transparent to callers. Concurrent requests during rotation all wait on a single-flight semaphore so only one HTTP call is made.
3. AI schema discovery
When DiscoverAiSchemas = true (the default), the library auto-registers your API's OpenAPI schemas with MesAuth so the AI assistant can describe and call your endpoints.
3.1 How it works
After ApplicationStarted, EndpointSchemaDiscoveryService waits AiDiscoveryDelaySeconds seconds (default 30), then:
- Resolves an OpenAPI document — tries Swashbuckle (
ISwaggerProvider) first, then the built-inAddOpenApi()(OpenApiDocumentService). If neither is registered it exits cleanly. - Extracts per-operation request/response/path-parameter JSON Schemas.
- POSTs them in batches of
AiDiscoveryBatchSize(default 25) to/function/register-schemason MesAuth.Api.
3.2 Options
| Option | Default | Notes |
|---|---|---|
DiscoverAiSchemas |
true |
Set false to disable entirely |
AiDiscoveryDelaySeconds |
30 |
Delay after startup before POSTing. Set higher for slow-starting services |
AiDiscoveryBatchSize |
25 |
Operations per POST request |
AiDiscoverySwaggerDocName |
"v1" |
The Swashbuckle document name (passed to ISwaggerProvider.GetSwagger) |
await builder.Services.AddMesAuthAsync(opt =>
{
// ...base options...
opt.DiscoverAiSchemas = true;
opt.AiDiscoveryDelaySeconds = 60; // give slow migrations time to finish
opt.AiDiscoveryBatchSize = 50;
opt.AiDiscoverySwaggerDocName = "v1";
});
3.3 Auth server: skip the HTTP self-call
When IsAuthServer = true, the HTTP self-call to register schemas hits the service's own external hostname, which IIS loopback cannot reach. Use LocalEndpointSchemaRegisterResolver instead:
opt.IsAuthServer = true;
opt.LocalEndpointSchemaRegisterResolver = async (appId, appKey, ops) =>
{
// Call your in-process registration service directly
return await schemaService.RegisterAsync(appId, appKey, ops);
};
4. Cluster / shared cache (Main.Api)
In a multi-instance deployment, each consumer app instance has its own in-process IMemoryCache. Without coordination, a key rotation forces every instance to independently re-fetch JWKS; a user permission change isn't visible until each instance's cache expires; and a token refresh on one instance produces a new access token that other instances haven't heard about, causing concurrent silent-refresh storms.
Main.Api (the YARP reverse-proxy layer) exposes /internal/cache/* and /internal/coord/* endpoints that act as a shared, cluster-wide in-memory store. Setting MainBaseUrl wires all consumer instances into this shared cache automatically.
4.1 Enabling
"MesAuth": {
"MainBaseUrl": "https://mes.kefico.vn"
}
Or in Program.cs:
opt.MainBaseUrl = builder.Configuration["MesAuth:MainBaseUrl"];
// Leave null/empty for single-instance deployments — all operations become no-ops.
MainBaseUrl is auto-derived from Issuer when not set and DirectDiscoveryBaseUrl is absent. For example, Issuer = "https://mes.kefico.vn/auth" → MainBaseUrl = "https://mes.kefico.vn". Set it explicitly if Main.Api is at a different address than the auth server.
Exception — Main.Api itself: Main.Api IS the proxy. Setting MainBaseUrl would cause it to call its own /internal/* endpoints circularly. Use DirectDiscoveryBaseUrl instead, which suppresses auto-derive:
"MesAuth": {
"DirectDiscoveryBaseUrl": "http://localhost:7199"
}
4.2 What gets cached in Main.Api
| Data | L1 (in-process) TTL | L2 (Main.Api) TTL | L2 key prefix |
|---|---|---|---|
| OpenID discovery config + JWKS | 5 min (with L2), 30 min (standalone) | 25 min | ma:disc: |
| User permissions | 2 min (with L2), CacheTtl (standalone) |
max(60s, CacheTtl) |
ma:uperm:{appId}: |
| App/service permissions | 2 min (with L2), CacheTtl (standalone) |
max(60s, CacheTtl) |
ma:aperm: |
| Refresh token results | 30 s (in-process fallback) | 30 s | ma:refresh: |
All L2 keys are hashed: prefix + first-8-bytes-of-SHA256(input) as lowercase hex. You will never see a raw user ID or token in Main.Api's store.
4.3 L1 + L2 cache flow (per request)
Request arrives
└─ L1 hit? → return immediately (no network)
└─ L2 hit? → populate L1, return (one Main.Api HTTP call)
└─ origin fetch (MesAuth.Api) → populate L1 + L2 (fire-and-forget)
L1 is always checked first. When MainBaseUrl is set, L1 TTLs are shortened (2 min for permissions, 5 min for discovery) so that changes published to L2 by other instances become visible within the L1 window.
4.4 Behavior when Main.Api is unreachable
IClientCacheService catches all exceptions and returns null/false/0 without throwing. The system degrades gracefully:
- Discovery — falls through to origin (MesAuth.Api direct call)
- Permissions — falls through to origin (MesAuth.Api direct call); results cached in L1 only
- Refresh results — falls back to in-process
IMemoryCache(30 s sliding TTL) - Semaphores / queues —
AcquireSemaphoreAsyncreturnsfalse;QueuePopAsyncreturnsnull
All Main.Api failures are logged at Warning level, rate-limited to once per 60 seconds, so a sustained outage doesn't flood logs.
5. IClientCacheService — full reference
Inject IClientCacheService directly in your own services to use Main.Api's shared cache for application-level data. When MainBaseUrl is not configured, all calls are no-ops.
public class MyService
{
private readonly IClientCacheService _cache;
public MyService(IClientCacheService cache) => _cache = cache;
}
5.1 Key/value store
// Get — returns null on miss or when Main.Api is unavailable
string? value = await _cache.GetAsync("my:key");
// Set — ttlSeconds = 0 means no expiry
await _cache.SetAsync("my:key", "hello", ttlSeconds: 300);
// Delete
await _cache.DeleteAsync("my:key");
All values are string. Serialize complex objects yourself:
var json = JsonSerializer.Serialize(myDto);
await _cache.SetAsync("config:v2", json, ttlSeconds: 600);
var raw = await _cache.GetAsync("config:v2");
var dto = raw != null ? JsonSerializer.Deserialize<MyDto>(raw) : null;
5.2 Distributed semaphore
var token = Guid.NewGuid().ToString(); // unique per acquisition attempt
bool acquired = await _cache.AcquireSemaphoreAsync(
key: "job:report-gen",
ttlMs: 30_000, // auto-release after 30 s (guards against crashes)
token: token);
if (acquired)
{
try { await RunExpensiveJobAsync(); }
finally { await _cache.ReleaseSemaphoreAsync("job:report-gen", token); }
}
else
{
// Another instance holds the lock — skip or queue
}
token is a fencing token: ReleaseSemaphoreAsync returns false if the token doesn't match the current owner, preventing accidental release of someone else's lock. Choose TTL conservatively — a crashed holder releases automatically after ttlMs milliseconds.
5.3 Named work queue
// Producer — push serialized work items
int queueLength = await _cache.QueuePushAsync("email:queue", JsonSerializer.Serialize(job));
// Consumer — pop one item at a time (FIFO)
string? raw = await _cache.QueuePopAsync("email:queue");
if (raw != null)
{
var job = JsonSerializer.Deserialize<EmailJob>(raw);
await SendEmailAsync(job);
}
The queue is backed by an in-memory list in Main.Api — it is not durable. If Main.Api restarts, queued items are lost. Use it for short-lived coordination (e.g., "notify all instances to reload config"), not as a message broker.
5.4 Key naming conventions
Follow the library's own convention: prefix:qualifier in lowercase, with ShortHash for sensitive inputs.
ma:disc: — discovery (internal, do not reuse)
ma:uperm:{appId}: — user permissions (internal)
ma:aperm: — app permissions (internal)
ma:refresh: — refresh results (internal)
yourapp:config:v1 — good: namespace + purpose + version
yourapp:lock:report — good: namespace + purpose
yourapp:q:notify — good: namespace + "q" for queues
Avoid raw user IDs or tokens in keys. Hash sensitive inputs with SHA256 before using them as key segments.
6. Concurrency primitives — semaphores & work queues
6.1 In-process single-flight (IRefreshCoordinator)
The library registers IRefreshCoordinator as a singleton. It provides an in-process SemaphoreSlim-per-token pattern so that concurrent requests sharing the same refresh token only trigger one actual refresh call.
You can inject and use it for your own single-flight patterns:
public class MyService
{
private readonly IRefreshCoordinator _coord;
public MyService(IRefreshCoordinator coord) => _coord = coord;
public async Task<bool> DoOncePerKeyAsync(string key, Func<Task<bool>> action)
{
// Only one caller executes 'action' per unique key at a time.
// Concurrent callers with the same key queue and all receive the result.
return await _coord.TryRefreshAsync(key, action);
}
}
IRefreshCoordinator is in-process only — it does not coordinate across instances. For cross-instance single-flight use IClientCacheService.AcquireSemaphoreAsync.
6.2 Cross-instance leader election pattern
public async Task RunOnceAcrossClusterAsync(CancellationToken ct)
{
var token = Guid.NewGuid().ToString();
const string key = "myapp:leader:daily-job";
const int ttlMs = 60_000; // 60 s — must exceed job duration
bool isLeader = await _cache.AcquireSemaphoreAsync(key, ttlMs, token, ct);
if (!isLeader) return; // another instance won
try
{
await RunDailyJobAsync(ct);
}
finally
{
await _cache.ReleaseSemaphoreAsync(key, token, ct);
}
}
When MainBaseUrl is not configured, AcquireSemaphoreAsync always returns false, so the pattern safely degrades: every instance runs independently (same as single-instance mode).
7. appsettings.json reference
{
"MesAuth": {
// ── Required ──────────────────────────────────────────────────────────────
// Application identity registered in MesAuth.Api
"AppId": "MYAPP",
"AppKey": "your-secret-key-at-least-16-chars",
// Path to the OpenID Connect discovery document on MesAuth.Api
// Always "/.well-known/openid-configuration" unless customized
"WellknowConfigUri": "/.well-known/openid-configuration",
// Public base URL of MesAuth.Api including path prefix
// Used as: JWT issuer claim, token endpoint prefix, permission endpoint prefix
"Issuer": "https://mes.kefico.vn/auth",
// ── Recommended ───────────────────────────────────────────────────────────
// Set true only for MesAuth.Api itself — tells the library it IS the auth server
// Consumer apps must leave this false (the default)
"IsAuthServer": false,
// Post endpoints to MesAuth at startup so the admin console can manage permissions
"AutoRegisterEndpoints": true,
// Minutes before token expiry to trigger a proactive silent refresh
// Prevents the auth server and consumer from racing to refresh the same token
// 0 = disable proactive refresh (tokens are only refreshed after expiry)
"ProactiveRefreshMinutes": 1,
// ── Cluster / shared cache ─────────────────────────────────────────────────
// Base URL of Main.Api (the YARP gateway and shared cache host)
// When set: L2 cache for discovery, permissions, and refresh results uses Main.Api
// When omitted: auto-derived from Issuer (e.g. https://mes.kefico.vn/auth → https://mes.kefico.vn)
// Set explicitly when Main.Api is at a different host than the auth server
// Set to "" to disable L2 cache completely (single-instance deployments)
"MainBaseUrl": "https://mes.kefico.vn",
// Only needed when THIS service IS Main.Api (the YARP proxy).
// Points directly at the auth backend, bypassing YARP, for startup discovery/JWKS calls.
// Also suppresses MainBaseUrl auto-derive to prevent circular /internal/* calls.
// Leave unset for all other consumer apps.
"DirectDiscoveryBaseUrl": "",
// ── Permission cache ───────────────────────────────────────────────────────
// How long to cache user/app permissions in L1 (in-process)
// Sliding expiry — clock resets on each cache hit
// Default: 15 minutes. Lower = more MesAuth.Api traffic; Higher = slower permission revocation
// When MainBaseUrl is set, L1 TTL is capped at 2 min regardless of this setting;
// L2 TTL = max(60s, CacheTtl) in seconds
// "CacheTtl": "00:15:00", // not a top-level appsettings key — set via code (see §8)
// ── AI schema discovery ────────────────────────────────────────────────────
// Push OpenAPI schemas to MesAuth so the AI assistant can describe and call your endpoints
"DiscoverAiSchemas": true,
"AiDiscoveryDelaySeconds": 30, // wait after startup before POSTing
"AiDiscoveryBatchSize": 25, // operations per POST
"AiDiscoverySwaggerDocName": "v1"
}
}
CacheTtlis not read from configuration — set it in theconfigurecallback:
opt.CacheTtl = TimeSpan.FromMinutes(10);
8. Program.cs reference
8.1 Minimal consumer app
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
await builder.Services.AddMesAuthAsync(opt =>
{
opt.AppId = builder.Configuration["MesAuth:AppId"]!;
opt.AppKey = builder.Configuration["MesAuth:AppKey"]!;
opt.WellknowConfigUri = builder.Configuration["MesAuth:WellknowConfigUri"]!;
opt.Issuer = builder.Configuration["MesAuth:Issuer"]!;
});
var app = builder.Build();
app.UseCors();
app.UseMesAuth(); // = UseAuthentication + UseAuthorization + PermMiddleware
app.MapControllers();
app.Run();
8.2 Full consumer app (all options)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(); // required for AI schema discovery
// CORS — must be registered before UseMesAuth()
var origins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>()
?? ["https://mes.kefico.vn"];
builder.Services.AddCors(opt =>
opt.AddDefaultPolicy(p => p.WithOrigins(origins)
.WithMethods("GET","POST","PUT","DELETE","OPTIONS","PATCH")
.AllowAnyHeader()
.AllowCredentials()));
await builder.Services.AddMesAuthAsync(opt =>
{
opt.AppId = builder.Configuration["MesAuth:AppId"]!;
opt.AppKey = builder.Configuration["MesAuth:AppKey"]!;
opt.WellknowConfigUri = builder.Configuration["MesAuth:WellknowConfigUri"]!;
opt.Issuer = builder.Configuration["MesAuth:Issuer"]!;
opt.IsAuthServer = false;
opt.AutoRegisterEndpoints = builder.Configuration.GetValue<bool>("MesAuth:AutoRegisterEndpoints", true);
opt.ProactiveRefreshMinutes = builder.Configuration.GetValue<int>("MesAuth:ProactiveRefreshMinutes", 1);
opt.MainBaseUrl = builder.Configuration["MesAuth:MainBaseUrl"];
opt.CacheTtl = TimeSpan.FromMinutes(15);
// AI schema discovery
opt.DiscoverAiSchemas = true;
opt.AiDiscoveryDelaySeconds = 30;
opt.AiDiscoveryBatchSize = 25;
opt.AiDiscoverySwaggerDocName = "v1";
});
builder.Services.AddHealthChecks();
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI();
app.UseCors(); // ← before UseMesAuth
app.UseMesAuth(); // ← Authentication + Authorization + PermMiddleware
app.MapHealthChecks("/health").AllowAnonymous();
app.MapControllers();
app.Run();
8.3 Auth server (IsAuthServer = true)
The auth server skips HTTP self-calls for discovery and permission resolution — it reads directly from its own in-process objects.
await builder.Services.AddMesAuthAsync(opt =>
{
opt.AppId = "MESAUTH";
opt.AppKey = builder.Configuration["MesAuth:AppKey"]!;
opt.WellknowConfigUri = "/.well-known/openid-configuration";
opt.Issuer = "https://mes.kefico.vn/auth";
opt.IsAuthServer = true;
// Skip HTTP self-call for discovery (IIS can't loopback to external hostname)
opt.LocalDiscoveryConfig = new DiscoveryConfig
{
Issuer = "https://mes.kefico.vn/auth",
JwksUri = "/.well-known/jwks.json",
Audience = "mes-api-clients",
// ... other fields from your JwtAuthConfiguration
};
// Skip HTTP self-call for permission lookup
opt.LocalPermissionResolver = async (userId, appId) =>
await permService.GetUserPermissionsAsync(userId, appId);
// Skip HTTP self-call for refresh tokens
opt.LocalRefreshTokenResolver = async (ctx, refreshToken) =>
{
var result = await tokenService.RefreshAsync(refreshToken);
return new TokenResponse(result.AccessToken, result.Message, result.RefreshToken);
};
// Skip HTTP self-call for function registration
opt.LocalFunctionRegisterResolver = async (appId, appKey, functions) =>
await funcService.RegisterAsync(appId, appKey, functions);
// Skip HTTP self-call for schema registration
opt.LocalEndpointSchemaRegisterResolver = async (appId, appKey, schemas) =>
await schemaService.RegisterAsync(appId, appKey, schemas);
// Disable L2 cache — don't call your own /internal/cache/* endpoint
opt.MainBaseUrl = null;
// DirectDiscoveryBaseUrl is not needed when LocalDiscoveryConfig is set
});
8.4 Main.Api (the YARP gateway — special case)
Main.Api is both a consumer of MesAuth.Authorizer and the YARP reverse proxy that routes to MesAuth.Api. It must not route auth discovery calls through itself.
await builder.Services.AddMesAuthAsync(opt =>
{
opt.AppId = builder.Configuration["MesAuth:AppId"]!;
opt.AppKey = builder.Configuration["MesAuth:AppKey"]!;
opt.WellknowConfigUri = builder.Configuration["MesAuth:WellknowConfigUri"]!;
opt.Issuer = builder.Configuration["MesAuth:Issuer"]!;
opt.IsAuthServer = false;
opt.AutoRegisterEndpoints = builder.Configuration.GetValue<bool>("MesAuth:AutoRegisterEndpoints", false);
opt.DiscoverAiSchemas = false; // Main.Api has no OpenAPI doc of its own
opt.ProactiveRefreshMinutes = builder.Configuration.GetValue<int>("MesAuth:ProactiveRefreshMinutes", 1);
// Bypass YARP for startup discovery — points directly at the auth backend
opt.DirectDiscoveryBaseUrl = builder.Configuration["MesAuth:DirectDiscoveryBaseUrl"];
// Suppresses MainBaseUrl auto-derive (would cause circular /internal/* calls)
opt.MainBaseUrl = builder.Configuration["MesAuth:MainBaseUrl"]; // leave null for Main.Api
});
9. HttpContext extension methods
All extensions are in MesAuth.Authorizer.AuthenServiceExtension.
9.1 User identity
IUser? user = context.GetUser();
// IUser fields (all nullable strings unless noted):
user.UserId // JWT "sub" claim
user.UserName // JWT "name" claim
user.FullName // JWT "family_name" claim
user.Email
user.Department
user.Position
user.EmployeeCode
user.Roles // HashSet<string> from "roles" claim
// HR fields: HrFullNameVn, HrFullNameEn, HrPosition, HrJobTitle,
// HrGender, HrMobile, HrEmail, HrJoinDate, HrBirthDate,
// HrWorkStatus, HrDoiTuong, HrTeamCode, HrLineCode, HrTeamId, HrLineId
Available in SignalR hubs too:
public override async Task OnConnectedAsync()
{
var user = Context.GetUser(); // HubCallerContext extension
}
9.2 Permission inspection
// Permissions granted in this request (populated by PermMiddleware)
HashSet<string>? perms = context.GetCurrentPermissions();
HashSet<string>? userPerms = context.GetUserPermissions(); // JWT path only
HashSet<string>? appPerms = context.GetAppPermissions(); // AppKey path only
// Auth type
bool isUser = context.IsUserAuthenticated(); // JWT bearer
bool isApp = context.IsAppAuthenticated(); // X-APP-ID / X-APP-KEY
// Caller app identity (set when X-APP-ID / X-APP-KEY pass validation)
string? appId = context.GetCallerAppId();
bool hasApp = context.HasCallerApp();
// Permission code that PermMiddleware resolved for this request
string? code = context.GetPermissionCode(); // e.g. "MYAPP.Api.Users.Get"
// Best actor for audit logs: UserName → CallerAppId → fallback
string actor = context.ResolveActor("System");
9.3 Centralized audit logging
Fire-and-forget HTTP call to MesAuth.Api /sys-logs/write:
// Shorthand helpers
context.LogInfo(ClientLogCategory.UserManagement, "UserCreated", "Created user foo");
context.LogWarn(ClientLogCategory.Authorization, "PermDenied", "Attempted admin action");
context.LogError(ClientLogCategory.Security, "SuspiciousLogin", "Rate limit hit", exception: ex);
// Full control
context.WriteLog(
level: ClientLogLevel.Information,
category: ClientLogCategory.Authentication,
eventType: "LoginSuccess",
message: "User logged in",
details: new { ip = "10.1.1.1", method = "LDAP" },
targetUserId: Guid.Parse("..."),
targetUserName: "other.user");
10. Common pitfalls
UseCors() after UseMesAuth()
UseMesAuth() appends expose headers via Response.OnStarting — a LIFO callback list. The CORS Access-Control-Allow-Origin header is also written in OnStarting. If CORS runs after MesAuth's callback, CORS headers are absent and browsers reject the response.
❌ app.UseMesAuth(); app.UseCors();
✅ app.UseCors(); app.UseMesAuth();
AddMesAuthAsync not awaited
The method is async and performs a startup HTTP call. If you forget await it silently runs in the background — services may be registered out of order or the DI container may build before JWT keys arrive.
❌ builder.Services.AddMesAuthAsync(opt => { ... });
✅ await builder.Services.AddMesAuthAsync(opt => { ... });
AppKey too short
ValidateOptions throws at startup if AppKey.Length < 16. Use at least a GUID-length secret.
Missing MainBaseUrl in multi-instance deployment
Without MainBaseUrl, each instance caches permissions independently. After a permission grant/revoke, some instances see the change immediately (their cache expired) while others serve stale permissions for up to CacheTtl (default 15 min). Set MainBaseUrl to point all instances at Main.Api's shared cache.
Main.Api setting its own MainBaseUrl
Main.Api is the shared cache host. If it sets MainBaseUrl pointing to itself, DiscoveryService and PermMiddleware call their own /internal/* endpoints — circular requests that time out at startup. Use DirectDiscoveryBaseUrl (which also suppresses MainBaseUrl auto-derive) instead.
Clock skew
Tokens are validated with ClockSkew = TimeSpan.FromMinutes(2). If the server clock drifts beyond ±2 minutes from the auth server's clock, freshly issued tokens fail validation. Ensure NTP is configured on all hosts. Do not increase ClockSkew to paper over infrastructure problems.
[AllowAnonymous] vs [MesAuthException]
[AllowAnonymous] bypasses all middleware including PermMiddleware — the user is unauthenticated, GetUser() returns null, and no auth cookie is read.
[MesAuthException] (or .MesAuthException()) bypasses only PermMiddleware's permission check. Auth still runs, cookies are still processed, GetUser() returns the authenticated user if a valid JWT/cookie is present. Use this for endpoints that need the user identity but whose permission code isn't registered (callbacks, webhooks from MesAuth itself, approval callbacks).
Permission code staleness after rename
EndpointRegistrationService derives permission codes from route templates at startup: AppId.RouteSegments.HttpMethod. If you rename a route (e.g., api/users → api/members), the new code is registered but any existing role grants attached to the old code are orphaned — users lose access until an admin re-grants under the new code. Treat permission codes as stable identifiers, not cosmetic names.
Proactive refresh race (two-sided refresh)
Without proactive refresh (ProactiveRefreshMinutes = 0), a near-expiry request arrives at both the consumer app and MesAuth.Api simultaneously — the consumer triggers a refresh on the token it just used, while MesAuth simultaneously processes it as a fresh request. Both sides may attempt to refresh the same token concurrently. Set ProactiveRefreshMinutes to a value (default 5, minimum 1 for active deployments) so the consumer refreshes before MesAuth sees the token as expired.
Negative-cache on permission failure
When PermMiddleware cannot reach MesAuth.Api after 3 retries (network failure, 5xx), it stores a null sentinel in L1 for 10 seconds with an absolute expiration. During this window, affected users receive 403/401 even if they have permission, to prevent hammering a struggling auth server. After 10 seconds the negative cache expires and normal lookups resume automatically. This is intentional — do not remove the negative-cache to avoid thundering-herd restorms.
X-Internal-Refresh header
The library marks its own token-refresh HTTP calls with X-Internal-Refresh: true. PermMiddleware.IsLoopbackOrAnonymous checks this header and skips permission lookup, preventing an infinite recursion when the refresh call itself is routed through the same middleware. Do not strip this header in a proxy layer.
| Product | Versions 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 is compatible. 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 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
- MesAuth.SslTrustHelper (>= 1.0.5)
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 10.0.1)
- Microsoft.Data.SqlClient (>= 6.0.2)
- Microsoft.Extensions.Caching.Abstractions (>= 10.0.1)
- Microsoft.Extensions.Caching.Memory (>= 10.0.1)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.1)
- Microsoft.Extensions.Options (>= 10.0.1)
-
net8.0
- MesAuth.SslTrustHelper (>= 1.0.5)
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 8.0.11)
- Microsoft.Data.SqlClient (>= 5.2.2)
- Microsoft.Extensions.Caching.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Caching.Memory (>= 8.0.1)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Options (>= 8.0.2)
-
net9.0
- MesAuth.SslTrustHelper (>= 1.0.5)
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 9.0.1)
- Microsoft.Data.SqlClient (>= 5.2.2)
- Microsoft.Extensions.Caching.Abstractions (>= 9.0.1)
- Microsoft.Extensions.Caching.Memory (>= 9.0.1)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.1)
- Microsoft.Extensions.Options (>= 9.0.1)
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 |
|---|---|---|
| 10.23.0 | 92 | 6/1/2026 |
| 10.22.0 | 86 | 5/30/2026 |
| 10.21.0 | 94 | 5/28/2026 |
| 10.20.1 | 100 | 5/25/2026 |
| 10.20.0 | 98 | 5/25/2026 |
| 10.19.1 | 94 | 5/25/2026 |
| 10.19.0 | 89 | 5/24/2026 |
| 10.18.0 | 92 | 5/24/2026 |
| 10.17.0 | 96 | 5/24/2026 |
| 10.16.2 | 88 | 5/21/2026 |
| 10.16.1 | 90 | 5/21/2026 |
| 10.16.0 | 91 | 5/21/2026 |
| 10.15.7 | 107 | 5/19/2026 |
| 10.15.6 | 96 | 5/19/2026 |
| 10.15.5 | 96 | 5/19/2026 |
| 10.15.4 | 93 | 5/19/2026 |
| 10.15.3 | 106 | 5/19/2026 |
| 10.15.1 | 94 | 5/18/2026 |
| 10.15.0 | 91 | 5/18/2026 |
| 10.14.0 | 94 | 5/18/2026 |