Skip to content

Latest commit

 

History

History
261 lines (209 loc) · 8.76 KB

File metadata and controls

261 lines (209 loc) · 8.76 KB

Error Handling

Error Codes

Overview

The AAuth SDK uses structured error codes at every layer: signature verification, token exchange, and consent polling. This page catalogs all error types and shows how to handle them.

Signature Errors (Resource → Agent)

When a resource rejects a signature, it returns 401 with the Signature-Error header.

SignatureErrorCode

namespace AAuth.Errors;

public enum SignatureErrorCode
{
    InvalidRequest,         // Missing required headers (Signature, Signature-Input, Signature-Key)
    InvalidInput,           // Covered components don't match the required set (see required_input)
    InvalidSignature,       // Signature bytes don't verify against key
    UnsupportedAlgorithm,   // Algorithm not supported by this resource
    InvalidKey,             // Key material is malformed or unsupported
    UnknownKey,             // Key not found (jwks_uri: kid not in JWKS)
    InvalidJwt,             // Agent token JWT fails validation
    ExpiredJwt,             // Agent token exp has passed
}

Wire Format

using AAuth.Errors;

// Formatting (server-side)
var header = SignatureError.Format(SignatureErrorCode.InvalidSignature);
// → "invalid_signature"

// With details
var header = SignatureError.Format(
    SignatureErrorCode.InvalidInput,
    requiredInput: new[] { "@method", "@authority", "@path" });
// → "invalid_input;required_input=\"@method\" \"@authority\" \"@path\""

// Parsing (agent-side)
if (SignatureError.TryParse(response.Headers["Signature-Error"], out var code))
{
    Console.WriteLine($"Signature rejected: {code}");
}

// Extract the components a resource demands on an invalid_input error
string[] required = SignatureError.ParseRequiredInput(
    response.Headers["Signature-Error"]);
// → ["content-digest"]   (empty array when no required_input is present)

Adaptive Retry on invalid_input

When challenge handling is enabled, the agent handles invalid_input with a required_input list automatically: it learns the additional covered components, re-signs the request covering them, and retries once. Learned components are cached per origin. If content-digest is among the required components, the signing handler computes it (RFC 9530, sha-256) from the request body before re-signing, so the retry succeeds without caller intervention. See Adaptive Signature Components for how to seed components proactively from resource metadata. ParseRequiredInput is exposed for callers implementing this handshake manually.

Token Errors (PS/AS → Agent)

When a Person Server or Access Server rejects a token exchange request.

TokenErrorCode

namespace AAuth.Errors;

public enum TokenErrorCode
{
    InvalidRequest,         // Malformed request body
    InvalidAgentToken,      // Agent token fails validation
    ExpiredAgentToken,      // Agent token exp has passed
    InvalidResourceToken,   // Resource token fails validation
    ExpiredResourceToken,   // Resource token exp has passed
    InteractionRequired,    // User must approve (deferred consent, non-terminal 202)
    UserUnreachable,        // No channel to the user; agent declared no interaction capability (terminal 400)
    ServerError,            // Internal server error (transient, retryable)
}

TokenErrorResponse

public sealed record TokenErrorResponse(TokenErrorCode Error, string? ErrorDescription = null)
{
    public string ErrorCode { get; }  // wire format: "invalid_request", "expired_agent_token", etc.
}

The TokenExchangeClient throws when it receives an error response from the PS.

When the PS returns a non-success status with a structured AAuth error body ({ "error": ..., "error_description": ... }), the exchange throws a typed AAuthTokenExchangeException carrying the parsed fields. Responses that are not parseable AAuth error objects fall back to a plain HttpRequestException.

