clockworklabs/SpacetimeDB · error · anyhow::Error

Issuer empty

Error message

Issuer empty

What it means

During IncomingClaims -> SpacetimeIdentityClaims conversion (crates/auth/src/identity.rs:110), an `iss` claim that deserializes to an empty string is rejected. The issuer is one of the two inputs to Identity::from_claims, so an empty issuer would collapse many tokens onto ambiguous identities. This fires when iss is present but empty, or absent and defaulted to empty.

Solutions

  1. Ensure the token service always includes a non-empty `iss` claim (short canonical issuer id) in every issued JWT.
  2. If the token is not a JWT with proper claims, use SpacetimeDB's own token flow instead of passing it through this claims path.
  3. Regenerate and redistribute tokens after fixing issuance.

Example fix

// before: payload missing issuer
{ "sub": "user-1" }
// after
{ "iss": "https://app.example.com", "sub": "user-1" }
Defensive patterns

Strategy: validation

Validate before calling

// Rust: require a non-empty issuer before attempting the conversion.
anyhow::ensure!(!claims.issuer.is_empty(), "token missing `iss` claim; refusing to derive identity");

Type guard

fn has_issuer(c: &IncomingClaims) -> bool {
    !c.issuer.is_empty()
}

Prevention

When it happens

Trigger: Authenticating with a JWT that omits the `iss` claim entirely, or carries `iss": ""`; tokens minted by custom services that only set `sub`; test/self-signed tokens generated without an issuer field.

Common situations: Hand-rolled token generation that never set iss; providers configured to strip iss; sending a raw API key or opaque token where a JWT is expected, so claims parse to defaults.

Related errors


AI-assisted analysis of clockworklabs/SpacetimeDB@3653d2ed49 (2026-08-20). Data as JSON: /api/errors/ad44e6e3e0834946. Report an issue: GitHub.

Appendix: source

Thrown at crates/auth/src/identity.rs:115

    /// All remaining claims from the JWT payload
    #[serde(flatten)]
    pub extra: Option<HashMap<Box<str>, serde_json::Value>>,
}

impl TryInto<SpacetimeIdentityClaims> for IncomingClaims {
    type Error = anyhow::Error;

    fn try_into(self) -> anyhow::Result<SpacetimeIdentityClaims> {
        // The issuer and subject must be less than 128 bytes.
        if self.issuer.len() > 128 {
            return Err(anyhow::anyhow!("Issuer too long: {:?}", self.issuer));
        }
        if self.subject.len() > 128 {
            return Err(anyhow::anyhow!("Subject too long: {:?}", self.subject));
        }
        // The issuer and subject must be non-empty.
        if self.issuer.is_empty() {
            return Err(anyhow::anyhow!("Issuer empty"));
        }
        if self.subject.is_empty() {
            return Err(anyhow::anyhow!("Subject empty"));
        }

        let computed_identity = Identity::from_claims(&self.issuer, &self.subject);
        // If an identity is provided, it must match the computed identity.
        if let Some(token_identity) = self.identity
            && token_identity != computed_identity
        {
            return Err(anyhow::anyhow!(
                    "Identity mismatch: token identity {token_identity:?} does not match computed identity {computed_identity:?}",
                ));
        }

        Ok(SpacetimeIdentityClaims {
            identity: computed_identity,
            subject: self.subject,

View on GitHub (pinned to 3653d2ed49)