{"record":{"id":"4bff633dcc3d31e1","repo":"fatedier/frp","slug":"token-in-heartbeat-doesn-t-match-token-from-config","errorCode":null,"errorMessage":"token in heartbeat doesn't match token from configuration","messagePattern":"token in heartbeat doesn't match token from configuration","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/auth/token.go","lineNumber":77,"sourceCode":"\tnewWorkConnMsg.Timestamp = time.Now().Unix()\n\tnewWorkConnMsg.PrivilegeKey = util.GetAuthKey(auth.token, newWorkConnMsg.Timestamp)\n\treturn nil\n}\n\nfunc (auth *TokenAuthSetterVerifier) VerifyLogin(m *msg.Login) error {\n\tif !util.ConstantTimeEqString(util.GetAuthKey(auth.token, m.Timestamp), m.PrivilegeKey) {\n\t\treturn fmt.Errorf(\"token in login doesn't match token from configuration\")\n\t}\n\treturn nil\n}\n\nfunc (auth *TokenAuthSetterVerifier) VerifyPing(m *msg.Ping) error {\n\tif !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) {\n\t\treturn nil\n\t}\n\n\tif !util.ConstantTimeEqString(util.GetAuthKey(auth.token, m.Timestamp), m.PrivilegeKey) {\n\t\treturn fmt.Errorf(\"token in heartbeat doesn't match token from configuration\")\n\t}\n\treturn nil\n}\n\nfunc (auth *TokenAuthSetterVerifier) VerifyNewWorkConn(m *msg.NewWorkConn) error {\n\tif !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) {\n\t\treturn nil\n\t}\n\n\tif !util.ConstantTimeEqString(util.GetAuthKey(auth.token, m.Timestamp), m.PrivilegeKey) {\n\t\treturn fmt.Errorf(\"token in NewWorkConn doesn't match token from configuration\")\n\t}\n\treturn nil\n}\n","sourceCodeStart":59,"sourceCodeEnd":92,"githubUrl":"https://github.com/fatedier/frp/blob/6c8a8d0a97d03b44e9528d30b30c70cb9d61b405/pkg/auth/token.go#L59-L92","documentation":"Identical static-token check to error 147 but applied to Ping messages, and only when 'HeartBeats' is included in the additional auth scopes (authentication.additionalAuthScopes). frps recomputes md5(token + ping.Timestamp) and compares with pingMsg.PrivilegeKey in constant time; mismatch rejects the heartbeat.","triggerScenarios":"HeartBeats scope enabled on frps and the ping PrivilegeKey was computed with a different token than frps's authentication.token — typically the Login succeeded (tokens matched then) but a second frpc process, or an edited config, sends pings with another token; or a custom client forgot to authenticate pings when the scope is enabled.","commonSituations":"Enabling additionalAuthScopes=[\"HeartBeats\"] on frps while a custom/SDK client only authenticates Login; token rotated on frpc but frps not restarted (or vice versa) mid-session.","solutions":["Keep authentication.token identical on both sides and include HeartBeats in additionalAuthScopes on both","If writing a custom client, set Ping.PrivilegeKey = md5(token || Ping.Timestamp) whenever the scope is enabled","Restart both frpc and frps after any token change so no live session mixes old and new keys"],"exampleFix":"# before — scopes enabled only on frps\n# frps.toml\nauthentication.additionalAuthScopes = [\"HeartBeats\"]\n\n# after — mirror on frpc\n# frpc.toml\nauthentication.additionalAuthScopes = [\"HeartBeats\"]","handlingStrategy":"validation","validationCode":"expected := md5hex(authToken + strconv.FormatInt(pingMsg.Timestamp, 10))\nif expected != pingMsg.PrivilegeKey {\n    return errors.New(\"heartbeat auth failed; check token and additionalAuthScopes parity\")\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Set additionalAuthScopes identically on frps and frpc","Sign every message type you enable in the scopes when writing custom clients","Restart both peers after token rotation"],"tags":["frp","authentication","token","heartbeat","config-mismatch"],"backgroundTag":null,"analyzedSha":"6c8a8d0a97d03b44e9528d30b30c70cb9d61b405","analyzedAt":"2026-08-15T06:53:27.215Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}