vxcontrol/pentagi · error · errUserHashMismatch

%w - token invalid for this installation

Error message

%w - token invalid for this installation

What it means

tryProtoTokenAuthentication compares apiClaims.UHASH (baked into the JWT at creation) with the current DB hash for the user. On mismatch it returns errUserHashMismatch wrapped as "token invalid for this installation" — the token was issued by a different installation/salt or the user's hash changed, so the token is bound to the wrong environment.

Source

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

	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) {
			return authResultFail, errors.New("user has been deleted")
		}
		return authResultFail, fmt.Errorf("error checking user status: %w", err)
	}

	if userStatus == models.UserStatusBlocked {
		return authResultFail, errors.New("user has been blocked")
	}

	if dbHash != apiClaims.UHASH {
		return authResultFail, fmt.Errorf("%w - token invalid for this installation", errUserHashMismatch)
	}

	// generate UUID from user hash (fallback to empty string if hash is invalid)
	uuid, err := rdb.MakeUuidStrFromHash(apiClaims.UHASH)
	if err != nil {
		// Use empty UUID for invalid hashes (e.g., in tests)
		uuid = ""
	}

	// set session fields similar to regular login
	c.Set("uid", apiClaims.UID)
	c.Set("uhash", apiClaims.UHASH)
	c.Set("rid", apiClaims.RID)
	c.Set("tid", models.UserTypeAPI.String())
	c.Set("prm", privileges)
	c.Set("gtm", time.Now().Unix())
	c.Set("exp", apiClaims.ExpiresAt.Unix())
	c.Set("uuid", uuid)

View on GitHub (pinned to ea665308ba)

Solutions

  1. Regenerate the API token on the installation you are calling so UHASH matches the current DB hash
  2. Align GLOBAL_SALT across environments that share a database (or isolate the databases)
  3. After any salt rotation, re-issue all API tokens and require users to log in again
  4. Confirm the caller is targeting the correct installation endpoint (prod vs staging URL)

Example fix

// before
curl https://staging.example.com/api -H "Authorization: Bearer $PROD_TOKEN"
// after
curl https://staging.example.com/api -H "Authorization: Bearer $STAGING_TOKEN"
Defensive patterns

Strategy: fallback

Type guard

func isInstallMismatch(err error) bool {
    return errors.Is(err, auth.ErrUserHashMismatch)
}

Try / catch

if isInstallMismatch(err) {
    // token bound to another installation: re-issue on the target environment
    return reissueTokenOnTarget()
}

Prevention

When it happens

Trigger: API token created under one GLOBAL_SALT used against a server with a different salt; DB copied between environments while tokens/cookies persist; user hash regenerated (e.g., password change flows that rotate the hash) invalidating previously issued tokens.

Common situations: Same Postgres shared by prod and staging with different salts; migrating installations without re-issuing API tokens; rotating GLOBAL_SALT without invalidating/re-issuing all API tokens.

Understand the failure class

Related errors


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