FronteggAuth.AspNetCore
1.0.0
dotnet add package FronteggAuth.AspNetCore --version 1.0.0
NuGet\Install-Package FronteggAuth.AspNetCore -Version 1.0.0
<PackageReference Include="FronteggAuth.AspNetCore" Version="1.0.0" />
<PackageVersion Include="FronteggAuth.AspNetCore" Version="1.0.0" />
<PackageReference Include="FronteggAuth.AspNetCore" />
paket add FronteggAuth.AspNetCore --version 1.0.0
#r "nuget: FronteggAuth.AspNetCore, 1.0.0"
#:package FronteggAuth.AspNetCore@1.0.0
#addin nuget:?package=FronteggAuth.AspNetCore&version=1.0.0
#tool nuget:?package=FronteggAuth.AspNetCore&version=1.0.0
FronteggAuth.AspNetCore
Frontegg CIAM authentication and authorization for
ASP.NET Core, in one AddFronteggAuth call.
Targets net8.0, net9.0, and net10.0.
What it does
AddFronteggAuth registers a smart scheme router over three credential types, and routes each request to
the right one automatically:
| Credential on the request | Scheme used |
|---|---|
API-key header (default X-API-KEY) |
Frontegg.ApiKey — Frontegg API keys / M2M tokens, validated against Frontegg's vendor introspection endpoint |
Authorization: Bearer … |
Bearer — standard JWT bearer validation |
| Neither | Cookies — the interactive session, challenged over OpenID Connect |
On top of that it gives you automatic refresh-token renewal, recovery from the OIDC failure modes that otherwise surface to a user as a blank error page, an optional Redis-backed cookie session store, and three interchangeable ways to express a permission gate.
The package carries no business rules. Every claim-type name is configurable and every product-specific decision enters through an injectable hook, so nothing in it assumes a particular tenant's role or permission vocabulary.
Install
dotnet add package FronteggAuth.AspNetCore
Configure
Bind from the FronteggSettings section:
{
"FronteggSettings": {
"ClientId": "your-frontegg-client-id",
"Authority": "https://auth.example.com", // your Frontegg login domain (the OIDC issuer)
"ApiBaseUrl": "https://api.frontegg.com", // Frontegg REST API
"ApiKey": "your-vendor-m2m-secret", // vendor secret for the management API — keep it out of source
"CookieName": ".FronteggAuth",
"CookieDomain": ".example.com", // optional, for cross-subdomain SSO
"RedisConnectionString": "redis:6379", // optional; this package's own Redis connection
// Optional: numeric permission ID → Frontegg permission key. Supply it only if your application
// addresses permissions by number as well as by key.
"PermissionIdMappings": { "123": "fe.secure.read", "124": "fe.secure.write" },
// Optional: tenant client credentials, for tenant-scoped tokens
"TenantClientId": "…",
"TenantSecret": "…"
}
}
ApiKey and TenantSecret are credentials. Put them in user-secrets, environment variables, or your secret
store — not in a committed appsettings.json.
Register & activate
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFronteggAuth(builder.Configuration);
var app = builder.Build();
app.UseRouting();
app.UseFronteggAuth(); // UseAuthentication → gating → claims enrichment → UseAuthorization
app.MapControllers();
app.Run();
UseFronteggAuth installs four pieces of middleware in an order that matters; call it after UseRouting and
before you map endpoints.
A second overload adjusts settings in code, which is how you reach the options that are delegates rather than values:
builder.Services.AddFronteggAuth(builder.Configuration, options =>
{
options.ConfigureDataProtection = dp => dp.PersistKeysToAzureBlobStorage(blobUri, credential);
});
Authorize
Three surfaces, one rule. They agree because they share a single definition of unconditional access: a
principal holding a role in BypassRoles (default system) or presenting a token type in BypassTokenTypes
(default tenantApiToken) passes every permission check.
Attributes
These are IAuthorizationFilters, so they run in the MVC pipeline only — a minimal-API delegate or a
Razor Component never invokes them. Use policies there instead.
// Native Frontegg permission keys (any-of); denied outright if the user holds a ReversePermission.
[PermissionAuthorize("fe.secure.read")]
public IActionResult Read() => Ok();
// Numeric permission IDs (any-of). Requires PermissionIdMappings or a custom IPermissionIdResolver;
// without one, no numeric claim is ever emitted and this denies everyone.
[PermissionIdAuthorize(123)]
public IActionResult ReadById() => Ok();
// Roles (any-of).
[RoleAuthorize("admin", "editor")]
public IActionResult Manage() => Ok();
// Opt an endpoint out of the package's filters entirely.
[SkipAuth]
public IActionResult Public() => Ok();
Global filter (every controller)
builder.Services.AddControllers(options =>
{
options.Filters.Add(new PermissionAuthorizeAttribute("fe.secure.read"));
// A reverse permission hard-denies its holders even when the forward check would pass:
// options.Filters.Add(new PermissionIdAuthorizeAttribute(123) { ReversePermissions = [456] });
});
Mark the actions that must stay public — sign-in, health checks — with [SkipAuth] or [AllowAnonymous].
Policies (controllers or minimal APIs)
The dynamic policy provider materializes permission policies on demand, so there is nothing to register per permission:
app.MapGet("/secure", () => "ok").RequireAuthorization("perm:fe.secure.read");
app.MapGet("/by-id", () => "ok").RequireAuthorization("permid:123");
[Authorize(Policy = "perm:fe.secure.read")]
public IActionResult Read() => Ok();
Policies have no reverse-permission concept — that exists only on the attributes.
The current user
Inject IIdentityUserService for a strongly-typed view of the principal:
public sealed class MeController(IIdentityUserService identity) : ControllerBase
{
[HttpGet("/me")]
public ActionResult<ApplicationUser?> Me() => identity.User;
}
ApplicationUser exposes permissions in both representations: Permissions (native Frontegg keys such as
fe.secure.read) and PermissionIds (numeric, populated only for keys that IPermissionIdResolver maps).
User vs GetUserAsync()
User projects whatever claims are already on the principal. The claims-enrichment middleware normally puts
the enriched ones there first — but it is skipped for [AllowAnonymous] endpoints, unmatched routes, and the
OIDC callbacks, and it does nothing when the credential carries no user/tenant id. On those paths
CompanyName, PermissionIds, and Roles come back empty.
GetUserAsync() closes that gap: when the principal yields no company name it fetches claims from
IUserClaimsProvider, merges them into the principal, and re-projects. The fetch happens at most once per
request, is skipped when a company name is already present, and fails open — a provider outage logs a
warning and returns the unenriched user rather than throwing. That is safe here specifically because
authorization has already been decided by the time it runs, so an unenriched projection cannot widen access.
[HttpGet("/me")]
public async Task<ActionResult<ApplicationUser?>> Me(CancellationToken cancellationToken)
=> await identity.GetUserAsync(cancellationToken);
Prefer User on ordinary authorized endpoints; reach for GetUserAsync() when the values must be present
regardless of the path.
Another user's permissions
IIdentityUserService only ever describes the current request. To look up a different user — an admin
screen, a background job, an impersonation check — inject IUserPermissionsService:
public sealed class AdminService(IUserPermissionsService permissions)
{
public async Task<bool> CanEditAsync(string userId, string tenantId, CancellationToken cancellationToken)
{
var result = await permissions.GetPermissionsAsync(userId, tenantId, cancellationToken);
return result.Permissions.Contains("fe.secure.write");
}
}
public sealed record UserPermissions(
string[] Permissions, // native Frontegg keys
int[] PermissionIds, // numeric IDs, for the keys IPermissionIdResolver maps
string[] Roles); // role keys held within the tenant
This is a projection over IUserClaimsProvider; it adds no data path of its own. It exists so callers never
name the claim types themselves — those names are configurable, so an application that reads claims directly
breaks silently the moment a host renames one.
Two behaviours worth knowing:
- Failures propagate. A provider outage throws rather than returning an empty set, because "no
permissions" and "could not read permissions" are indistinguishable to a caller and the difference can widen
access for an inverted check. Handle the exception; do not treat it as an empty set. (This is the opposite of
GetUserAsync(), and deliberately so — see above.) - Results are cached per user+tenant for
ClaimsCacheDurationSeconds(default 10). Repeat calls are cheap but may lag a permission change by that window. There is no invalidation hook.
Fail-closed claims enrichment
If the claims authority is unreachable, the default is to reject the request: the provider throws
FronteggClaimsUnavailableException and the enrichment middleware returns 401. A principal whose roles could
not be read is not the same as a principal with no roles.
Setting FailOpenOnClaimsUnavailable = true lets the request continue with an authenticated but role- and
permission-less principal — permission-gated endpoints then return 403 while a bare [Authorize] endpoint
succeeds. Enable it only if that is genuinely what you want during an outage. Failed lookups are never cached
either way, so recovery is immediate.
Pluggable hooks
Defaults are registered with TryAdd*, so registering your own implementation after AddFronteggAuth
wins:
| Interface | Default | Purpose |
|---|---|---|
IUserClaimsProvider |
FronteggUserClaimsProvider — reads roles and permissions from the Frontegg management API |
Source the user's permission/role claims. To consume claims rather than replace the source, use IUserPermissionsService instead. |
IPermissionIdResolver |
DefaultPermissionIdResolver, backed by PermissionIdMappings |
Map a Frontegg permission key → numeric ID. |
IAccountStatusValidator |
NullAccountStatusValidator (allow-all) |
Gate access on account-status claims. ClaimBasedAccountStatusValidator is included. |
IClaimsTransformer |
NullClaimsTransformer (no-op) |
Add application-specific roles/claims after enrichment. |
builder.Services.AddFronteggAuth(builder.Configuration);
builder.Services.AddScoped<IUserClaimsProvider, MyClaimsProvider>();
builder.Services.AddSingleton<IAccountStatusValidator, ClaimBasedAccountStatusValidator>();
Permission keys are tenant-defined, so this package ships no permission constants. Declare your own:
public static class MyPermissions
{
public const string Read = "fe.secure.read";
public const int ReadId = 123;
}
If your Frontegg application is a confidential client (requires an OIDC client secret), post-configure
OpenIdConnectOptions for the Frontegg.Oidc scheme to set one — the default configures a public client with
PKCE.
Redis ticket store
Cookie session tickets are stored in Redis when a connection is available. Redis is resolved the first time
the ticket store is used, not while services are registered, so AddFronteggAuth never blocks startup on a
Redis dial and does not care whether it ran before or after your own registrations.
Resolution order:
FronteggSettings:RedisConnectionString— the package opens, owns, and disposes its ownConnectionMultiplexer, independent of your application's Redis even when both point at the same server (look for theFrontegg:<CookieName>client name inCLIENT LIST). Registered under a DI key, so it never collides with your ownIConnectionMultiplexer.- An
IConnectionMultiplexeryou registered — used only when the setting above is absent, so a multi-instance application gets a distributed ticket store without restating its connection string. The borrowed connection is never disposed by this package. - In-memory — neither of the above. The store is registered so it can still be resolved, but it is deliberately not attached to the cookie: a per-process store behind a load balancer reads as a sign-in loop rather than as an error. The cookie stays self-contained (chunked) and validates on any instance sharing the Data Protection key ring; a warning is logged.
Tickets are namespaced under authticket:<CookieName>:, so sharing a database with application data is safe.
Data Protection
The package always calls AddDataProtection().SetApplicationName(DataProtectionApplicationName) — the key
ring's discriminator, which must match across every instance serving the same cookie. Where those keys are
stored is yours to choose, via the ConfigureDataProtection callback:
builder.Services.AddFronteggAuth(builder.Configuration, options =>
{
options.ConfigureDataProtection = dp => dp.PersistKeysToAzureBlobStorage(blobUri, credential);
});
Left unset, keys stay at the ASP.NET Core default (the local file system), which is per-instance — fine for a single process, a sign-in loop behind a load balancer. The package itself depends on no persistence provider.
For AWS Systems Manager Parameter Store, install the companion package
FronteggAuth.AspNetCore.DataProtection.Aws:
options.PersistDataProtectionKeysToSsm("/myapp/{environment}/dataprotection");
License
MIT.
| 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
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 10.0.11)
- Microsoft.AspNetCore.Authentication.OpenIdConnect (>= 10.0.11)
- StackExchange.Redis (>= 3.1.13)
-
net8.0
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 8.0.30)
- Microsoft.AspNetCore.Authentication.OpenIdConnect (>= 8.0.30)
- StackExchange.Redis (>= 3.1.13)
-
net9.0
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 9.0.19)
- Microsoft.AspNetCore.Authentication.OpenIdConnect (>= 9.0.19)
- StackExchange.Redis (>= 3.1.13)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on FronteggAuth.AspNetCore:
| Package | Downloads |
|---|---|
|
FronteggAuth.AspNetCore.DataProtection.Aws
AWS Systems Manager Parameter Store persistence for FronteggAuth.AspNetCore's Data Protection key ring. Optional companion package — the core package takes no dependency on the AWS SDK. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0 | 126 | 8/18/2026 |