.NET server quickstart

The .NET server SDK verifies auth4.dev access tokens the same way as the TypeScript server SDK. Use it in an ASP.NET Core API, a plain .NET game backend or a Unity dedicated server.

Before you start

You need three values from the console:

OptionConsole valueExample
TenantIdProject IDten_abcdefghijklmnop
IssuerIssuerhttps://auth.auth4.dev/t/ten_abcdefghijklmnop
AudienceAudience (the client ID of the application calling your API)cli_…

Instead of Issuer you can set AuthOrigin, for examplehttps://auth.auth4.dev, and the SDK uses{AuthOrigin}/t/{TenantId}. Signing keys always come from{issuer}/.well-known/jwks.json, never from a URL in the token.

1. Add the SDK

The SDK is two projects in packages/sdk-dotnet:Auth4.Server (for .NET 8 and .NET Standard 2.1) andAuth4.Server.AspNetCore (for ASP.NET Core 8 and later). They aren't on NuGet yet, so reference the project from a copy of the repository:

GameApi.csproj
<ItemGroup>
  <ProjectReference Include="path/to/auth4/packages/sdk-dotnet/src/Auth4.Server.AspNetCore/Auth4.Server.AspNetCore.csproj" />
</ItemGroup>

2. Protect an ASP.NET Core API

AddAuth4Bearer registers an authentication scheme. Requests without anAuthorization header pass through to anonymous endpoints. A missing or invalid token on a protected endpoint gets 401 with WWW-Authenticate: Bearer.

Excerpt from examples/dotnet/GameApi/Program.cs
builder.Services.AddAuthentication(Auth4BearerDefaults.AuthenticationScheme)
    .AddAuth4Bearer(options =>
    {
        options.TenantId = Setting("AUTH4_TENANT_ID");
        options.Issuer = Setting("AUTH4_ISSUER");
        options.Audience = Setting("AUTH4_AUDIENCE");
    });

builder.Services.AddAuthorization(options =>
{
    // Guests can play, but only upgraded players can trade.
    options.AddPolicy("upgraded", policy => policy.RequireAuthenticatedUser().RequireNonGuest());
});
Excerpt from examples/dotnet/GameApi/Program.cs
app.MapGet("/", () => Results.Ok(new { ok = true }));

app.MapGet("/profile", (HttpContext context) =>
{
    var token = context.GetAuth4AccessToken()!;
    return Results.Ok(new { player = token.Subject, guest = token.IsGuest, roles = token.Roles });
}).RequireAuthorization();

app.MapPost("/moderation/kick", () => Results.Ok(new { kicked = true }))
    .RequireAuthorization(new Microsoft.AspNetCore.Authorization.AuthorizeAttribute { Roles = "moderator" });

var trade = app.MapPost("/trade", () => Results.Ok(new { traded = true }));
if (live)
{
    trade.RequireAuthorization(new Microsoft.AspNetCore.Authorization.AuthorizeAttribute
    {
        AuthenticationSchemes = "Auth4Live",
        Policy = "upgraded",
    });
}
else
{
    trade.RequireAuthorization("upgraded");
}
  • The user's name is the player's user ID (sub). GetAuth4AccessToken returns the verified token with every claim.
  • Roles from the token become ASP.NET Core roles, so [Authorize(Roles = "moderator")]and User.IsInRole work.
  • RequireNonGuest lets only upgraded players through, and RequireScope requires a granted scope.

Run the example from the repository root:

AUTH4_TENANT_ID=ten_… \
AUTH4_ISSUER=https://auth.auth4.dev/t/ten_… \
AUTH4_AUDIENCE=cli_… \
dotnet run --project examples/dotnet/GameApi --urls http://127.0.0.1:5080

curl -H "Authorization: Bearer $ACCESS_TOKEN" http://127.0.0.1:5080/profile

To catch a wrong project ID or auth origin at startup, call DiscoverAsync(). It checks that the issuer's discovery document names the same issuer and key URL:

Excerpt from examples/dotnet/GameApi/Program.cs
// Catch a wrong project ID or auth origin at startup instead of on the first request.
await app.Services.GetRequiredService<Auth4BearerClients>().Get().DiscoverAsync();

Without ASP.NET Core

In a plain .NET backend or a Unity dedicated server, use Auth4ServerClient directly:

Verify a request
using Auth4.Server;

// One client per audience. Reuse it, so its signing-key cache is shared.
var auth4 = new Auth4ServerClient(new Auth4ServerOptions
{
    TenantId = "ten_…",
    AuthOrigin = "https://auth.auth4.dev", // or Issuer = "https://auth.auth4.dev/t/ten_…"
    Audience = "cli_…",
});

