Skip to content

Latest commit

 

History

History
383 lines (304 loc) · 13.3 KB

File metadata and controls

383 lines (304 loc) · 13.3 KB

Dependency Injection

Register AAuth services in ASP.NET Core and hosted applications using the built-in DI extensions.

Key Principle

How your agent obtains its token depends on its deployment model:

  • Hosted services (web apps, APIs, orchestrators with a stable URL): Self-issue agent tokens at runtime. Generate a key at startup, publish /.well-known/aauth-agent.json, and build tokens locally. No external AP needed.
  • CLI / desktop / mobile agents (no stable URL): Enrol with an Agent Provider once (provisioning step), then refresh tokens from the AP at runtime.

In both cases, the agent token is short-lived (typically 1 hour) and refreshed automatically by the SDK. You never persist it.

flowchart LR
    subgraph Hosted
        H1["Startup: Generate key"] --> H2["Publish /.well-known/aauth-agent.json"]
        H2 --> H3["Runtime: self-issue token via AgentTokenBuilder"]
    end
    subgraph CLI/Desktop
        P["Provisioning: EnrolAsync(keyStore)"] --> C["App config: AAuth:LocalKeyHandle"]
        C --> S["Startup: keyStore.LoadAsync → AddAAuthAgent"]
        S --> R["Runtime: SDK calls AP refresh before expiry"]
    end
Loading

See Bootstrap & Enrollment for the CLI/desktop provisioning step, or Getting Started for the self-issued path.

Agent Registration (Outbound Requests)

Pseudonymous (HWK) — Signing Only

No enrollment required. Generate or load a key and register:

var key = AAuthKey.Generate(); // or load from persistent storage

builder.Services.AddAAuthAgent("signing-only", options =>
{
    options.Key = key;
    options.UseHwk();
});

Identity-Based (JWT) — Self-Issued (Hosted Services)

No AP enrollment needed. The service generates a key and self-issues tokens:

var key = AAuthKey.Generate();
const string Kid = "svc-key-1";
var issuer = "https://my-service.example";

builder.Services.AddAAuthAgent("self-issued", options =>
{
    options.Key = key;
    options.PersonServer = "https://ps.example";
    options.TokenRefresher = SelfIssuedTokenRefresher.Create(key, issuer, "aauth:my-service@my-service.example")
        .WithKid(Kid)
        .WithPersonServer("https://ps.example")
        .Build();
});

// Also publish agent metadata so verifiers can discover the JWKS
app.MapAAuthAgentWellKnown(new AAuthAgentMetadataOptions
{
    Issuer = issuer,
    SigningKeys = new Dictionary<string, AAuthKey> { [Kid] = key },
});

Identity-Based (JWT) — AP-Enrolled (CLI/Desktop Agents)

Load the key by local handle from the store and configure token refresh:

var keyStore = FileKeyStore.Default();
var localKeyHandle = configuration["AAuth:LocalKeyHandle"]!;
var key = await keyStore.LoadAsync(localKeyHandle)
    ?? throw new InvalidOperationException($"Key '{localKeyHandle}' not found.");
var apRefreshEndpoint = configuration["AAuth:ApRefreshEndpoint"]!;

builder.Services.AddAAuthAgent("identity", options =>
{
    options.Key = key;
    options.PersonServer = "https://ps.example";
    options.TokenRefresher = AgentProviderTokenRefresher.Create(apRefreshEndpoint, localKeyHandle)
        .WithKeyStore(keyStore)
        .Build();
});

If your AP issues longer-lived tokens and you manage refresh externally, you can still configure the refresher accordingly:

builder.Services.AddAAuthAgent("identity", options =>
{
    options.Key = key;
    options.PersonServer = "https://ps.example";
    options.TokenRefresher = AgentProviderTokenRefresher.Create(apRefreshEndpoint, localKeyHandle)
        .WithKeyStore(keyStore)
        .Build();
});

With User Interaction (Deferred Consent)

When the Person Server requires user approval, provide interaction callbacks:

builder.Services.AddAAuthAgent("interactive", options =>
{
    options.Key = key;
    options.PersonServer = "https://ps.example";
    options.TokenRefresher = AgentProviderTokenRefresher.Create(apRefreshEndpoint, localKeyHandle)
        .WithKeyStore(keyStore)
        .Build();
    options.InteractionHandling = true;
    options.InteractionHandlingOptions = io =>
    {
        io.OnInteractionRequired = async (url, code, ct) =>
        {
            // Present URL and code to user
            logger.LogInformation("Approve at {Url} with code {Code}", url, code);
        };
        io.PollingTimeout = TimeSpan.FromMinutes(3);
    };
});