public sealed class AAuthTokenExchangeException : Exception
{
    public string ErrorCode { get; }          // e.g. "invalid_resource_token"
    public string? ErrorDescription { get; }  // optional human-readable text
    public int StatusCode { get; }            // HTTP status from the token endpoint
    public bool IsTerminal { get; }           // false only for "server_error" (retryable)
}
try
{
    var authToken = await exchangeClient.ExchangeAsync(personServer, resourceToken);
}
catch (AAuthTokenExchangeException ex)
{
    Console.WriteLine($"Token exchange failed: {ex.ErrorCode} (HTTP {ex.StatusCode})");
    if (!ex.IsTerminal)
    {
        // Transient (server_error) — a later retry may succeed.
    }
}
catch (HttpRequestException ex)
{
    // Transport failure, or a non-success response without a parseable
    // AAuth error body.
    Console.WriteLine($"Transport error: {ex.Message}");
}

If you're calling the PS manually, parse the body yourself with TokenErrorResponse:

var response = await signedClient.PostAsync(psTokenEndpoint, content);
if (!response.IsSuccessStatusCode)
{
    var error = await response.Content.ReadFromJsonAsync<TokenErrorResponse>();
    Console.WriteLine($"Token exchange failed: {error?.ErrorCode}{error?.ErrorDescription}");
}

Polling Errors (Deferred Consent)

When polling a pending URL during deferred consent.

PollingErrorCode

namespace AAuth.Errors;

public enum PollingErrorCode
{
    Denied,        // User explicitly denied the request
    Abandoned,     // User navigated away / session expired
    Expired,       // Interaction timed out server-side
    InvalidCode,   // Code doesn't match any pending interaction
    SlowDown,      // Polling too fast — back off
    ServerError,   // Internal server error
}

PollingErrorException

public sealed class PollingErrorException : Exception
{
    public PollingErrorCode ErrorCode { get; }
    public int StatusCode { get; }

    // Wire format helpers
    public static string ToWireCode(PollingErrorCode code);       // e.g., "denied"
    public static bool TryParseCode(string? code, out PollingErrorCode result);
}

The DeferredPoller handles SlowDown automatically (backs off). Terminal errors (Denied, Abandoned, Expired) are thrown as PollingErrorException.

Interaction Exceptions

High-level exceptions thrown by ChallengeHandler and TokenExchangeClient:

namespace AAuth.Agent;

// User denied the request at the interaction URL
public sealed class AAuthInteractionDeniedException : Exception { }

// Polling timed out (MaxTotalWait elapsed without resolution)
public sealed class AAuthInteractionTimeoutException : Exception { }

Handling in Application Code

try
{
    var response = await client.GetAsync("https://resource.example/data");
    response.EnsureSuccessStatusCode();
}
catch (AAuthInteractionDeniedException)
{
    // User said no — show appropriate UI
    Console.WriteLine("Access denied by user.");
}
catch (AAuthInteractionTimeoutException)
{
    // Timed out waiting — offer to retry
    Console.WriteLine("Approval timed out. Try again?");
}
catch (AAuthVerificationException ex)
{
    // Signature verification failed (server-side)
    Console.WriteLine($"Verification error: {ex.Message}");
}
catch (TokenVerificationException ex)
{
    // Token validation failed
    Console.WriteLine($"Token error: {ex.Message}");
}

Exception Hierarchy

Exception Thrown By Meaning
AAuthVerificationException AAuthVerifier Signature bytes invalid
TokenVerificationException TokenVerifier JWT fails validation
AAuthTokenExchangeException TokenExchangeClient / ChallengeHandler PS token endpoint returned a structured error
AAuthInteractionDeniedException DeferredPoller / ChallengeHandler User denied
AAuthInteractionTimeoutException DeferredPoller / ChallengeHandler Polling timed out
PollingErrorException DeferredPoller PS returned terminal error during polling

Server-Side Error Emission

// In middleware or endpoint — set the Signature-Error header
context.Response.Headers[SignatureError.HeaderName] =
    SignatureError.Format(SignatureErrorCode.InvalidSignature);
context.Response.StatusCode = 401;

// Token exchange error response
return Results.Json(
    new { error = "expired_resource_token", error_description = "Resource token has expired" },
    statusCode: 400);

Further Reading