clockworklabs/SpacetimeDB · error · TokenValidationError::Other

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

Error message

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

What it means

BasicTokenValidator validated the JWT's signature and standard claims, but the iss (issuer) claim does not equal the issuer the server was configured to expect. Signature validity alone is insufficient: the token must originate from the configured identity provider or deployment issuer.

Source

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

        // TODO: We should require a specific audience at some point.
        validation.validate_aud = false;

        let data = decode::<IncomingClaims>(token, self, &validation)?;
        let claims = data.claims;
        claims.try_into().map_err(TokenValidationError::Other)
    }
}

#[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 524b4487d9)

Solutions

  1. Re-login against the correct server to obtain a token with the expected issuer.
  2. Decode the token payload (e.g. jwt.io or `spacetime identity decode`) and compare its iss claim with the issuer the server expects.
  3. If you operate the server, align the configured issuer with the identity provider's actual issuer URL.
  4. Do not reuse tokens across deployments that have different issuers.
Defensive patterns

Strategy: validation

Validate before calling

function assertIssuerMatches(token: string, expectedIssuer: string): void {
  const iss = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString()).iss;
  if (iss !== expectedIssuer) {
    throw new Error(`token issuer '${iss}' does not match server issuer '${expectedIssuer}' — re-login`);
  }
}

Try / catch

try {
  await callApi(token);
} catch (e) {
  if (String(e).includes('Issuer mismatch')) {
    token = await freshLogin(server); // mint token from the right deployment
    return callApi(token);
  }
  throw e;
}

Prevention

When it happens

Trigger: Sending a token minted by a different SpacetimeDB deployment or OIDC provider than the one configured (e.g. a testnet token against a self-hosted node); the server's expected issuer configuration changed after the token was issued; enterprise OIDC where the iss URL differs between environments.

Common situations: Reusing SPACETIMEDB_SPACETIME_TOKEN across deployments; server config changed the expected issuer; the identity provider migrated its issuer URL after tokens were minted.

Related errors


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