{"record":{"id":"2384480283fceb24","repo":"nats-io/nats-server","slug":"auth-callout-violation-q-on-account-q","errorCode":null,"errorMessage":"auth callout violation: %q on account %q","messagePattern":"auth callout violation: %q on account %q","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/auth_callout.go","lineNumber":99,"sourceCode":"\t\txkp, xkey = s.xkp, s.info.XKey\n\t}\n\n\t// Create a keypair for the user. We will expect this public user to be in the signed response.\n\t// This prevents replay attacks.\n\tukp, _ := nkeys.CreateUser()\n\tpub, _ := ukp.PublicKey()\n\n\treply := s.newRespInbox()\n\trespCh := make(chan string, 1)\n\n\tdecodeResponse := func(rc *client, rmsg []byte, acc *Account) (*jwt.UserClaims, error) {\n\t\taccount := acc.Name\n\t\t_, msg := rc.msgParts(rmsg)\n\n\t\t// This signals not authorized.\n\t\t// Since this is an account subscription will always have \"\\r\\n\".\n\t\tif len(msg) <= LEN_CR_LF {\n\t\t\treturn nil, fmt.Errorf(\"auth callout violation: %q on account %q\", \"no reason supplied\", account)\n\t\t}\n\t\t// Strip trailing CRLF.\n\t\tmsg = msg[:len(msg)-LEN_CR_LF]\n\t\tencrypted := false\n\t\t// If we sent an encrypted request the response could be encrypted as well.\n\t\t// we are expecting the input to be `eyJ` if it is a JWT\n\t\tif xkp != nil && len(msg) > 0 && !bytes.HasPrefix(msg, []byte(jwtPrefix)) {\n\t\t\tvar err error\n\t\t\tmsg, err = xkp.Open(msg, pubAccXKey)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"error decrypting auth callout response on account %q: %v\", account, err)\n\t\t\t}\n\t\t\tencrypted = true\n\t\t}\n\n\t\tcr, err := jwt.DecodeAuthorizationResponseClaims(string(msg))\n\t\tif err != nil {\n\t\t\treturn nil, err","sourceCodeStart":81,"sourceCodeEnd":117,"githubUrl":"https://github.com/nats-io/nats-server/blob/3a66a489d262bf89b71a71c955c94920394532f3/server/auth_callout.go#L81-L117","documentation":"The auth callout (decentralized authentication) service on the account responded with an empty authorization response, i.e. a message containing nothing beyond the trailing CRLF. The server treats an empty response as a rejection with no reason and reports this violation naming 'no reason supplied' and the account.","triggerScenarios":"While processing an authorization request response (rc.msgParts on the reply message), if len(msg) <= LEN_CR_LF the library returns this error. Happens when the auth callout service publishes an empty message body to the access-control reply subject, or sends a response that is only CRLF.","commonSituations":"Auth callout service bug that replies without serializing the AuthorizationResponseClaims; service crashing and a stub/empty publish being sent; proxy or middleware stripping the payload; test harness replying with an empty ACK.","solutions":["Fix the auth callout service to publish a valid signed AuthorizationResponseClaims JWT in the response body.","Log the raw response in the callout service to find where the empty body originates.","Ensure the callout service only publishes responses to the reply subject and includes the error/ok decision field.","If intentional rejection, respond with a properly signed response carrying an error reason instead of an empty message."],"exampleFix":"// before (callout service)\nnc.Publish(msg.Reply, []byte(\"\\r\\n\"))\n// after\nresp, _ := jwt.EncodeAuthorizationResponseClaims(claims, signerKey)\nnc.Publish(msg.Reply, []byte(resp))","handlingStrategy":"validation","validationCode":"// in the auth callout service before publishing the response\nif len(respBytes) <= 2 {\n    return errors.New(\"refusing to publish empty authorization response\")\n}\nnc.Publish(msg.Reply, respBytes)","typeGuard":"func hasBody(msg []byte) bool { return len(msg) > 2 }","tryCatchPattern":"user, err := s.lookupAccountForClientRequest(...)\nif err != nil && strings.Contains(err.Error(), \"auth callout violation\") {\n    log.Printf(\"auth callout returned empty response on %s\", account)\n    return nil, ErrAuthentication\n}","preventionTips":["Always publish a signed AuthorizationResponseClaims from the callout service","Add a self-test that sends a request and verifies a non-empty response","Log raw responses in the callout service","Guard against crash-path handlers publishing empty ACKs"],"tags":["auth-callout","authentication","nats"],"backgroundTag":"auth-callout-invalid-response","analyzedSha":"3a66a489d262bf89b71a71c955c94920394532f3","analyzedAt":"2026-09-02T04:41:54.247Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T15:18:49.778Z"}