nats-io/nats-server · error

error decrypting auth callout response on account %q: %v

Error message

error decrypting auth callout response on account %q: %v

What it means

The server received an auth callout response that should be encrypted (because the request was encrypted with the account's xkey) but failed to decrypt it using the xkey key pair. The raw encrypted payload could not be opened, so the authorization decision cannot be read.

Source

Thrown at server/auth_callout.go:110

	decodeResponse := func(rc *client, rmsg []byte, acc *Account) (*jwt.UserClaims, error) {
		account := acc.Name
		_, msg := rc.msgParts(rmsg)

		// This signals not authorized.
		// Since this is an account subscription will always have "\r\n".
		if len(msg) <= LEN_CR_LF {
			return nil, fmt.Errorf("auth callout violation: %q on account %q", "no reason supplied", account)
		}
		// Strip trailing CRLF.
		msg = msg[:len(msg)-LEN_CR_LF]
		encrypted := false
		// If we sent an encrypted request the response could be encrypted as well.
		// we are expecting the input to be `eyJ` if it is a JWT
		if xkp != nil && len(msg) > 0 && !bytes.HasPrefix(msg, []byte(jwtPrefix)) {
			var err error
			msg, err = xkp.Open(msg, pubAccXKey)
			if err != nil {
				return nil, fmt.Errorf("error decrypting auth callout response on account %q: %v", account, err)
			}
			encrypted = true
		}

		cr, err := jwt.DecodeAuthorizationResponseClaims(string(msg))
		if err != nil {
			return nil, err
		}
		vr := jwt.CreateValidationResults()
		cr.Validate(vr)
		if len(vr.Issues) > 0 {
			return nil, fmt.Errorf("authorization response had validation errors: %v", vr.Issues[0])
		}

		// the subject is the user id
		if cr.Subject != pub {
			return nil, errors.New("auth callout violation: auth callout response is not for expected user")
		}

View on GitHub (pinned to 3a66a489d2)

Solutions

  1. Ensure the callout service encrypts responses with the account's current public xkey published on the account claims.
  2. Rotate keys coherently: after changing the account xkey, restart/redeploy the callout service with the new key.
  3. Log the crypto error detail in the wrapped %v to identify mismatched-key vs corrupted-data cases.
  4. Verify the response is not raw plaintext; it must be encrypted or a JWT starting with 'eyJ'.

Example fix

// before (callout service)
nc.Publish(msg.Reply, []byte("denied: bad token"))
// after
sealed, _ := xkey.Seal([]byte(respJWT), accPubXKey, svcXKey)
nc.Publish(msg.Reply, sealed)
Defensive patterns

Strategy: try-catch

Validate before calling

// in the auth callout service: confirm the account's public xkey before sealing
accJWT, _ := fetchAccountJWT(accPubKey)
accClaims, _ := jwt.DecodeAccountClaims(accJWT)
if len(accClaims.EncryptionKeys) == 0 { return errors.New("no xkey on account; do not encrypt") }

Type guard

func hasXKey(ac *jwt.AccountClaims) bool { return len(ac.EncryptionKeys) > 0 }

Try / catch

user, err := s.lookupAccountForClientRequest(...)
if err != nil && strings.Contains(err.Error(), "error decrypting auth callout response") {
    log.Printf("callout response decryption failed: %v", err)
    return nil, ErrAuthentication
}

Prevention

When it happens

Trigger: During response handling, when the request was sent with xkp != nil and the response does not start with the JWT prefix 'eyJ', the library attempts xkp.Open(msg, pubAccXKey). Any decryption failure (wrong key, corrupted payload, non-XKey data) returns this error wrapping the crypto failure and the account name.

Common situations: Auth callout service encrypting the response with the wrong public xkey (rotated or mismatched keys), sending plaintext that doesn't begin with 'eyJ', payload truncated by middleware, or key rotation on the account without restarting the callout service.

Related errors


AI-assisted analysis of nats-io/nats-server@3a66a489d2 (2026-09-02). Data as JSON: /api/errors/c00e02996bfb2b3b. Report an issue: GitHub.