MongoDB Authentication

By default, a freshly installed MongoDB server has no login at all — anyone who can reach the port can read and write every database on it. Authentication is the process of proving who you are (a username and password, a certificate, or an external identity provider) before MongoDB will run a single command for you. It is deliberately separate from authorization: authentication answers “who are you?”, while authorization (covered in the roles and RBAC lesson) answers “what are you allowed to do now that we know who you are?”. Every deployment that is reachable from anywhere other than a trusted localhost session should have authentication enabled.

Overview / How Authentication Works

A new mongod instance starts with no access control by default. You can connect with mongosh, run any command, drop any database, with nothing checked. This is convenient for local development but is not something you should ever expose to a network. To turn authentication on, you either start mongod with the --auth flag or set security.authorization: enabled in the YAML config file. Once that is on, every connection must present valid credentials before running commands, with one exception: the localhost exception, which lets you connect from the same machine without credentials only until the first user is created — and only to create that first user. This exists so you are never permanently locked out of a brand-new deployment.

Users are not global. Each user document is created against a specific database (the user’s authentication databaseauthSource), and is stored internally in admin.system.users, hashed — MongoDB never stores or transmits your password in plain text. The default mechanism, SCRAM-SHA-256 (Salted Challenge Response Authentication Mechanism), is a challenge-response protocol: the client and server exchange nonces and salted hashes back and forth, and at no point does the raw password cross the wire. This is the same family of protocol used by PostgreSQL and other modern databases.

MongoDB supports several authentication mechanisms beyond SCRAM, mostly relevant to MongoDB Enterprise and Atlas:

Mechanism Use case
SCRAM-SHA-256 Default since MongoDB 4.0. Username/password, salted hash challenge-response.
SCRAM-SHA-1 Legacy mechanism, still supported for backward compatibility; avoid for new deployments.
x.509 Client presents a TLS certificate instead of a password; common for service-to-service auth.
LDAP proxy Enterprise/Atlas only — delegates authentication to a corporate LDAP directory.
Kerberos (GSSAPI) Enterprise only — integrates with existing Kerberos infrastructure.
AWS IAM Atlas only — authenticate using AWS IAM credentials/roles instead of a MongoDB password.

A crucial detail beginners miss: a user created in one database is not automatically usable against another database unless you tell the driver which database to authenticate against via authSource. This trips people up constantly and is covered in Common Mistakes below.

Syntax

db.createUser({
  user: "<username>",
  pwd: "<password>",       // or passwordPrompt() to avoid typing it in plain text
  roles: [
    { role: "<roleName>", db: "<databaseName>" }
  ]
});
  • user — the username; unique within its authentication database.
  • pwd — the password; use passwordPrompt() in mongosh instead of a literal string so it never appears in your shell history.
  • roles — an array of built-in or custom roles, each naming the database that role applies to. This is what makes MongoDB’s authorization granular per-database.

Other user-management commands you will use constantly: db.getUsers() lists users on the current database, db.dropUser("name") removes one, db.updateUser("name", { roles: [...] }) changes roles, and db.changeUserPassword("name", "newPwd") rotates a password.

Examples

Example 1: Creating the first admin user and enabling authentication

Before turning on --auth, connect to a fresh, unauthenticated mongod and create an administrative user first — otherwise you will lock yourself out of everything except the narrow localhost exception.

use admin
db.createUser({
  user: "dbAdmin",
  pwd: passwordPrompt(),
  roles: [
    { role: "userAdminAnyDatabase", db: "admin" },
    { role: "readWriteAnyDatabase", db: "admin" },
    { role: "clusterAdmin", db: "admin" }
  ]
});
{ ok: 1 }

Now restart the server with authentication enabled:

mongod --auth --port 27017 --dbpath /data/db

And reconnect, this time presenting credentials:

mongosh "mongodb://dbAdmin:<password>@localhost:27017/admin"

From this point on, every connection to this server — local or remote — must authenticate. The userAdminAnyDatabase role lets this account create more users later; note it deliberately does not include full read/write by itself, which is why the example also grants readWriteAnyDatabase for a genuine admin account.

Example 2: Creating a scoped application user

An application should never connect using the admin account. Create a narrowly-scoped user for the actual app database:

use myapp
db.createUser({
  user: "myappUser",
  pwd: passwordPrompt(),
  roles: [
    { role: "readWrite", db: "myapp" }
  ]
});
{ ok: 1 }

This account can read and write documents in myapp only. It cannot drop other databases, create new users, or touch admin or any other application’s data, even if the connection string or credentials leak.

Example 3: Connecting and verifying the user

mongosh "mongodb://myappUser:<password>@localhost:27017/myapp?authSource=myapp"
db.getUsers();
{
  users: [
    {
      _id: 'myapp.myappUser',
      userId: UUID('a1b2c3d4-...'),
      user: 'myappUser',
      db: 'myapp',
      roles: [ { role: 'readWrite', db: 'myapp' } ],
      mechanisms: [ 'SCRAM-SHA-1', 'SCRAM-SHA-256' ]
    }
  ],
  ok: 1
}

