Skip to content

Instantly share code, notes, and snippets.

@synopse
Last active November 4, 2025 09:35
Show Gist options
  • Select an option

  • Save synopse/f2612ac2f3dd6798c8883a562df05934 to your computer and use it in GitHub Desktop.

Select an option

Save synopse/f2612ac2f3dd6798c8883a562df05934 to your computer and use it in GitHub Desktop.
Best Pattern for Authentication in MongoDB Clients (Version 8.1+)

Differences Between SCRAM-SHA-1 and SCRAM-SHA-256 in MongoDB

SCRAM (Salted Challenge Response Authentication Mechanism) is MongoDB's default authentication method, based on IETF RFC 5802 (for SHA-1) and RFC 7677 (for SHA-256). Both variants follow a similar challenge-response flow to avoid sending plaintext passwords over the wire, but they differ in security, hashing, and MongoDB-specific tweaks. SCRAM-SHA-256 became the default in MongoDB 4.0+ due to SHA-1's deprecation and known weaknesses (e.g., collision vulnerabilities).

Here's a comparison:

Aspect SCRAM-SHA-1 SCRAM-SHA-256
Hashing Algorithm SHA-1 (160-bit output) SHA-256 (256-bit output) – stronger collision resistance and future-proof.
Security Level Adequate but deprecated; vulnerable to certain attacks (e.g., length extension in HMAC). Higher security; recommended for new deployments. Protects against replay attacks via stronger hashing.
MongoDB-Specific Password Preprocessing Uses an extra MD5 hash: passphrase = MD5(username + ":mongo:" + password). This is then salted/iterated with SHA-1. (Non-standard; not in RFC 5802.) No MD5 step; uses raw normalized password directly, salted/iterated with SHA-256. (Follows RFC 7677.)
Iteration Count Configurable via scramIterationCount (default: 10,000). Configurable via scramSHA256IterationCount (default: 15,000; higher for added work factor). Server sends the value in its challenge.
Password Digestion for Storage Flexible: Client or server can compute/store the salted password hash (Hi) during user creation/update. Server-side only: Client sends plaintext password (over TLS) to server for digestion/storage. Clients cannot pre-digest for SHA-256 users.
Stored Credential storedKey and serverKey derived from SHA-1-based Hi. storedKey and serverKey derived from SHA-256-based Hi.
Protocol Flow 3-message exchange (ClientFirst, ServerFirst, ClientFinal, ServerFinal). Bi-directional proof. Identical flow, but with SHA-256 in all HMAC/H computations.
Compatibility Supported in MongoDB 3.0+; fallback for older setups. Default in 4.0+; requires compatible drivers (e.g., most modern MongoDB drivers auto-negotiate).
Performance Faster (weaker hash). Slightly slower due to stronger hash and higher default iterations.

