Speculative SCRAM authentication omits skipEmptyExchange, causing an extra round-trip per connection

    • Type: Bug
    • Resolution: Unresolved
    • Priority: Minor - P4
    • None
    • Affects Version/s: None
    • Component/s: Security
    • None
    • ALL
    • None
    • None
    • None
    • None
    • None
    • None
    • None

      Summary

      When mongos (or any internal client) opens a connection to mongod using speculative SCRAM-SHA-256 authentication, it omits the options field (containing skipEmptyExchange: true) from the saslStart payload embedded in the hello handshake. This forces a third SASL round-trip (an empty saslContinue exchange) that serves no purpose beyond satisfying the SCRAM RFC formality. Drivers correctly include this option and complete authentication in two steps.

      Steps to Reproduce

      Start a sharded cluster with keyfile authentication, enable command verbosity on the shard mongod, and examine the Successfully authenticated log entries (id 5286306) from mongos connections. Compare with a direct mongosh connection.

      mlaunch init --sharded 1 --replicaset --nodes 1 --auth --dir /tmp/test
      mongosh --port 27018 -u user -p password --authenticationDatabase admin --eval \
        'db.adminCommand({setParameter: 1, logComponentVerbosity: {command: {verbosity: 3}, accessControl: {verbosity: 3}}})'
      # restart mongos, then grep the mongod log for id 5286306
      

      Observed Behavior

      Every mongos-to-mongod connection authenticates in 3 steps (step_total: 3):

      • Step 1 (in hello): speculativeAuthenticate carries a saslStart with mechanism: "SCRAM-SHA-256", payload, and db: "local" — note: no options field
      • Step 2: saslContinue (client-final message) — server responds with done: false
      • Step 3: saslContinue (empty payload) — server responds with done: trueunnecessary extra round-trip

      Mongod logs confirm this with authentication metrics in the Successfully authenticated entry:

      "metrics": {
        "conversation_duration": {
          "summary": {
            "0": {"step": 1, "step_total": 3, "duration_micros": 842},
            "1": {"step": 2, "step_total": 3, "duration_micros":   7},
            "2": {"step": 3, "step_total": 3, "duration_micros":   0}
          }
        }
      }
      

      Step 3 takes 0 µs of CPU — it is a pure network round-trip with no computational work.

      Expected Behavior

      Authentication should complete in 2 steps (step_total: 2), as it does for mongosh and all driver connections that send skipEmptyExchange: true in the saslStart options:

      • Step 1 (in hello): speculativeAuthenticate includes a saslStart with options containing skipEmptyExchange: true
      • Step 2: saslContinue (client-final) — server responds with done: true

      mongosh connecting directly to mongod shows this in the log:

      "metrics": {
        "conversation_duration": {
          "summary": {
            "0": {"step": 1, "step_total": 2, "duration_micros": 2209},
            "1": {"step": 2, "step_total": 2, "duration_micros":    5}
          }
        }
      }
      

      Root Cause

      _speculateSaslStart() in src/mongo/client/authenticate.cpp builds the speculative saslStart body without the options field:

      BSONObjBuilder saslStart;
      saslStart.append("saslStart", 1);
      saslStart.append("mechanism", mechanism);
      saslStart.appendBinData("payload", int(payload.size()), BinDataGeneral, payload.c_str());
      saslStart.append("db", authDB);
      // Missing: saslStart.append("options", BSON(saslCommandOptionSkipEmptyExchange << true));
      helloRequestBuilder->append(kSpeculativeAuthenticate, saslStart.obj());
      

      The non-speculative explicit saslStart path in src/mongo/client/sasl_client_authenticate_impl.cpp correctly includes it:

      BSONObj saslFirstCommandPrefix =
          BSON(saslStartCommandName << 1 << saslCommandMechanismFieldName << mechanismName
                                    << "options" << BSON(saslCommandOptionSkipEmptyExchange << true));
      

      Without options, the server's SaslSCRAMServerMechanism::setOptions() is never called, _skipEmptyExchange stays false, and _totalSteps() returns 3 instead of 2 (src/mongo/db/auth/sasl_scram_server_conversation.h).

      Fix

      Add one line to _speculateSaslStart() in src/mongo/client/authenticate.cpp:

      saslStart.append("options", BSON(saslCommandOptionSkipEmptyExchange << true));
      

      Impact

      This affects every mongos-to-mongod connection (and mongod-to-mongod replica set connections) using SCRAM keyfile authentication — which is the default internal auth mechanism. Each new connection in the pool pays one unnecessary network round-trip during the handshake.

            Assignee:
            Unassigned
            Reporter:
            Jeffrey Yemin
            Votes:
            0 Vote for this issue
            Watchers:
            1 Start watching this issue

              Created:
              Updated: