ArrowLabs.Auth.Client 0.1.0

dotnet add package ArrowLabs.Auth.Client --version 0.1.0
                    
NuGet\Install-Package ArrowLabs.Auth.Client -Version 0.1.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="ArrowLabs.Auth.Client" Version="0.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ArrowLabs.Auth.Client" Version="0.1.0" />
                    
Directory.Packages.props
<PackageReference Include="ArrowLabs.Auth.Client" />
                    
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 ArrowLabs.Auth.Client --version 0.1.0
                    
#r "nuget: ArrowLabs.Auth.Client, 0.1.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 ArrowLabs.Auth.Client@0.1.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=ArrowLabs.Auth.Client&version=0.1.0
                    
Install as a Cake Addin
#tool nuget:?package=ArrowLabs.Auth.Client&version=0.1.0
                    
Install as a Cake Tool

ArrowLabs.Auth.Client

.NET client for the ArrowLabs Auth platform. Drop-in JWT bearer authentication for ASP.NET Core APIs that are protected by ArrowLabs-issued access tokens, plus typed accessors for the platform claims.

Access tokens are RS256 JWTs validated offline against the platform's JWKS — your API never calls the auth service on the hot path.

Status: v1 ships JWT validation + claims helpers, the OAuth client (PKCE flow, token exchange, refresh, revoke), and the RabbitMQ event consumer (typed handlers, idempotency, DLQ-on-failure). NuGet packaging lands in a subsequent slice.

Install

dotnet add package ArrowLabs.Auth.Client

Targets net10.0.

Quickstart

Register the scheme and add the standard auth middleware:

using System.Security.Claims;
using ArrowLabs.Auth.Client;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddArrowLabsAuth(options =>
{
    options.BaseUrl = "https://api.arrowlabs.co.uk"; // your auth API base URL
    options.Audience = "your-client-id";             // the aud your tokens are minted for
});
builder.Services.AddAuthorization();

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapGet("/me", (ClaimsPrincipal user) => new
{
    userId = user.GetUserId(),
    org = user.GetOrgSlug(),
    roles = user.GetRoles(),
}).RequireAuthorization();

app.Run();

AddArrowLabsAuth configures ASP.NET Core's JWT bearer handler with Authority = BaseUrl — it discovers /.well-known/openid-configuration, resolves the JWKS endpoint, and handles key fetch/caching/rotation for you. It validates the signature (RS256 only), iss, aud, and exp.

Options

Option Required Default Description
BaseUrl ✓ — Auth API base URL (no trailing slash). Used as the JWT Authority + expected issuer.
Audience ✓ — Expected aud claim — your application's client_id.
RequireHttpsMetadata true Require HTTPS for metadata/JWKS. Set false only for local http://localhost development.
// Local development against a non-HTTPS auth API:
builder.Services.AddArrowLabsAuth(options =>
{
    options.BaseUrl = "http://localhost:5116";
    options.Audience = "your-client-id";
    options.RequireHttpsMetadata = false;
});

Authorization

Standard [Authorize] works out of the box. Roles are mapped from the roles claim, so role checks work too:

[Authorize]                       // any authenticated caller
[Authorize(Roles = "admin")]      // requires the "admin" role
public class ReportsController : ControllerBase { /* ... */ }

User.Identity.Name is the user id (the sub claim).

Claims

Typed extension methods on ClaimsPrincipal map the platform's claim names so you don't hand-read raw strings:

string?               userId    = User.GetUserId();    // sub
string?               orgId     = User.GetOrgId();      // org_id
string?               orgSlug   = User.GetOrgSlug();    // org
string?               email     = User.GetEmail();      // email
IReadOnlyList<string> roles     = User.GetRoles();      // roles (empty if none)
IReadOnlyList<string> appAccess = User.GetAppAccess();  // app_access (empty if none)

Single-value accessors return null when the claim is absent; collection accessors return an empty list. The raw claim names are also exposed as constants on ArrowLabsClaimTypes.

OAuth client

For apps that complete the OAuth redirect flow server-side (confidential or public/PKCE clients), register the OAuth client:

builder.Services.AddArrowLabsOAuthClient(options =>
{
    options.BaseUrl = "https://api.arrowlabs.co.uk";
    options.ClientId = "your-client-id";
    options.ClientSecret = "your-client-secret"; // omit for public (PKCE-only) clients
});

Inject IArrowLabsOAuthClient where you need it.

1. Start the flow (PKCE + redirect)

app.MapGet("/login", (IArrowLabsOAuthClient oauth, HttpContext ctx) =>
{
    var pkce = Pkce.Generate();
    var state = Guid.NewGuid().ToString("N");

    // Stash the verifier + state in the user's session — you'll need them at the callback.
    ctx.Session.SetString("pkce_verifier", pkce.Verifier);
    ctx.Session.SetString("oauth_state", state);

    var url = oauth.BuildAuthorizationUrl(
        codeChallenge: pkce.Challenge,
        redirectUri: "https://app.example.com/callback",
        state: state);

    return Results.Redirect(url);
});

2. Handle the callback (exchange the code)

