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
- Inspect the raw token payload via JwtClaims.RawPayload (it is the raw JSON string) to see exactly which claims the token carries
- Fix the token issuer to include a string 'sub' claim (e.g. "sub": "user-123")
- Use tokens from an OIDC-compliant provider or the SpacetimeDB auth stack so 'sub' is always a string
- 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
- Check ctx.Auth.HasJwt and validate the payload shape before reading claim properties
- Only accept tokens from issuers guaranteed to emit a string 'sub'
- Log RawPayload (not the raw token) when claim errors occur to speed up diagnosis
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
- JWT missing or invalid 'iss' claim
- Unexpected type for 'aud' claim in JWT
- Failed to verify token: ${response.statusText}
- Issuer too long: {:?}
- Subject too long: {:?}
AI-assisted analysis of clockworklabs/SpacetimeDB@524b4487d9 (2026-08-16).
Data as JSON: /api/errors/fd5b4b1d8a2aee7f.
Report an issue: GitHub.