vxcontrol/pentagi · error

token is invalid

Error message

token is invalid

What it means

ValidateAPIToken failed to verify the bearer token's JWT signature, claims, or expiry using the server's global salt, so the middleware returns authResultFail with this generic invalid-token error. The token is syntactically present but cryptographically or structurally unacceptable.

Source

Thrown at backend/pkg/server/auth/auth_middleware.go:218

	}

	if !strings.HasPrefix(authHeader, "Bearer ") {
		return authResultSkip, errors.New("bearer scheme must be used")
	}
	token := authHeader[7:]
	if token == "" {
		return authResultSkip, errors.New("token can't be empty")
	}

	// skip validation if using default salt (for backward compatibility)
	if p.globalSalt == "" || p.globalSalt == "salt" {
		return authResultSkip, errors.New("token validation disabled with default salt")
	}

	// try to validate as API token first (new format with JWT signing key)
	apiClaims, apiErr := ValidateAPIToken(token, p.globalSalt)
	if apiErr != nil {
		return authResultFail, errors.New("token is invalid")
	}

	// check token status and get privileges through cache
	status, privileges, err := p.tokenCache.GetStatus(apiClaims.TokenID)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return authResultFail, errors.New("token not found in database")
		}
		return authResultFail, fmt.Errorf("error checking token status: %w", err)
	}
	if status != models.TokenStatusActive {
		return authResultFail, errors.New("token has been revoked")
	}

	// Verify user hash matches database
	dbHash, userStatus, err := p.userCache.GetUserHash(apiClaims.UID)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {

View on GitHub (pinned to ea665308ba)

Solutions

  1. Generate a fresh API token from the settings UI and update the client.
  2. Check the token was copied exactly — no quotes, whitespace, or line breaks.
  3. If the salt was rotated, reissue all tokens; old ones are permanently invalid.
  4. Confirm server time is correct (NTP) if tokens appear to expire early.

Example fix

// before
const token = JSON.parse(fs.readFileSync("token.json")); // object, not string

// after
const token = fs.readFileSync("token.txt", "utf8").trim();
Defensive patterns

Strategy: retry

Validate before calling

const token = loadToken();
if (!/^[A-Za-z0-9\-_=.]+$/.test(token)) {
  throw new Error("API token is malformed (check for quotes/whitespace/truncation)");
}

Type guard

function looksLikeJwt(t: string): boolean {
  const p = t.split(".");
  return p.length === 3 && p.every((s) => s.length > 0);
}

Try / catch

try {
  return await call(token);
} catch (e) {
  if (is401(e) && /token is invalid/i.test(e.message)) {
    const fresh = await mintNewToken(); // salt may have rotated; old tokens are dead
    return await call(fresh);
  }
  throw e;
}

Prevention

When it happens

Trigger: Sending an expired API token; a token signed with a different/old salt (salt rotated after token issuance); a corrupted or truncated token string; a cookie/session JWT or token from another installation passed as an API token.

Common situations: Salt changed in config after tokens were issued; copying a token with surrounding quotes or whitespace; old tokens surviving a server migration; clock skew making an unexpired-on-client token expired-on-server.

Understand the failure class

Related errors


AI-assisted analysis of vxcontrol/pentagi@ea665308ba (2026-09-01). Data as JSON: /api/errors/d39702389373d61e. Report an issue: GitHub.