{"record":{"id":"6169987221a8cf62","repo":"vxcontrol/pentagi","slug":"w-token-invalid-for-this-installation","errorCode":null,"errorMessage":"%w - token invalid for this installation","messagePattern":"%w - token invalid for this installation","errorType":"http","errorClass":"errUserHashMismatch","httpStatus":401,"severity":"error","filePath":"backend/pkg/server/auth/auth_middleware.go","lineNumber":247,"sourceCode":"\tif status != models.TokenStatusActive {\n\t\treturn authResultFail, errors.New(\"token has been revoked\")\n\t}\n\n\t// Verify user hash matches database\n\tdbHash, userStatus, err := p.userCache.GetUserHash(apiClaims.UID)\n\tif err != nil {\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\treturn authResultFail, errors.New(\"user has been deleted\")\n\t\t}\n\t\treturn authResultFail, fmt.Errorf(\"error checking user status: %w\", err)\n\t}\n\n\tif userStatus == models.UserStatusBlocked {\n\t\treturn authResultFail, errors.New(\"user has been blocked\")\n\t}\n\n\tif dbHash != apiClaims.UHASH {\n\t\treturn authResultFail, fmt.Errorf(\"%w - token invalid for this installation\", errUserHashMismatch)\n\t}\n\n\t// generate UUID from user hash (fallback to empty string if hash is invalid)\n\tuuid, err := rdb.MakeUuidStrFromHash(apiClaims.UHASH)\n\tif err != nil {\n\t\t// Use empty UUID for invalid hashes (e.g., in tests)\n\t\tuuid = \"\"\n\t}\n\n\t// set session fields similar to regular login\n\tc.Set(\"uid\", apiClaims.UID)\n\tc.Set(\"uhash\", apiClaims.UHASH)\n\tc.Set(\"rid\", apiClaims.RID)\n\tc.Set(\"tid\", models.UserTypeAPI.String())\n\tc.Set(\"prm\", privileges)\n\tc.Set(\"gtm\", time.Now().Unix())\n\tc.Set(\"exp\", apiClaims.ExpiresAt.Unix())\n\tc.Set(\"uuid\", uuid)","sourceCodeStart":229,"sourceCodeEnd":265,"githubUrl":"https://github.com/vxcontrol/pentagi/blob/ea665308baaff015b226f308438a68d929d0f29b/backend/pkg/server/auth/auth_middleware.go#L229-L265","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Regenerate the API token on the installation you are calling so UHASH matches the current DB hash","Align GLOBAL_SALT across environments that share a database (or isolate the databases)","After any salt rotation, re-issue all API tokens and require users to log in again","Confirm the caller is targeting the correct installation endpoint (prod vs staging URL)"],"exampleFix":"// before\ncurl https://staging.example.com/api -H \"Authorization: Bearer $PROD_TOKEN\"\n// after\ncurl https://staging.example.com/api -H \"Authorization: Bearer $STAGING_TOKEN\"","handlingStrategy":"fallback","validationCode":null,"typeGuard":"func isInstallMismatch(err error) bool {\n    return errors.Is(err, auth.ErrUserHashMismatch)\n}","tryCatchPattern":"if isInstallMismatch(err) {\n    // token bound to another installation: re-issue on the target environment\n    return reissueTokenOnTarget()\n}","preventionTips":["Keep one salt per environment and never share databases across different salts","Re-issue all API tokens after GLOBAL_SALT rotation","Label tokens with their issuing environment and verify the target URL matches","Treat UHASH mismatch as a configuration drift signal and alert on it"],"tags":["authentication","api-token","hash-mismatch"],"backgroundTag":"user-hash-mismatch","analyzedSha":"ea665308baaff015b226f308438a68d929d0f29b","analyzedAt":"2026-09-01T14:16:31.421Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}