Authentication failed from MongoDB, error code 18, means one thing: the server received a username and password and could not match them to a user. It does not mean the server is down, the port is closed or TLS is broken - those fail earlier and look different. When the credentials are definitely right, the cause is nearly always one of four things: the user lives in a different database from the one the driver asked (authSource), the password the driver sent is not the password you typed (encoding, quoting, a stray newline), the connection string is not the one you think is being used, or a mechanism or +srv default has changed what gets sent.
The quickest route to the answer is to stop guessing from the client side and ask the server, then reproduce the connection by hand in mongosh. This post goes through that diagnosis in order, then the traps specific to each driver. For how a URI is put together in the first place - the parts, the options, pool sizes - see MongoDB connection strings; this one assumes you have a string and it is not working.
First, confirm it really is an authentication error#
A surprising amount of time is lost fixing passwords for errors that were never about passwords. MongoDB fails in layers, and each layer has its own message. Work out which layer stopped you before changing anything.
| Message | Layer | Look at |
|---|---|---|
Authentication failed / code 18 | Credentials | User, database, password |
bad auth : Authentication failed | Credentials (Atlas) | Database user, not your Atlas login |
ServerSelectionTimeoutError | Network or TLS | Host, port, firewall, tls |
ECONNREFUSED | Network | Bind address, port, service running |
querySrv ENOTFOUND | DNS | +srv on a host without SRV records |
requires authentication (code 13) | Credentials missing | URI has no user, or env not loaded |
not authorized on shop to execute | Roles | User exists but lacks the role |
The last two are worth separating from the first. command find requires authentication means you connected anonymously - the credentials never made it into the request, typically because an environment variable was empty and the code fell back to mongodb://localhost:27017. not authorized means authentication worked and the user simply has no role on that database. Neither is fixed by changing the password, and both are fixed by looking at what the user is allowed to do, or at which URI your process actually loaded.
A good habit before going further: print the URI your application is really using, with the password masked. Something like uri.replace(/:([^@/]+)@/, ":****@") in JavaScript gives you the host, port, database and options without leaking the secret. A good share of "Authentication failed with the right password" turns out to be a staging URI in a production container, or a default in the code that was never overridden.
Ask the server which part was wrong#
The client gets a deliberately vague message, because telling an attacker "that user does not exist" would help them. The server log is not vague. If you have access to the mongod log, find the line for the failed attempt; on MongoDB 4.4 and later it is structured JSON with the message Authentication failed and an attr object that names the mechanism, the user, the database it looked in, the client address, and the actual reason.
{"msg":"Authentication failed","attr":{"mechanism":"SCRAM-SHA-256", "principalName":"shopapp","authenticationDatabase":"shop", "remote":"203.0.113.40:51522", "error":"UserNotFound: Could not find user \"shopapp\" for db \"shop\""}}That one line answers most of the question. Read three fields:
authenticationDatabaseis the database the driver asked. If it saysshopand your user was created inadmin, the fix isauthSource=admin, and the password was never the problem.errorstarting withUserNotFoundmeans no user by that name exists in that database. Either the name is misspelt, it lives in another database, or you are talking to a different server from the one you created it on.errormentioning a SCRAM failure such asstoredKey mismatchmeans the user was found and the password did not match. Now, and only now, is it worth looking at the password itself.
The exact wording and layout of these lines varies between versions, and older servers wrote plain text rather than JSON, but the same three facts are in there. If you cannot read the server log - a managed service, or a server you do not run - mongosh gets you to the same answers, as below.
authSource and the user that lives somewhere else#
Every MongoDB user belongs to one database. The connection string's authSource says which one to look in, and when it is missing the driver uses the database in the URI path, falling back to admin only when the path is empty. That default is the single largest cause of code 18.
The classic case: you were given an administrative user that lives in admin, and you write a URI pointing at your application database.
mongodb://root:pw@db.example.net:27017/shopThe driver looks for root in shop, finds nobody, and reports failure. Add ?authSource=admin and the same credentials work. The reverse mistake is just as common: a user created with use shop before db.createUser(...), then a URI with authSource=admin copied from a tutorial. That user does not exist in admin, so the answer is authSource=shop - or recreate the user in admin with a role scoped to shop and stop thinking about it.
To find out where a user really lives, connect with any account that can view users and ask the whole server:
db.adminCommand({ usersInfo: { forAllDBs: true } }).users .map(u => `${u.user} @ ${u.db}`)Each line shows a username and the database it belongs to. Two users with the same name in two databases are entirely separate accounts with separate passwords, which explains the occasional "it works from one app and not the other" - the two apps are authenticating as different users that happen to share a name.
For a single database you can also run db.getSiblingDB("shop").getUsers(). An empty array means there is nobody in shop, which settles the authSource question at once.
The wrong server entirely
UserNotFound for a user you know you created has a duller explanation more often than people admit: the client is not talking to the server you created it on. A local mongod on your laptop listening on 27017 will happily answer a URI meant for a remote host if an SSH tunnel is forwarding the same local port, or if a .env file points at localhost. Check with db.hostInfo().system.hostname once connected, or look at the remote address in the server log. If you use a tunnel, forward to a different local port such as 27018 so the two can never be confused - SSH tunnels and port forwarding covers the syntax.
The password that is not the password you typed#
When the server log says the user was found and the password did not match, assume the bytes that arrived differ from the ones you intended. These are the ways that happens, roughly in order of frequency.
Percent-encoding, missing or doubled
Any of : / ? # [ ] @ % in the password must be percent-encoded inside a URI. Leave one unencoded and the driver either rejects the URI or reads part of the password as the host. The opposite failure is quieter: encoding twice. A password of p@ss correctly becomes p%40ss in the URI; if that encoded string is then passed through encodeURIComponent again, it becomes p%2540ss, and the server receives the literal text p%40ss. Encode exactly once, at the point where the URI is built, and never store an already-encoded password in a variable that something else will encode.
The same thing happens in reverse when the password is passed as a separate driver option instead of in the URI. Separate options are not URI-decoded, so a password copied from an encoded URI and pasted into a password field arrives with %40 in it. Whichever form you use, the separate field takes the raw password and the URI takes the encoded one.
Shell quoting
Put a URI on a command line without quotes and the shell gets to it first. & between options backgrounds the command and drops everything after it, so authSource=admin silently vanishes. Inside double quotes, $ starts a variable: a password of pa$word becomes pa plus whatever $word expands to, usually nothing. In an interactive bash session ! can trigger history expansion too.
$ mongosh 'mongodb://shopapp:pa$word@db.example.net:27017/shop?authSource=admin'Single quotes in bash and zsh pass everything through literally. PowerShell expands $ inside double quotes as well and uses the backtick as its escape character, so single quotes are the safe choice there too. Better still, leave the password out and let mongosh prompt for it, which removes the shell from the problem entirely.
Environment files, secrets and trailing newlines
A secret created with echo "pw" > file contains a newline after the password; printf '%s' "pw" does not. Read that file into an environment variable or a Kubernetes secret and the driver sends pw\n, which does not match. Some .env loaders keep surrounding quotes as part of the value, some strip them, and the behaviour differs between libraries and versions. Docker Compose treats $ in a compose file as variable interpolation, so a literal dollar sign has to be written $$ there.
The quick check is to print the length of the password your process received and compare it with the length you expected. An extra one or two characters is a newline or a pair of quotes.
Docker's root user that was never recreated
The official mongo image creates its root user from MONGO_INITDB_ROOT_USERNAME and MONGO_INITDB_ROOT_PASSWORD, in the admin database, and only when the data directory is empty on first start. Change those variables later and nothing happens: the existing volume still holds the old user with the old password. If you rotated the password in the compose file and auth now fails, that is why. Either change the password inside the database with changeUserPassword, or deliberately throw the volume away and start again - which deletes the data. Note also that MONGO_INITDB_DATABASE does not create a user in that database; the root user still lives in admin, so authSource=admin applies.
mongodb vs mongodb+srv, and what it changes for auth#
A mongodb+srv:// string makes the driver look up the server list in DNS. It also reads a TXT record on the same name, and that TXT record is allowed to set exactly three options: authSource, replicaSet and loadBalanced. This is how managed services such as MongoDB Atlas make authSource=admin appear without you writing it. If you take an Atlas-style string apart and rewrite it as a plain mongodb:// list of hosts, you lose those defaults, and the result fails authentication unless you add authSource=admin back yourself. Options in the URI override the TXT record, so an authSource you add by hand always wins.
The other +srv default is TLS, which is on. Converting to mongodb:// without adding tls=true gives you a connection the server refuses, and that surfaces as a timeout rather than an auth error.
For Atlas specifically, a few points account for most "bad auth" reports:
- The user in the URI must be a database user from the project's Database Access page, not the email and password you use to sign in to the Atlas website. They are unrelated accounts.
- A newly created or edited database user can take a short while to propagate to the cluster; an immediate failure after a change is worth one retry before you start debugging.
- An address missing from the network access list does not produce
bad auth. It produces a server selection timeout, because the connection is dropped before authentication starts.
When people search for "mongodb get connection string", the answer depends on where the database is. Atlas gives you one from the cluster's Connect dialogue, in +srv form. A self-hosted or rented standalone server gives you a host, a port, a user and a password, and you build mongodb://user:pass@host:port/db?authSource=... yourself. A standalone server has no SRV records, so +srv does not apply.
TLS options: when the failure is not authentication#
TLS problems happen before the server ever sees a password, so they never produce code 18. They produce ServerSelectionTimeoutError or a handshake error mentioning certificates. It is still worth knowing the options, because changing TLS settings while chasing an auth bug often adds a second failure on top of the first.
| Option | Default | Use |
|---|---|---|
tls | false (true with +srv) | Encrypt the connection |
tlsCAFile | system store | CA that signed a private certificate |
tlsCertificateKeyFile | none | Client certificate, for X.509 |
tlsAllowInvalidHostnames | false | Skip name check - testing only |
tlsAllowInvalidCertificates | false | Skip all checks - testing only |
ssl=true is an older alias for tls=true and is still accepted. If a server uses a certificate from a private CA, point tlsCAFile at that CA rather than reaching for tlsAllowInvalidCertificates, which accepts any certificate from anyone and defeats the point of encrypting. Setting tls=true against a server that is not configured for TLS fails at the handshake, whatever the credentials.
One authentication method does depend on TLS: X.509 client certificates. With authMechanism=MONGODB-X509 the certificate is the credential, there is no password, and authSource must be $external, the virtual database that holds externally authenticated users. LDAP (PLAIN), Kerberos (GSSAPI) and AWS IAM (MONGODB-AWS) also authenticate against $external. If you copied a URI that says authSource=$external and you are using a normal username and password, that is your bug.
SCRAM mechanisms
Ordinary password users authenticate with SCRAM. Since MongoDB 4.0, new users get both SCRAM-SHA-1 and SCRAM-SHA-256 credentials by default, and drivers negotiate the stronger one. Problems appear when a URI forces authMechanism=SCRAM-SHA-1 and the user was created with only SHA-256 (or the reverse), or when a driver too old to know SHA-256 meets a user that only has SHA-256. The fix is nearly always to delete authMechanism from the URI and let the driver negotiate. usersInfo shows a user's mechanisms array if you need to check.
Testing the connection with mongosh#
mongosh is the reference client. If it connects with a set of values, those values are correct, and anything that fails with the same values is a problem in the application or its environment. Test in this order, changing one thing at a time.
- Connect without credentials, to prove the network path:
mongosh "mongodb://db.example.net:27017" --eval 'db.runCommand({ ping: 1 })'. A{ ok: 1 }means host, port and TLS are fine; a timeout means stop looking at passwords. - Connect with separate flags and a prompted password, so neither encoding nor shell quoting can interfere.
- Once that works, connect with your full URI in single quotes. If step 2 succeeds and step 3 fails, the URI is the problem - encoding,
authSource, or an option. - Finally, run the application with the same URI. If it fails here, the application is receiving something different.
$ mongosh --host db.example.net --port 27017 \ -u shopapp -p --authenticationDatabase admin shop-p with no value prompts for the password. --authenticationDatabase is the command-line name for authSource, which is a common source of confusion when switching between the two forms.
You can also test credentials from inside an existing session, which is handy when you are already connected as an administrator and want to try a different user:
db.getSiblingDB("admin").auth("shopapp", passwordPrompt())db.runCommand({ connectionStatus: 1 }).authInfoauth() returns { ok: 1 } on success and throws an Authentication failed error otherwise; the database you call it on is the authSource being tested. connectionStatus then shows which users and roles the session holds. Compass behaves the same way: in its advanced options the Authentication tab has an "Authentication Database" field, which is authSource under another name.
Driver-specific gotchas#
Every driver accepts the same URI, but each has its own way of taking credentials as separate options, and each has a quirk that produces code 18.
Node.js driver and Mongoose
The Node driver takes credentials either in the URI or as auth: { username, password }, with authSource as a top-level option. Separate options are not decoded, so pass the raw password there.
const client = new MongoClient("mongodb://db.example.net:27017/shop", { auth: { username: "shopapp", password: process.env.MONGO_PASSWORD }, authSource: "admin",});Mongoose adds dbName, user and pass options. dbName changes which database your models use, but it is easy to end up with a URI path and a dbName that disagree about which database is meant. Whenever you use dbName, set authSource explicitly rather than relying on any default, and the question never arises.
PyMongo
PyMongo accepts username, password and authSource keyword arguments, which avoid encoding altogether. If you do build a URI, urllib.parse.quote_plus is the function the PyMongo documentation uses. PyMongo reports the failure as OperationFailure with code 18 and codeName: AuthenticationFailed in the full error; catching ServerSelectionTimeoutError instead and logging "auth failed" is a common way to hide a network problem behind the wrong message.
client = MongoClient( "db.example.net", 27017, username="shopapp", password=os.environ["MONGO_PASSWORD"], authSource="admin",)Go, Java and PHP
The Go driver applies a URI and then lets you override credentials with options.Credential, whose AuthSource field defaults the same way the URI does. If you call SetAuth with a credential that leaves AuthSource empty, check what the URI path says. In Java, MongoCredential.createScramSha256Credential(user, source, password) takes the auth database as its second argument - pass the application database there by habit and you have reproduced the classic bug in a new language. The PHP library takes URI options as the second argument to new MongoDB\Client($uri, $uriOptions), where username, password and authSource can be set without encoding.
Frameworks that build the URI for you
Spring Boot, Laravel packages and similar wrappers often assemble the URI from separate settings such as host, database, username and password. Read how yours does it. Several of them put the configured database in the URI path, which makes it the default authSource, and expose a separate setting for the authentication database that is easy to miss. When a framework is involved, log the final URI (masked) once at start-up; it is usually obvious from there.
Resetting a password or recovering access#
If the user exists and you simply do not know its password, an administrator can set a new one without touching anything else:
db.getSiblingDB("admin").changeUserPassword("shopapp", passwordPrompt())Run it on the database the user belongs to - admin here, shop if that is where the user lives. Existing connections stay authenticated, so restart the application afterwards to prove the new password works.
On a server you run yourself and where you have lost every administrator password, the documented route is to restart mongod with access control disabled (remove or comment out security.authorization: enabled in mongod.conf), bound only to 127.0.0.1, create or reset an administrator, and then turn authorization back on. Do it with the database unreachable from outside for the whole window, because for those minutes anyone who can connect is effectively root.
When there are no users at all, MongoDB allows a connection from localhost to create the first one - the localhost exception. Once any user exists, the exception closes. That is why a fresh self-hosted install sometimes accepts unauthenticated commands from the server itself and refuses them from anywhere else.
On RE:NODE database hosting, a MongoDB server is created with credentials generated for that server and is reached directly on the host and port shown on the plan - there is no proxy in front of it, so the TLS and +srv traps above do not come into play unless you set them up. If authentication fails, the usual suspects are the ones in this post: the database the user belongs to, and how the password got into your URI. The databases section of the guides covers how the panel handles database credentials.
FAQ#
Why does MongoDB say authentication failed when my password is correct?
Because the server looked for the user in a different database from the one where it was created. The driver searches the database named by authSource, and when that is missing it uses the database in the URI path. A user created in admin and a URI ending in /shop fails every time until you add ?authSource=admin.
What does 'bad auth : Authentication failed' mean on Atlas?
It is Atlas's wording for the same error code. Check that you are using a database user from the Database Access page rather than your Atlas sign-in, that the password in the URI is percent-encoded, and that you have not rewritten the +srv string as a plain host list without adding authSource=admin back.
How do I find which database a MongoDB user belongs to?
Connect as an administrator and run db.adminCommand({ usersInfo: { forAllDBs: true } }). Each entry has a user and a db field; the db value is what goes in authSource. Without administrative access, the server log line for a failed attempt shows which database the client asked.
Do I need to URL-encode the password if I pass it separately?
No, and you must not. Encoding applies only to the URI. Driver options such as password, and the -p prompt in mongosh, take the raw password; an encoded value there is sent with the % sequences intact and fails.
Is authSource the same as the database I want to use?
Only when the user was created in that database. The database in the URI path is where your queries go by default; authSource is where the server looks up your account. They often differ, which is the whole reason the option exists.
Can I use mongodb+srv with a single server I rent?
Only if someone has created SRV records for it, and a single rented server normally has none. Use mongodb:// with the host and port. If you want a readable name, an A record pointing at the address is enough.