Key Notes:

  • Both use per-user random salts and tunable iterations to resist brute-force attacks.
  • Authentication requires TLS (e.g., STARTTLS or direct TLS) to protect the exchange.
  • The stored credentials (in system.users) include the salt, iterations, storedKey (H(ClientKey)), and serverKey (HMAC(SaltedPassword, "Server Key")) – never the plaintext password.
  • MongoDB drivers (e.g., in C#, Java, Python) typically auto-select the mechanism based on server capabilities and user config.

Extending Your SCRAM-SHA-1 Implementation to Support SCRAM-SHA-256

Since your SCRAM-SHA-1 implementation is already working, extending it to SHA-256 is straightforward: the protocol structure (message formats, proofs, signatures) is identical, per the RFCs. The changes are isolated to the hashing functions and password preprocessing. Assume your implementation handles the standard SCRAM flow:

  1. Client sends ClientFirst: n,,n=username,r=client-nonce (mechanism name differs: "SCRAM-SHA-256").
  2. Server sends ServerFirst: r=server-nonce,s=salt,i=iterations.
  3. Client computes proofs: Using password, salt, iterations → SaltedPassword → ClientKey → StoredKey → ClientSignature → ClientProof. Sends ClientFinal: c=biws,r=...,p=client-proof.
  4. Server verifies and sends ServerFinal: v=server-proof.

To add SHA-256 support, parameterize your code by mechanism (e.g., via a config flag or auto-detection from server capabilities). Here's how to adapt it step-by-step (pseudocode; adapt to your language/library, e.g., using crypto libs like OpenSSL, Java's MessageDigest, or Python's hmac/hashlib):

Step 1: Parameterize the Hash Function

  • Replace SHA-1 with SHA-256 everywhere it's used.
  • Key functions to swap:
    • H(data): SHA-1 → SHA-256 digest.
    • HMAC(key, data): HMAC-SHA-1 → HMAC-SHA-256.

Pseudocode:

import hmac
import hashlib

def get_hash_func(mechanism):
    if mechanism == "SCRAM-SHA-1":
        return hashlib.sha1
    elif mechanism == "SCRAM-SHA-256":
        return hashlib.sha256
    else:
        raise ValueError("Unsupported mechanism")

def H(data, mechanism):
    hf = get_hash_func(mechanism)
    return hf(data).digest()

def HMAC(key, data, mechanism):
    hf = get_hash_func(mechanism)
    return hmac.new(key, data, hf).digest()

Step 2: Adjust Password Preprocessing (Remove MD5 for SHA-256)

  • For SHA-1: Compute preprocessed_password = MD5(normalized_username + b":mongo:" + normalized_password).
  • For SHA-256: Use preprocessed_password = normalized_password directly (no MD5).
  • Normalization: Apply SASLprep (RFC 4013) to username and password (map non-ASCII, trim, etc.). Most crypto libs have helpers; skip if your SHA-1 impl already does it.

Pseudocode:

from hashlib import md5  # Only for SHA-1

def normalize(s):
    # Implement SASLprep: lowercase, map specials, etc. (use a lib if available)
    return s.lower().encode('utf-8')  # Simplified

def preprocess_password(username, password, mechanism):
    norm_user = normalize(username)
    norm_pass = normalize(password)
    if mechanism == "SCRAM-SHA-1":
        # MongoDB-specific MD5 step
        input_str = norm_user + b":mongo:" + norm_pass
        return md5(input_str).digest()
    elif mechanism == "SCRAM-SHA-256":
        # Standard: no MD5
        return norm_pass
    else:
        raise ValueError("Unsupported")

Step 3: Update SaltedPassword Computation

  • This is the core derivation: SaltedPassword = Hi(preprocessed_password, salt, iterations).
  • Hi is PBKDF2-like: U1 = HMAC(preprocessed, salt), U2 = HMAC(U1, salt), ..., UN = HMAC(U_{N-1}, salt); then XOR them.
  • Your SHA-1 impl likely has this loop – just plug in the new HMAC/H.

Pseudocode (add to your existing Hi function):

def Hi(password, salt, iterations, mechanism):
    u = HMAC(password, b"Client Key" + H(b"SCRAM-SHA-256" if mechanism == "SCRAM-SHA-256" else b"SCRAM-SHA-1", mechanism), mechanism)  # SaltedPassword = Hi(password, salt, i)
    # Wait, no: SaltedPassword = Hi(password, salt, i) where Hi is the iterated HMAC:
    result = b'\x00' * len(H(b'', mechanism))  # Init XOR result
    u = HMAC(password, salt + b'\x01', mechanism)  # U1 = HMAC(password, salt || int(1))
    result = bytes(a ^ b for a, b in zip(result, u))
    for _ in range(1, iterations):
        u = HMAC(password, u, mechanism)
        result = bytes(a ^ b for a, b in zip(result, u))
    return result

# Then:
salted_password = Hi(preprocessed_password, salt, iterations, mechanism)

Correction on Hi: The standard SCRAM Hi(P, S, i) is:

  • U0 = empty
  • U1 = HMAC(P, S + \x00\x00\x00\x01)
  • U2 = HMAC(P, U1)
  • ...
  • Ui = HMAC(P, U_{i-1})
  • SaltedPassword = U1 XOR U2 XOR ... XOR Ui

Yes, that's it. Your SHA-1 code likely has this; just swap HMAC.

Step 4: Compute Keys and Proofs (Minimal Changes)

  • ClientKey = HMAC(salted_password, "Client Key")
  • StoredKey = H(ClientKey, mechanism)
  • ClientSignature = H( AuthMessage + "," + ClientProof ) (AuthMessage is concatenated prior messages)
  • ClientProof = ClientKey XOR ClientSignature
  • Server proof similarly.
  • Use SHA-256 for the channel-binding flag if needed (but usually c=n,, for no binding).

Step 5: Handle Mechanism Negotiation

  • In MongoDB, query isMaster or use driver auto-negotiation to pick "SCRAM-SHA-256" if supported.
  • Send the mechanism name in the SASL init message.
  • Use the server's i value (iterations) from ServerFirst.

Step 6: Testing and Edge Cases

  • Test against a MongoDB 4.0+ instance with a SHA-256 user (create via db.createUser({user: "test", pwd: "pass", mechanisms: ["SCRAM-SHA-256"]})).
  • Ensure TLS is used (password/salt exchanged).
  • Verify iteration counts match server's.
  • If your SHA-1 impl handles client-digested storage, disable it for SHA-256 (irrelevant for auth).
  • Libraries: If refactoring from scratch, consider open-source impls like Python's scram lib or MongoDB driver source (e.g., PyMongo's sasl_scram.py).

