Zautha.AspNetCore 0.4.0

dotnet add package Zautha.AspNetCore --version 0.4.0
                    
NuGet\Install-Package Zautha.AspNetCore -Version 0.4.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Zautha.AspNetCore" Version="0.4.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Zautha.AspNetCore" Version="0.4.0" />
                    
Directory.Packages.props
<PackageReference Include="Zautha.AspNetCore" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Zautha.AspNetCore --version 0.4.0
                    
#r "nuget: Zautha.AspNetCore, 0.4.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Zautha.AspNetCore@0.4.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Zautha.AspNetCore&version=0.4.0
                    
Install as a Cake Addin
#tool nuget:?package=Zautha.AspNetCore&version=0.4.0
                    
Install as a Cake Tool

Zautha.AspNetCore

Zautha for ASP.NET Core (.NET 10): authenticate your API's requests by the Zautha session token your frontend sends (@zautha/js, @zautha/react), and manage your users and sessions through the Backend API.

Install

dotnet add package Zautha.AspNetCore

Keep the keys out of source control, e.g. with user secrets during development:

dotnet user-secrets init
dotnet user-secrets set "Zautha:PublishableKey" "pk_test_…"
dotnet user-secrets set "Zautha:SecretKey" "sk_test_…"

Authenticate requests

using System.Security.Claims;
using Zautha.AspNetCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddZauthaAuthentication(options =>
{
    options.PublishableKey = builder.Configuration["Zautha:PublishableKey"];
    options.AuthorizedParties = ["https://app.example.com"];
});
builder.Services.AddAuthorization();

var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();

app.MapGet("/api/whoami", (ClaimsPrincipal user) => user.GetZauthaAuth().UserId).RequireAuthorization();

app.Run();

The frontend sends the token as Authorization: Bearer <token> (await zautha.session.getToken()). A request is authenticated only when the token

  • is signed (RS256) by one of your instance's keys, read from its JWKS and cached for an hour (an unknown key id triggers a refetch, at most every five minutes),
  • was issued by your instance's Frontend API (from the publishable key) and has not expired (5 s tolerance),
  • was minted for one of AuthorizedParties, the exact origins your frontends are served from.

Anything else gets 401 with WWW-Authenticate: Bearer; the reason goes to the log, never to the response. The options are checked when the app starts: a malformed publishable key or an empty AuthorizedParties stops it.

user.GetZauthaAuth() returns the user id (user_…), session id (sess_…), the authorized party and the minutes since the last first- and second-factor verification. It throws when the request was not authenticated by Zautha — call it only in endpoints that require authorization. Custom claims from your instance's session token template (when your instance defines them) are plain claims: user.FindFirst("plan")?.Value.

To verify without any network request, set options.JwtKey to one of your instance's public signing keys as an RSA public key in PEM (-----BEGIN PUBLIC KEY-----), e.g. made from a key of the instance's JWKS (GetJwksAsync() below).

The scheme (ZauthaDefaults.AuthenticationScheme, "Zautha") is a JwtBearer scheme; its events are yours, e.g. a JSON body for 401:

using Microsoft.AspNetCore.Authentication.JwtBearer;

builder.Services.Configure<JwtBearerOptions>(ZauthaDefaults.AuthenticationScheme, options =>
    options.Events = new JwtBearerEvents
    {
        OnChallenge = async context =>
        {
            context.HandleResponse();
            context.Response.StatusCode = StatusCodes.Status401Unauthorized;
            context.Response.Headers.WWWAuthenticate = "Bearer";
            await context.Response.WriteAsJsonAsync(new { code = "unauthenticated" });
        },
    });

Reverification

For a sensitive operation (changing a payout account, deleting data), ask for a recent identity check instead of just a signed-in user:

app.MapPost("/api/transfer", () => Results.Ok(new { ok = true }))
    .RequireAuthorization()
    .RequireZauthaReverification(ZauthaReverification.Strict);

RequireZauthaReverification lets the request through only when the user verified their identity recently enough:

Requirement Window User with two-step verification User without it
ZauthaReverification.Strict 10 minutes second factor first factor
ZauthaReverification.Moderate 1 hour second factor first factor
ZauthaReverification.Lax 1 day second factor first factor
ZauthaReverification.StrictMfa 10 minutes both factors first factor

The first factor is the password, an email code or a link; the second factor is an authenticator app code or a backup code. ZauthaReverification.Within(TimeSpan.FromMinutes(30), ZauthaReverificationLevel.FirstFactor) sets both yourself (whole minutes from 1 to 1440; the level is SecondFactor when left out, and FirstFactor always asks for the first factor). Otherwise it answers 403 with an application/problem+json body whose code is reverification_required and whose reverification member says what was asked for ({"level":"second_factor","after_minutes":10}). On the frontend, useReverification (@zautha/react) recognizes that response, asks the user to verify again (with the factors that level needs) and retries the request with a fresh token. The filter works on route groups too; a request that is not authenticated by Zautha is challenged (401), so keep RequireAuthorization().

In your own code, user.GetZauthaAuth().IsReverified(ZauthaReverification.Strict) answers the same question. The session token counts whole minutes since each verification (FactorVerificationAge; the second is -1 for a user without two-step verification), so a 10-minute window passes a token that says 9 and not one that says 10. A token can be up to about a minute old, so the decision can lag Zautha's own by about a minute.

Backend API

builder.Services.AddZauthaClient(options =>
{
    options.SecretKey = builder.Configuration["Zautha:SecretKey"];
});

app.MapGet("/api/me", async (ClaimsPrincipal user, IZauthaClient zautha, CancellationToken ct) =>
{
    var me = await zautha.Users.GetAsync(user.GetZauthaAuth().UserId, ct);
    // Only what the browser may see: private metadata stays on the server.
    return Results.Ok(new { id = me.Id, primary_email_address = me.PrimaryEmailAddress, first_name = me.FirstName, last_name = me.LastName });
}).RequireAuthorization();

IZauthaClient covers users (Users.ListAsync, GetAsync, CreateAsync, UpdateAsync, DeleteAsync, BanAsync, UnbanAsync, RemoveMfaAsync, ListSessionsAsync), sessions (Sessions.RevokeAsync) and the instance's public keys (GetJwksAsync).

  • CreateAsync(request, idempotencyKey): retrying with the same key within 24 hours returns the first response instead of creating a second user.
  • UpdateAsync sends only the properties you assign: new UpdateUserRequest { FirstName = null } clears the first name, other fields stay as they are, and assigned metadata replaces the whole object.
  • RemoveMfaAsync removes a user's two-step verification (authenticator app, passkeys and backup codes) when they lost their devices, and ends all their sessions; ZauthaUser.MfaEnabled, TotpEnabled, Passkeys and BackupCodesRemaining show where a user stands.
  • Every error response throws ZauthaApiException; branch on Code (resource_not_found, form_identifier_exists, too_many_requests with RetryAfter, …) and quote TraceId to support. The client does not retry by itself, but app-wide resilience handlers (e.g. ConfigureHttpClientDefaults(b => b.AddStandardResilienceHandler()) in Aspire's ServiceDefaults) apply to it too: pass an idempotencyKey to CreateAsync when you use them.
  • Requests time out after 10 seconds; AddZauthaClient returns the IHttpClientBuilder for your own handlers and timeout.

License

MIT

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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
0.4.0 0 10/2/2026
0.3.0 26 10/1/2026
0.2.0 34 10/1/2026
0.1.0 42 9/30/2026