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
- 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
- Re-login against the correctly-configured server so a matching token is issued
- 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
- Keep issuer configuration identical across environments; template it from one source of truth
- Watch for trailing-slash differences in issuer URLs when providers normalize them
- After any provider migration, re-issue tokens rather than reusing old ones
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
- No kid found
- Error fetching public key for issuer
- Invalid JSON: failed to parse string
- Invalid OIDC URL scheme
- Issuer empty
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)