This should get you ~95% there with minimal code changes (mostly in preprocessing and hash swaps). If you share your language or code snippets, I can refine further!

Best Pattern for Authentication in MongoDB Clients (Version 8.1+)

In MongoDB 8.1 and later, the buildInfo command requires authentication when access control is enabled, which prevents unauthenticated clients from retrieving server build details (e.g., version, storage engines). This is a security enhancement to limit exposure of internal server information. Earlier versions allowed buildInfo without auth, so clients that relied on it during connection setup may need adjustments.

The recommended authentication pattern for MongoDB clients (e.g., drivers in Node.js, Python, Java, etc.) follows the standard connection lifecycle, which is designed to handle this constraint gracefully. Here's the step-by-step sequence:

  1. Establish the Initial Connection: Open a TCP socket to the MongoDB server(s). No commands are sent yet.

  2. Run the hello Command (Before Authentication):

    • Send { hello: 1 } (or the legacy { ismaster: 1 }) immediately after connecting. This is always safe and recommended before authentication.
    • Why before auth? It doesn't require authentication and is essential for topology discovery (e.g., identifying if the instance is a standalone, primary/secondary in a replica set, or shard). Drivers use this to build the cluster topology, select appropriate servers, and handle failover. Running it unauthenticated avoids blocking on auth failures and ensures the client can proceed even if credentials are invalid initially.
    • If auth fails later, the client can still report topology info for debugging.
  3. Authenticate the Session:

    • Once topology is known, authenticate using your chosen mechanism (e.g., SCRAM-SHA-256, the default and best practice for most cases; or X.509 for certificate-based auth).
    • Use the driver's built-in auth methods, passing credentials via the connection URI (e.g., mongodb://user:pass@host:27017/?authSource=admin) or explicitly in code. Authenticate against the appropriate database (usually admin for cluster-wide users).
    • Best practices:
      • Enable TLS/SSL for all connections to encrypt credentials in transit.
      • Use role-based access control (RBAC): Grant minimal roles (e.g., readWrite for app users, clusterMonitor if buildInfo is needed).
      • Avoid storing credentials in plain text; use secure vaults or environment variables.
      • For SCRAM, prefer SHA-256 over SHA-1 for stronger hashing.
  4. Run Privileged Commands (After Authentication):

    • Now execute commands like buildInfo, getMore, or data operations (find, insert).
    • If your client needs server version/build info early (e.g., for feature detection), fetch it post-auth. Most modern drivers cache this from the initial hello response (which includes a maxWireVersion for compatibility checks), reducing the need for buildInfo altogether.

Example in Python (using PyMongo driver)

from pymongo import MongoClient

# Step 1-2: Connect and implicit hello (PyMongo handles topology discovery)
client = MongoClient('mongodb://host:27017/')

# Step 3: Authenticate (PyMongo auto-auths if URI has creds; explicit here for clarity)
client.admin.authenticate('username', 'password', mechanism='SCRAM-SHA-256')

# Step 4: Now safe to run buildInfo
build_info = client.admin.command('buildInfo')
print(build_info['version'])  # e.g., "8.1.0"

Why This Pattern?

  • Resilient to Auth Failures: hello ensures basic connectivity and topology info even if auth fails.
  • Compliant with 8.1+: Avoids buildInfo pre-auth errors.
  • Efficient: Aligns with MongoDB's Server Discovery and Monitoring (SDAM) spec, used by all official drivers. No need for custom workarounds.
  • Alternatives if buildInfo is Critical Early: If your app must have build info before full auth (rare), consider a separate unauthenticated "monitor" user with clusterMonitor role, but this weakens security—avoid if possible.

