clockworklabs/SpacetimeDB · error · InvalidOperationException

Unexpected type for 'aud' claim in JWT

Error message

Unexpected type for 'aud' claim in JWT

What it means

JwtClaims.ExtractAudience (backing the lazily-evaluated Audience property) accepts an 'aud' claim that is a JSON string or an array of strings; a missing 'aud' is fine (empty list). Any other JSON value kind — number, boolean, object, or null — throws InvalidOperationException when Audience is first accessed.

Source

Thrown at crates/bindings-csharp/Runtime/JwtClaims.cs:82

    }

    private List<string> ExtractAudience()
    {
        if (!RootElement.TryGetProperty("aud", out var aud))
        {
            return [];
        }

        return aud.ValueKind switch
        {
            JsonValueKind.String => [aud.GetString()!],
            JsonValueKind.Array =>
            [
                .. aud.EnumerateArray()
                    .Where(e => e.ValueKind == JsonValueKind.String)
                    .Select(e => e.GetString()!),
            ],
            _ => throw new InvalidOperationException("Unexpected type for 'aud' claim in JWT"),
        };
    }

    public IReadOnlyList<string> Audience => _audience.Value;

    // TODO: Should this be exposed as a JsonDocument, since that it in the stdlib?
    public string RawPayload => _payload;
}

View on GitHub (pinned to 524b4487d9)

Solutions

  1. Inspect JwtClaims.RawPayload to see the actual JSON type of 'aud'
  2. Fix the issuer to emit 'aud' as a string or an array of strings (the only shapes the JWT spec allows)
  3. If you must tolerate other shapes, parse RawPayload yourself with JsonDocument instead of using Audience

Example fix

// before
var audiences = jwt.Audience; // throws for aud: 42 / null / {}

// after
using var doc = JsonDocument.Parse(jwt.RawPayload);
List<string> audiences = [];
if (doc.RootElement.TryGetProperty("aud", out var aud))
{
    if (aud.ValueKind == JsonValueKind.String) audiences.Add(aud.GetString()!);
    else if (aud.ValueKind == JsonValueKind.Array)
        audiences.AddRange(aud.EnumerateArray().Where(e => e.ValueKind == JsonValueKind.String).Select(e => e.GetString()!));
}
Defensive patterns

Strategy: validation

Validate before calling

bool HasValidAudShape(JwtClaims? jwt)
{
    if (jwt == null) return true; // missing 'aud' is fine
    using var doc = JsonDocument.Parse(jwt.RawPayload);
    if (!doc.RootElement.TryGetProperty("aud", out var aud)) return true;
    return aud.ValueKind is JsonValueKind.String or JsonValueKind.Array;
}

Type guard

static bool AudienceIsSafe(JwtClaims? jwt) => HasValidAudShape(jwt);

Try / catch

try { var aud = jwt.Audience; }
catch (InvalidOperationException e) when (e.Message.Contains("aud")) { /* non-standard 'aud' claim; parse RawPayload yourself */ }

Prevention

When it happens

Trigger: Reading jwt.Audience when the token's 'aud' claim is a number (e.g. a client_id configured as an int), a boolean, null, or a JSON object. Only triggered by the first access because the audience list is computed lazily.

Common situations: Custom auth servers that store audience as a numeric app id; mis-serialized tokens where 'aud' becomes null; issuers that emit aud as an object map of {client: scopes}.

Related errors


AI-assisted analysis of clockworklabs/SpacetimeDB@524b4487d9 (2026-08-16). Data as JSON: /api/errors/5ab1d66fba4fb5cd. Report an issue: GitHub.