clockworklabs/SpacetimeDB · error · InvalidOperationException

JWT missing or invalid 'sub' claim

Error message

JWT missing or invalid 'sub' claim

What it means

Thrown by the Subject accessor of JwtClaims, which wraps the decoded JWT payload of the token a connected client presented. In the C# server-module bindings the payload is fetched over FFI (AuthCtx.FromConnectionId -> FFI.get_jwt) and parsed lazily, so the error fires on first access of the property. The library assumes standards-compliant tokens where 'sub' is a string and fails fast rather than returning null.

Source

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

    }

    private JsonDocument Parsed => _parsed.Value;

    private JsonElement RootElement => Parsed.RootElement;

    public string Subject
    {
        get
        {
            if (
                RootElement.TryGetProperty("sub", out var sub)
                && sub.ValueKind == JsonValueKind.String
            )
            {
                return sub.GetString()!;
            }

            throw new InvalidOperationException("JWT missing or invalid 'sub' claim");
        }
    }

    public string Issuer
    {
        get
        {
            if (
                RootElement.TryGetProperty("iss", out var iss)
                && iss.ValueKind == JsonValueKind.String
            )
            {
                return iss.GetString()!;
            }

            throw new InvalidOperationException("JWT missing or invalid 'iss' claim");
        }
    }

View on GitHub (pinned to 524b4487d9)

Solutions

  1. Inspect the raw token payload via JwtClaims.RawPayload (it is the raw JSON string) to see exactly which claims the token carries
  2. Fix the token issuer to include a string 'sub' claim (e.g. "sub": "user-123")
  3. Use tokens from an OIDC-compliant provider or the SpacetimeDB auth stack so 'sub' is always a string
  4. If 'sub' is legitimately optional for your module, read it defensively with JsonDocument.Parse on RawPayload instead of the Subject property

Example fix

// before
var senderId = ctx.Auth.Jwt!.Subject; // throws if 'sub' missing/non-string

// after
var jwt = ctx.Auth.Jwt;
var senderId = jwt != null && TryGetClaim(jwt.RawPayload, "sub", out var s) ? s : "<anonymous>";
Defensive patterns

Strategy: validation

Validate before calling

bool HasStringSub(JwtClaims? jwt)
{
    if (jwt == null) return false;
    using var doc = JsonDocument.Parse(jwt.RawPayload);
    return doc.RootElement.TryGetProperty("sub", out var s) && s.ValueKind == JsonValueKind.String;
}

Type guard

static bool HasSubject(JwtClaims? jwt) => jwt != null && HasStringSub(jwt);

Try / catch

try { var sub = jwt.Subject; }
catch (InvalidOperationException) { /* token lacks a string 'sub'; fall back to default identity handling */ }

Prevention

When it happens

Trigger: Reading authCtx.Jwt!.Subject (e.g. inspecting the sender identity in a reducer) when the client's JWT payload has no 'sub' claim, or 'sub' is not a JSON string (null, number, object, array).

Common situations: Custom or non-OIDC token issuers that omit standard claims; test tokens generated with default/empty payloads on jwt.io-style tools; accidentally treating the JWT header segment as the payload; tokens from an older or non-standard auth flow.

Related errors


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