With Token Refresh

For long-lived agents, enable automatic token refresh before expiry:

builder.Services.AddAAuthAgent("refreshing", options =>
{
    options.Key = key;
    options.PersonServer = "https://ps.example";
    options.TokenRefresher = AgentProviderTokenRefresher.Create("https://ap.example/refresh", localKeyHandle)
        .WithKeyStore(keyStore)
        .Build();
});

Resource Registration (Inbound Verification)

Verification Middleware

builder.Services.AddAAuthResource(options =>
{
    options.Issuer = "https://my-resource.example";
    options.SigningKeys = new() { ["key-1"] = resourceKey };
});

var app = builder.Build();
app.UseAAuthVerification(); // HTTP sig + JWT issuer verification middleware
app.MapAAuthWellKnown();    // /.well-known/aauth-resource.json + /jwks.json

With Scope Descriptions (Published in Metadata)

builder.Services.AddAAuthResource(options =>
{
    options.Issuer = "https://my-resource.example";
    options.SigningKeys = new() { ["key-1"] = resourceKey };
    options.ClientName = "Supply Chain Service";
    options.ScopeDescriptions = new()
    {
        ["data:read"] = "Read supply chain data",
        ["data:write"] = "Modify supply chain records",
    };
});

Custom Authorization Endpoint

Override the authorization endpoint for advanced scenarios (e.g., custom access server):

builder.Services.AddAAuthResource(options =>
{
    options.Issuer = "https://my-resource.example";
    options.SigningKeys = new() { ["key-1"] = resourceKey };
    options.AuthorizationEndpoint = "https://as.example/authorize";
});

Authentication & Authorization Policies

To map verification results into a ClaimsPrincipal and enforce per-endpoint access, register the AAuth authentication scheme, the authorization handlers, and any named scope/role policies:

builder.Services.AddAAuthAuthentication();   // maps result → ClaimsPrincipal
builder.Services.AddAAuthAuthorization();    // scope handler + built-in policies

