clockworklabs/SpacetimeDB · error · TokenValidationError

Issuer mismatch: got

Error message

Issuer mismatch: got {:?}, expected {:?}

What it means

BasicTokenValidator validated the token's signature and claims, but the configured expected issuer does not equal the token's iss claim (byte-for-byte, both shown with Debug quoting). A cryptographically valid token is still rejected because it was minted by a different identity provider than the one this validator trusts.

Solutions

  1. Compare the two values printed in the error and make the server's configured issuer match the token's iss exactly, including any trailing slash
  2. Re-login against the correctly-configured server so a matching token is issued
  3. If you operate the OIDC provider, keep its issuer URL stable and identical on both sides

Example fix

# before
spacetimedb start --jwt-issuer https://auth.example.com   # token iss is https://auth.example.com/

# after
spacetimedb start --jwt-issuer https://auth.example.com/  # byte-exact match
Defensive patterns

Strategy: validation

Validate before calling

// Decode the iss claim without verification and compare before submitting the token
let iss = unverified_iss(token)?; // split '.', base64-decode segment 1, read "iss"
if iss != *expected_issuer {
    return Err(format!("token issuer {iss} does not match configured {expected_issuer}"));
}

Prevention

When it happens

Trigger: Self-hosted spacetimedb configured with an issuer (e.g. via --jwt-issuer or a JWKS whose issuer field) that differs from the iss claim in the presented token; using testnet/mainnet-issued tokens against a private server with its own issuer.

Common situations: Environment mismatch (token from one environment, server configured for another); OIDC provider changing its issuer URL (trailing slash added/removed); server configs copied between deployments without updating the issuer.

Related errors


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

Appendix: source

Thrown at crates/core/src/auth/token_validation.rs:196

        if SystemTime::now()
            .duration_since(exp)
            .is_ok_and(|elapsed| elapsed > Duration::from_secs(60))
        {
            return Err(JwtErrorKind::ExpiredSignature.into());
        }
    }
    Ok(())
}

#[async_trait]
impl TokenValidator for BasicTokenValidator {
    async fn validate_token(&self, token: &str) -> Result<SpacetimeIdentityClaims, TokenValidationError> {
        // This validates everything but the issuer.
        let claims = self.public_key.validate_token(token).await?;
        if let Some(expected_issuer) = &self.issuer
            && *claims.issuer != **expected_issuer
        {
            return Err(TokenValidationError::Other(anyhow::anyhow!(
                "Issuer mismatch: got {:?}, expected {:?}",
                claims.issuer,
                expected_issuer
            )));
        }
        Ok(claims)
    }
}

// Validates tokens by looking up public keys and caching them.
pub struct CachingOidcTokenValidator {
    cache: async_cache::AsyncCache<Arc<JwksValidator>, KeyFetcher>,
}

impl CachingOidcTokenValidator {
    pub fn new(refresh_duration: Duration, expiry: Option<Duration>) -> Self {
        let cache = async_cache::Options::new(refresh_duration, KeyFetcher)
            .with_expire(expiry)

View on GitHub (pinned to 3653d2ed49)