The authSource=myapp query parameter tells the driver which database holds this user’s credentials. Because this user was created while use myapp was active, its authentication database is myapp, not admin.

Example 4: Connecting from a Node.js application

import { MongoClient } from "mongodb";

const user = process.env.MONGO_USER;
const pass = process.env.MONGO_PASS;
const uri = `mongodb://${user}:${pass}@/myapp?authSource=myapp`;

const client = new MongoClient(uri);

async function main() {
  await client.connect();
  const orders = client.db("myapp").collection("orders");
  const recent = await orders.find({ status: "shipped" }).limit(5).toArray();
  console.log(recent);
  await client.close();
}

main();

Credentials come from environment variables, never hardcoded strings, and the connection string explicitly sets authSource so the driver authenticates against the right database regardless of which database it ultimately queries.

How It Works Step by Step (the SCRAM handshake)

When a client authenticates with SCRAM-SHA-256, roughly this happens:

  • 1. Client-first message — the client sends the username and a random client nonce.
  • 2. Server-first message — the server looks up the user’s stored salt and iteration count, appends its own nonce, and sends back the combined nonce plus salt.
  • 3. Client-final message — the client combines the password (never sent directly) with the salt to derive a key, hashes it together with the full exchange so far, and sends only that proof.
  • 4. Server verification — the server performs the equivalent computation using the hash it stored at user-creation time and compares proofs. If they match, it returns its own proof so the client can verify the server too (mutual authentication), and the session is now authenticated.

Because the raw password is never transmitted and the stored value is a salted hash, even someone who reads the admin.system.users collection directly (or a database dump) cannot recover the plaintext password or replay it against a different, unrelated system.

Common Mistakes

Mistake 1: Enabling --auth before any user exists

Starting mongod --auth against a database with zero users does not error out, but you will find yourself unable to do anything except use the narrow localhost exception, which itself only permits creating that first user. Always create at least one userAdminAnyDatabase account before flipping --auth on, exactly as shown in Example 1.

Mistake 2: Forgetting authSource

A user created under one database will fail to authenticate if the driver assumes credentials live in whatever database you happen to be querying.

mongosh "mongodb://myappUser:<password>@localhost:27017/otherdb"
# MongoServerError: Authentication failed.

The fix is to explicitly tell the driver where the credentials actually live:

mongosh "mongodb://myappUser:<password>@localhost:27017/otherdb?authSource=myapp"

Mistake 3: Granting overprivileged roles to application accounts

It is tempting to give an app account root so you never hit a permissions error again — but that means a single leaked connection string can drop every database on the cluster.

// Wrong: an application account should never hold root
db.createUser({
  user: "appUser",
  pwd: passwordPrompt(),
  roles: [ "root" ]
});
// Correct: scope it to exactly the database and access it needs
db.createUser({
  user: "appUser",
  pwd: passwordPrompt(),
  roles: [ { role: "readWrite", db: "myapp" } ]
});

If different parts of the same application only need to read, give them read instead of readWrite — least privilege limits the blast radius of any single leaked credential.

Best Practices

  • Enable --auth (or security.authorization: enabled) on every deployment reachable from outside a trusted localhost session — there is no safe “we’ll add it later.”
  • Create one dedicated user per application/service, scoped to only the database(s) and roles it actually needs, rather than reusing an admin account everywhere.
  • Never hardcode credentials in source code or commit them to version control; load them from environment variables or a secrets manager.
  • Always include authSource in connection strings unless the user’s authentication database happens to equal the database you’re connecting to.
  • Prefer SCRAM-SHA-256 over the legacy SCRAM-SHA-1 for new users; it uses a stronger hash and is the modern default.
  • Rotate passwords periodically with db.changeUserPassword(), and drop unused accounts with db.dropUser().
  • In production, layer authentication with TLS/network encryption and IP allowlisting — authentication alone does not protect data in transit.
  • Use db.getUsers() periodically to audit which accounts exist and what roles they hold.

Practice Exercises

  • Start a local mongod without --auth, create a userAdminAnyDatabase account on admin, then restart with --auth and confirm an unauthenticated mongosh connection is refused for normal operations.
  • Create two users on two different databases (shop and analytics), each with readWrite scoped only to its own database. Confirm the shop user’s connection string with authSource=shop fails to read from analytics.
  • Write a short Node.js script using the driver that reads its username and password from process.env, builds a connection string with authSource, and connects successfully. Expected result: a resolved MongoClient connection with no credentials visible anywhere in your source file.

Summary

  • Authentication confirms identity; authorization (roles) decides what an authenticated identity can do — they are separate systems.
  • MongoDB has no authentication by default; enable it with --auth or security.authorization: enabled, and always create an admin user first.
  • SCRAM-SHA-256 is the default mechanism: passwords are never sent or stored in plain text, only salted hashes and challenge-response proofs.
  • Users are created per-database via db.createUser(); that database becomes their authSource unless the connection string says otherwise.
  • Scope application accounts to least privilege — never hand out root for convenience.
  • Forgetting authSource in a connection string is one of the most common “Authentication failed” errors in practice.