try
{
    var token = await auth4.ValidateAuthorizationHeaderAsync(authorizationHeader);
    // token.Subject is the player's user ID. Key your game data on it.
}
catch (Auth4TokenException error) when (error.IsOutage)
{
    // Signing keys or introspection were unavailable: report an outage, don't sign the player out.
}
catch (Auth4TokenException)
{
    // Missing, malformed, expired or otherwise invalid: reject the request.
}

For a Unity dedicated server, build Auth4.Server for .NET Standard 2.1 and add the DLL to the server build together with System.Text.Json, which Unity doesn't include. Game clients don't verify tokens; they use the Unity SDK.

3. Use the claims

PropertyClaimMeaning
SubjectsubThe player's user ID. It stays the same when a guest upgrades.
TenantIdtenant_idYour project ID. Always equal to the configured TenantId.
ClientIdclient_idThe application the token was issued to.
ScopesscopeGranted scopes. Check one with HasScope.
SessionIdsidThe session ID, when the token belongs to a session.
IsGuestauth4_guesttrue while the player hasn't upgraded their guest account. The next token after an upgrade says false.
Rolesauth4_rolesThe player's roles in this application. Check them with HasRole orHasAnyRole. See Roles and metadata.
Metadataauth4_metaThe player's flat metadata object. Empty when the player has none.
ExpiresAtexpAt most five minutes after issue.

What the SDK checks

  • The RS256 signature, with a key from the configured issuer, and the token typeat+jwt.
  • iss, aud, tenant_id, exp andnbf, with no clock tolerance by default. Allow up to 60 seconds of drift withClockTolerance.
  • The claim rules auth4.dev issues tokens with. A token with an unknown claim, a malformed role or a nested metadata value is rejected.
  • ID tokens, tokens from other projects and console management tokens are always rejected.

ID tokens have their own method, ValidateIdTokenAsync. Use it only to confirm a sign-in for your own client ID, never to authorize an API call:

var idToken = await auth4.ValidateIdTokenAsync(rawIdToken, clientId: "cli_…", expectedNonce: nonce);

Failures and key rotation

  • Signing keys are cached for five minutes. A token signed with an unknown key ID triggers one key refresh, at most once every 30 seconds, so key rotation needs no restart.
  • Failures throw Auth4TokenException. Only the code InvalidToken means the token is bad. JwksUnavailable, JwksRefreshLimited andIntrospectionUnavailable are outages, and IsOutage istrue.
  • The ASP.NET Core handler answers an outage with 503 and Retry-After: 5instead of 401, so players aren't signed out. SetOutageAsServiceUnavailable = false to answer 401 instead.

Introspect mode

Introspect mode asks the project's introspection endpoint whether the token's session is still live after offline verification passes. It needs the client ID and secret of a confidential server client of the same project:

Introspect mode
options.Introspection = new Auth4IntrospectionOptions
{
    ClientId = configuration["AUTH4_SERVER_CLIENT_ID"],
    ClientSecret = configuration["AUTH4_SERVER_CLIENT_SECRET"],
    CacheTtl = TimeSpan.FromSeconds(5),
};

The example adds it as a second scheme, so only sensitive endpoints pay for the extra request:

Excerpt from examples/dotnet/GameApi/Program.cs
// Optional: a second scheme that also checks the session is still live, for sensitive endpoints.
var serverClientId = builder.Configuration["AUTH4_SERVER_CLIENT_ID"];
var serverClientSecret = builder.Configuration["AUTH4_SERVER_CLIENT_SECRET"];
var live = !string.IsNullOrEmpty(serverClientId) && !string.IsNullOrEmpty(serverClientSecret);
if (live)
{
    builder.Services.AddAuthentication().AddAuth4Bearer("Auth4Live", options =>
    {
        options.TenantId = Setting("AUTH4_TENANT_ID");
        options.Issuer = Setting("AUTH4_ISSUER");
        options.Audience = Setting("AUTH4_AUDIENCE");
        options.Introspection = new Auth4IntrospectionOptions
        {
            ClientId = serverClientId!,
            ClientSecret = serverClientSecret!,
            CacheTtl = TimeSpan.FromSeconds(5),
        };
    });
}
  • The endpoint is derived from the configured issuer, never from the token. It is available asIntrospectionUrl.
  • An active answer is reused for CacheTtl: 5 seconds by default, at most 60, and zero to check every request. An inactive answer is kept until the token expires.
  • If the endpoint can't be reached or answers with an error, verification fails closed withIntrospectionUnavailable.
  • The SDK never logs the secret or includes it in ToString(). Load it from your secret store, never from source code.

Create a server client in the console under Applications; see Server clients and their secrets.

SDK notes: packages/sdk-dotnet/README.md. Example: examples/dotnet/README.md. Tests: packages/sdk-dotnet/tests/Auth4.Server.Tests and tests/integration/auth4d-169.dotnet-sdk.test.ts.