app.MapGet("/callback", async (string code, string state, IArrowLabsOAuthClient oauth, HttpContext ctx) =>
{
    if (state != ctx.Session.GetString("oauth_state"))
        return Results.BadRequest("state mismatch");

    var result = await oauth.ExchangeCodeAsync(
        code,
        redirectUri: "https://app.example.com/callback",
        codeVerifier: ctx.Session.GetString("pkce_verifier"));

    return result switch
    {
        TokenResult.Success s => Results.Ok(new { s.AccessToken, s.ExpiresIn, s.RefreshToken }),
        TokenResult.Failure f => Results.BadRequest(new { f.Error, f.ErrorDescription }),
        _ => Results.StatusCode(500),
    };
});

3. Refresh and revoke

// Rotate tokens (the refresh token rotates on every use):
var refreshed = await oauth.RefreshTokensAsync(currentRefreshToken);

// Revoke a refresh token (e.g. on sign-out). Idempotent — returns Success even if unknown:
var revoked = await oauth.RevokeTokenAsync(currentRefreshToken);

Both token operations return the same TokenResult union (Success / Failure); transport failures surface as Failure("network_error", …), so there's a single error path with no try/catch for expected outcomes.

Event consumer

For backends that react to Platform Events (user registered, suspended, roles changed, app deleted, …), the library hosts a RabbitMQ consumer that drains your dedicated queue and dispatches typed payloads to handlers you register in DI. It uses RabbitMQ.Client with automatic connection recovery.

The platform's dispatcher provisions a durable per-app queue arrowlabs.events.{your_client_id} and binds it to the event types you subscribe to. The consumer attaches to that queue — it never declares or binds topology.

1. Implement handlers

using ArrowLabs.Auth.Client;

public sealed class ProvisionOnRegister(IAccountService accounts)
    : IArrowLabsEventHandler<UserRegisteredPayload>
{
    public Task HandleAsync(UserRegisteredPayload payload, EventEnvelope envelope, CancellationToken ct)
        => accounts.ProvisionAsync(payload.UserId, payload.Email, ct);
}

public sealed class DisableOnSuspend(IAccountService accounts)
    : IArrowLabsEventHandler<UserSuspendedPayload>
{
    public Task HandleAsync(UserSuspendedPayload payload, EventEnvelope envelope, CancellationToken ct)
        => accounts.DisableAsync(payload.UserId, ct);
}

Handlers are resolved per-message from a DI scope, so they can inject scoped services. Multiple handlers per event type are allowed.

2. Register the consumer and handlers

builder.Services.AddArrowLabsEventConsumer(options =>
{
    options.AmqpUrl = builder.Configuration["Amqp:Url"]!; // amqps://user:pass@host:5671
    options.ClientId = "your-client-id";                  // → queue arrowlabs.events.your-client-id
    options.PrefetchCount = 10;                            // max in-flight; default 10
});

builder.Services.AddArrowLabsEventHandler<ProvisionOnRegister>();
builder.Services.AddArrowLabsEventHandler<DisableOnSuspend>();

The consumer runs as an IHostedService — it starts and stops with your app.

Delivery & failure semantics

  • Ack on success. When every handler for a message resolves, the message is acknowledged.
  • Failure → dead-letter, no requeue. If a handler throws (or the body can't be decoded), the message is nacked without requeue — the broker routes it to the dead-letter queue. No poison-message loops. Failed events are inspectable and retryable from the admin portal's DLQ view.

Idempotency

Events are at-least-once — redelivery happens. Make handlers idempotent. Every EventEnvelope carries a unique EventId; register an IArrowLabsEventDeduplicator to wire it to your own store:

public sealed class RedisDeduplicator(IConnectionMultiplexer redis) : IArrowLabsEventDeduplicator
{
    public async Task<bool> IsDuplicateAsync(string eventId, CancellationToken ct) =>
        await redis.GetDatabase().KeyExistsAsync($"evt:{eventId}");

    public Task MarkProcessedAsync(string eventId, CancellationToken ct) =>
        redis.GetDatabase().StringSetAsync($"evt:{eventId}", "1", TimeSpan.FromDays(7));
}

// builder.Services.AddSingleton<IArrowLabsEventDeduplicator, RedisDeduplicator>();

IsDuplicateAsync is checked before handlers run (a duplicate is acked and skipped); MarkProcessedAsync runs after they all succeed, before the ack. The SDK ships no store — the seam is yours.

The full event catalogue is exposed as PlatformEventTypes constants, and every payload (UserRegisteredPayload, UserSuspendedPayload, …) is a strongly-typed record.

How validation works

  • Offline. Tokens are validated cryptographically against the JWKS — no call to the auth API per request.
  • RS256 only. The accepted algorithm is pinned to RS256, blocking algorithm-confusion attacks.
  • No revocation check. Access tokens are short-lived (15 minutes); revocation is handled by expiry + refresh-token rotation, not by this validator.

Versioning

Follows semver. Published to NuGet from this repository on vX.Y.Z tags.

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.1.0 158 6/20/2026
0.0.0-dev 109 6/20/2026