// Named convenience policies (apply with RequireAuthorization(...)):
builder.Services.AddAAuthScopePolicy("AAuth.Scope.data:read", "data:read");
builder.Services.AddAAuthRolePolicy("AAuth.Role.admin", "admin");
  • AddAAuthAuthorization() registers the built-in AAuth.Authenticated, AAuth.Identified, and AAuth.Authorized policies plus AAuthScopeHandler.
  • AddAAuthScopePolicy(policyName, requiredScope) registers a policy that requires an AAuthLevel.Authorized auth token carrying requiredScope — an agent-token-only (PoP) request cannot satisfy it.
  • AddAAuthRolePolicy(policyName, requiredRole) registers a policy that requires an AAuthLevel.Authorized auth token plus requiredRole (mapped from the token's roles claim to the standard ClaimTypes.Role).

See Authorization Policies for details.

Shared Discovery Services

Register shared MetadataClient and JwksClient singletons with custom cache settings:

builder.Services.AddAAuthDiscovery(options =>
{
    options.MetadataCacheTtl = TimeSpan.FromMinutes(10);
    options.JwksCacheTtl = TimeSpan.FromHours(2);
});

Both AddAAuthAgent and AddAAuthResource register their own discovery clients if AddAAuthDiscovery has not been called. Call it explicitly to share instances and control cache behavior.

Consuming Registered Clients

Via IHttpClientFactory

public class MyAgentService(IHttpClientFactory factory)
{
    private readonly HttpClient _client = factory.CreateClient("identity");

    public async Task<string> FetchDataAsync()
    {
        var response = await _client.GetAsync("https://resource.example/data");
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsStringAsync();
    }
}

Multiple Named Clients

Register different clients for different resources or signing modes:

builder.Services.AddAAuthAgent("internal-api", options =>
{
    options.Key = key;
    options.PersonServer = "https://ps.internal";
    options.TokenRefresher = internalRefresher;
});

builder.Services.AddAAuthAgent("external-api", options =>
{
    options.Key = externalKey;
    options.PersonServer = "https://ps.partner.example";
    options.TokenRefresher = externalRefresher;
});

Complete Example: Agent + Resource in One App

An app that verifies inbound AAuth requests AND makes signed outbound requests. See samples/Orchestrator for a full working implementation with call chaining.

var builder = WebApplication.CreateBuilder(args);

// Inbound: verify signatures on incoming requests
builder.Services.AddAAuthResource(options =>
{
    options.Issuer = "https://my-service.example";
    options.SigningKeys = new() { ["rs-1"] = resourceKey };
});

// Outbound: sign requests to downstream resources
builder.Services.AddAAuthAgent("downstream", options =>
{
    options.Key = agentKey;
    options.PersonServer = "https://ps.example";
    options.TokenRefresher = AgentProviderTokenRefresher.Create(apRefreshEndpoint, localKeyHandle)
        .WithKeyStore(keyStore)
        .Build();
});

var app = builder.Build();
app.UseAAuthVerification();
app.MapAAuthWellKnown();

app.MapGet("/data", async (HttpContext ctx, IHttpClientFactory factory) =>
{
    // Inbound request was verified by middleware
    var parsed = ctx.GetAAuthParsedKey()!;

    // Make signed outbound request
    var client = factory.CreateClient("downstream");
    var downstream = await client.GetStringAsync("https://other-resource.example/api");

    return Results.Ok(new { agent = parsed.Payload?["sub"]?.ToString(), downstream });
});

app.Run();

Options Reference

AAuthAgentOptions

Property Type Default Description
Key IAAuthKey required Agent signing key (must have private component)
BaseAddress Uri? null Target resource URL
SignatureKeyProvider ISignatureKeyProvider? null Custom signature key provider
PersonServer string? null PS URL; with ChallengeHandling, enables challenge flow
ChallengeHandling bool false Enable challenge handling
ChallengeHandlingOptions Action<ChallengeHandlingOptions>? null Configure challenge handling behavior
InteractionHandling bool false Enable interaction handling
InteractionHandlingOptions Action<InteractionHandlingOptions>? null Configure interaction handling behavior
TokenRefresher ITokenRefresher? null Auto-refresh before token expiry
RefreshThreshold TimeSpan? null Time before expiry to trigger refresh
Capabilities string[]? null Agent capabilities to advertise
InnerHandler HttpMessageHandler? null Custom inner HTTP handler
CallChainProvider Func<string?>? null Provider for upstream auth token (call chaining)

AAuthResourceOptions

Property Type Default Description
Issuer string required Resource HTTPS URL (metadata + audience)
SigningKeys Dictionary<string, AAuthKey> empty Keys for signing resource tokens
ClientName string? null Human-readable name in metadata
ScopeDescriptions Dictionary<string, string>? null Scope descriptions in metadata
SignatureWindow int? null Advertised signature validity (seconds)
AuthorizationEndpoint string? null AS authorization URL
RevocationEndpoint string? null Revocation endpoint URL

AAuthDiscoveryOptions

Property Type Default Description
MetadataCacheTtl TimeSpan 5 min How long to cache well-known metadata
JwksCacheTtl TimeSpan 1 hour How long to cache JWKS documents

Call Chaining (AAuthClientBuilder)

For intermediary services that act as both resource and agent, AAuthClientBuilder provides call-chaining methods:

// From HttpContext (reads UpstreamAuthTokenFeature set by middleware)
var client = new AAuthClientBuilder(key)
    .UseJwt(() => tokenHolder.Token)
    .WithTokenRefresh(refresher)
    .WithCallChaining(httpContext)
    .Build();

// From a raw upstream token string
var client = new AAuthClientBuilder(key)
    .UseJwt(() => tokenHolder.Token)
    .WithTokenRefresh(refresher)
    .WithCallChaining(upstreamAuthToken)
    .Build();

// From a dynamic provider
var client = new AAuthClientBuilder(key)
    .UseJwt(() => tokenHolder.Token)
    .WithTokenRefresh(refresher)
    .WithCallChaining(() => GetUpstreamToken())
    .Build();

WithCallChaining automatically:

  • Routes downstream exchanges to the correct PS/AS via CallChainingRouter
  • Passes upstream_token in exchange POST body
  • Inserts MissionForwardingHandler to propagate AAuth-Mission headers
  • Handles the full 401 → exchange → retry cycle