For official driver-specific flows, refer to MongoDB's driver docs (e.g., C# Driver Authentication). If you're using a custom client, implement SDAM to mirror this sequence. If issues persist, check server logs for auth errors (e.g., SASL handshake failures).

Using hello Response Parameters for Authentication Mechanism Detection

No, you cannot reliably use hello.minWireVersion or hello.maxWireVersion to guess the authentication mechanism to be used. These fields indicate the range of supported wire protocol versions (an integer from 0 onward, where higher numbers denote newer protocol features like OP_MSG in version 6+). While they provide indirect clues about potential auth support—based on correlations with MongoDB server versions—they are not definitive for selecting or negotiating mechanisms. Here's why, plus details on other hello parameters:

Why Wire Versions Don't Suffice for Guessing Auth Mechanisms

  • Indirect Correlation Only:

    • Wire protocol versions evolve with MongoDB releases, and some auth features were introduced alongside them:
      MongoDB Version Min/Max Wire Version Range Key Auth-Related Changes
      Pre-3.0 0–2 MONGODB-CR (legacy, deprecated).
      3.0–3.4 3–5 SCRAM-SHA-1 introduced as default (replaces MONGODB-CR).
      3.6+ 6–7 Full OP_MSG support; SCRAM-SHA-1 stable. GSSAPI (Kerberos) enhanced.
      4.0+ 8–9 SCRAM-SHA-256 added as default (stronger hashing per RFC 7677).
      4.2+ 10–12 Minor auth tweaks; no major mechanism changes.
      5.0+ 13–15 AWS IAM support via MONGODB-AWS.
      6.0+ 16–17 OIDC preview; SCRAM-SHA-256 mandatory in some configs.
      7.0+ 18+ Further OIDC integration.
    • How to Infer: If maxWireVersion >= 8, the server likely supports SCRAM-SHA-256 (introduced in 4.0). For maxWireVersion >= 13, AWS IAM might be viable. However, this is server-version-dependent, not mechanism-specific—admins can disable mechanisms (e.g., via --setParameter disableNonSSLConnectionLogging=1 or role policies), and wire versions don't list enabled mechs.
    • Limitations: Guessing fails for custom configs, Enterprise-only mechs (e.g., LDAP via SASL/PLAIN), or if the client specifies a mechanism in the URI (e.g., ?authMechanism=SCRAM-SHA-1). Always negotiate during SASL handshake instead.
  • Recommendation: Use wire versions for compatibility checks (e.g., client must support at least minWireVersion), not auth selection. Drivers like PyMongo or the official Node.js driver use them to decide protocol features but fall back to SASL negotiation for auth.

Other hello Parameters and Auth Info

The standard hello response (without optional args) does not include direct auth mechanism details. Relevant fields:

  • No Other Auth-Relevant Fields: Parameters like isWritablePrimary, maxBsonObjectSize, readOnly, hosts, setName, etc., focus on topology, limits, and status—none expose auth mechs.
  • saslSupportedMechs (The Key Field for Auth Detection):
    • What It Is: An array of strings listing SASL-based mechanisms supported for a specific user (e.g., ["SCRAM-SHA-256", "SCRAM-SHA-1", "GSSAPI"]).
    • How to Get It: Include the optional saslSupportedMechs: "<username>" argument in your hello command:
      db.runCommand({ hello: 1, saslSupportedMechs: "myUser" })
      • This returns user-specific mechs (filtered by the user's roles and server config). It's available in MongoDB 4.4+ and requires no auth to query (but the username must exist).
      • Without this arg, the field is absent.
    • Use Cases: Ideal for clients to probe supported mechs pre-auth (e.g., fallback from SCRAM-SHA-256 to SHA-1 if unavailable). It's the best pattern for determining viable mechanisms without trial-and-error.
    • Limitations: User-specific (run per user); doesn't include non-SASL mechs like X.509 or MONGODB-AWS directly (those are negotiated separately).

Best Pattern for Clients

  1. Run standard { hello: 1 } for topology/wire versions.
  2. If needed, run { hello: 1, saslSupportedMechs: "<username>" } to list mechs.
  3. Negotiate via SASL (drivers handle this; specify in URI like ?authMechanism=SCRAM-SHA-256).
  4. Fallback: Start with SCRAM-SHA-256 (default in 4.0+); use wire versions to avoid deprecated clients.

This aligns with MongoDB's Server Discovery and Monitoring (SDAM) spec. For full details, see the hello command docs. If building a custom client, implement SASL per RFC 5802/7677.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment