RE:NODE

MongoDB Authentication failed: authSource a další opravy

Text: Tým RE:NODE
17 min čtení


Authentication failed z MongoDB, chybový kód 18, znamená jedinou věc: server dostal uživatelské jméno a heslo a nedokázal je přiřadit k žádnému uživateli. Neznamená to, že server neběží, že je zavřený port nebo že je rozbité TLS - ty problémy selžou dřív a vypadají jinak. Když jsou přihlašovací údaje jistě správné, příčinou je téměř vždy jedna ze čtyř věcí: uživatel žije v jiné databázi, než kterou se driver ptal (authSource), heslo, které driver poslal, není heslo, které jsi napsal (kódování, uvozovky, zatoulaný konec řádku), connection string není ten, o kterém si myslíš, že se používá, nebo mechanismus či výchozí hodnota u +srv změnily, co se posílá.

Nejrychlejší cesta k odpovědi je přestat hádat z klientské strany, zeptat se serveru a pak připojení ručně zopakovat v mongosh. Tento článek prochází diagnostiku popořadě a potom pasti specifické pro jednotlivé drivery. O tom, jak se URI vůbec skládá - části, volby, velikosti poolu - se dočteš v článku MongoDB connection strings; tady předpokládáme, že řetězec máš a nefunguje.

Nejdřív ověř, že jde opravdu o chybu autentizace#

Překvapivé množství času se ztratí opravováním hesel kvůli chybám, které s hesly nikdy nesouvisely. MongoDB selhává po vrstvách a každá vrstva má vlastní hlášku. Než cokoli změníš, zjisti, na které vrstvě ses zastavil.

HláškaVrstvaKam se podívat
Authentication failed / code 18Přihlašovací údajeUživatel, databáze, heslo
bad auth : Authentication failedPřihlašovací údaje (Atlas)Databázový uživatel, ne tvůj login do Atlasu
ServerSelectionTimeoutErrorSíť nebo TLSHost, port, firewall, tls
ECONNREFUSEDSíťBind adresa, port, běžící služba
querySrv ENOTFOUNDDNS+srv u hostu bez SRV záznamů
requires authentication (code 13)Chybí přihlašovací údajeURI neobsahuje uživatele nebo se nenačetlo prostředí
not authorized on shop to executeRoleUživatel existuje, ale nemá roli

Poslední dvě je dobré oddělit od první. command find requires authentication znamená, že ses připojil anonymně - přihlašovací údaje se do požadavku nikdy nedostaly, typicky proto, že proměnná prostředí byla prázdná a kód sáhl po výchozím mongodb://localhost:27017. not authorized znamená, že autentizace proběhla a uživatel prostě nemá v dané databázi žádnou roli. Ani jedno se spraví změnou hesla, obojí se řeší tím, že se podíváš, co uživatel smí, nebo které URI tvůj proces skutečně načetl.

Dobrý zvyk, než půjdeš dál: vypiš URI, které aplikace opravdu používá, s maskovaným heslem. Něco jako uri.replace(/:([^@/]+)@/, ":****@") v JavaScriptu ti ukáže host, port, databázi a volby bez úniku tajemství. Slušná část případů „Authentication failed se správným heslem“ se ukáže jako staging URI v produkčním kontejneru nebo výchozí hodnota v kódu, kterou nikdo nepřepsal.

Zeptej se serveru, která část byla špatně#

Klient dostane záměrně vágní hlášku, protože sdělit útočníkovi „takový uživatel neexistuje“ by mu pomohlo. Log serveru vágní není. Pokud máš přístup k logu mongod, najdi řádek k neúspěšnému pokusu; v MongoDB 4.4 a novějších je to strukturovaný JSON se zprávou Authentication failed a objektem attr, který pojmenovává mechanismus, uživatele, databázi, ve které se hledalo, adresu klienta a skutečný důvod.

json
{"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\""}}

Ten jediný řádek zodpoví většinu otázky. Čti tři pole:

  • authenticationDatabase je databáze, na kterou se driver ptal. Pokud tam stojí shop a tvůj uživatel byl vytvořen v admin, oprava je authSource=admin a heslo nikdy nebylo problém.
  • error začínající UserNotFound znamená, že v dané databázi žádný uživatel s tím jménem neexistuje. Buď je jméno s překlepem, žije v jiné databázi, nebo mluvíš s jiným serverem než tím, na kterém jsi ho vytvořil.
  • error zmiňující selhání SCRAM, například storedKey mismatch, znamená, že uživatel byl nalezen a heslo nesedělo. Teprve teď má smysl podívat se na samotné heslo.

Přesné znění a rozložení těchto řádků se mezi verzemi liší a starší servery psaly prostý text místo JSONu, ale tytéž tři fakty v nich jsou. Pokud log serveru číst nemůžeš - spravovaná služba nebo server, který neprovozuješ - dostaneš se ke stejným odpovědím přes mongosh, jak je popsáno níže.

authSource a uživatel, který žije jinde#

Každý uživatel MongoDB patří do jedné databáze. authSource v connection stringu říká, ve které se má hledat, a když chybí, driver použije databázi z cesty v URI a na admin se vrátí, jen když je cesta prázdná. Tato výchozí hodnota je zdaleka největší příčinou kódu 18.

Klasický případ: dostal jsi administrátorského uživatele, který žije v admin, a napíšeš URI mířící na aplikační databázi.

code
mongodb://root:pw@db.example.net:27017/shop

Driver hledá root v shop, nikoho nenajde a ohlásí selhání. Přidej ?authSource=admin a stejné přihlašovací údaje fungují. Opačná chyba je stejně běžná: uživatel vytvořený po use shop příkazem db.createUser(...) a pak URI s authSource=admin zkopírované z nějakého tutoriálu. Takový uživatel v admin neexistuje, takže správná odpověď je authSource=shop - nebo uživatele znovu vytvoř v admin s rolí omezenou na shop a přestaň nad tím přemýšlet.

Abys zjistil, kde uživatel skutečně žije, připoj se libovolným účtem, který smí zobrazovat uživatele, a zeptej se celého serveru:

javascript
db.adminCommand({ usersInfo: { forAllDBs: true } }).users  .map(u => `${u.user} @ ${u.db}`)

Každý řádek ukáže uživatelské jméno a databázi, do které patří. Dva uživatelé se stejným jménem ve dvou databázích jsou naprosto oddělené účty s oddělenými hesly, což vysvětluje občasné „z jedné aplikace to funguje a z druhé ne“ - obě aplikace se autentizují jako různí uživatelé, kteří se shodou okolností jmenují stejně.

Pro jednu databázi můžeš také spustit db.getSiblingDB("shop").getUsers(). Prázdné pole znamená, že v shop nikdo není, a otázka authSource je tím okamžitě vyřešená.

Úplně jiný server

UserNotFound u uživatele, o kterém víš, že jsi ho vytvořil, má častěji, než se přiznává, banálnější vysvětlení: klient nemluví se serverem, na kterém jsi ho vytvořil. Lokální mongod na tvém notebooku, který poslouchá na 27017, ochotně odpoví na URI určené pro vzdálený host, pokud stejný lokální port přesměrovává SSH tunel nebo pokud soubor .env míří na localhost. Ověř to po připojení příkazem db.hostInfo().system.hostname nebo se podívej na adresu remote v logu serveru. Pokud používáš tunel, přesměruj na jiný lokální port, třeba 27018, aby se obě spojení nikdy nedala zaměnit - syntaxi pokrývá článek SSH tunely a přesměrování portů.

Heslo, které není to, které jsi napsal#

Když log serveru říká, že uživatel byl nalezen a heslo nesedělo, předpokládej, že dorazily jiné bajty, než jaké sis přál. Tady jsou způsoby, jak se to stane, zhruba podle četnosti.

Procentové kódování, chybějící nebo zdvojené

Kterýkoli ze znaků : / ? # [ ] @ % v hesle musí být v URI procentově zakódován. Nech jeden nezakódovaný a driver buď URI odmítne, nebo přečte část hesla jako host. Opačná chyba je nenápadnější: kódování dvakrát. Heslo p@ss se v URI správně změní na p%40ss; pokud se tento zakódovaný řetězec pošle ještě jednou přes encodeURIComponent, stane se z něj p%2540ss a server dostane doslovný text p%40ss. Kóduj přesně jednou, v místě, kde se URI sestavuje, a nikdy neukládej už zakódované heslo do proměnné, kterou bude kódovat něco dalšího.

Totéž se děje obráceně, když se heslo předává jako samostatná volba driveru místo v URI. Samostatné volby se nedekódují, takže heslo zkopírované ze zakódovaného URI a vložené do pole password dorazí s %40 uvnitř. Ať použiješ kteroukoli formu, samostatné pole bere surové heslo a URI bere zakódované.

Uvozovky v shellu

Vlož URI na příkazovou řádku bez uvozovek a shell se k němu dostane první. & mezi volbami pošle příkaz na pozadí a zahodí vše za sebou, takže authSource=admin tiše zmizí. Uvnitř dvojitých uvozovek začíná $ proměnnou: heslo pa$word se změní na pa plus cokoli, na co se rozvine $word, obvykle nic. V interaktivní relaci bashe může ! spustit i rozvinutí historie.

bash
$ mongosh 'mongodb://shopapp:pa$word@db.example.net:27017/shop?authSource=admin'

Jednoduché uvozovky v bashi a zsh předávají všechno doslovně. PowerShell rozvíjí $ také uvnitř dvojitých uvozovek a jako únikový znak používá zpětný apostrof, takže i tam jsou bezpečnou volbou jednoduché uvozovky. Ještě lepší je heslo vynechat a nechat mongosh, ať se na něj zeptá, čímž shell z problému úplně zmizí.

Soubory prostředí, secrets a koncové konce řádků

Secret vytvořený příkazem echo "pw" > file obsahuje za heslem konec řádku; printf '%s' "pw" ho neobsahuje. Načti takový soubor do proměnné prostředí nebo do Kubernetes secretu a driver pošle pw\n, což nesedí. Některé loadery .env ponechávají okolní uvozovky jako součást hodnoty, jiné je odstraní a chování se liší mezi knihovnami i verzemi. Docker Compose bere $ v compose souboru jako interpolaci proměnné, takže doslovné dolary se tam musí psát $$.

Rychlá kontrola je vypsat délku hesla, které tvůj proces dostal, a porovnat ji s tou, kterou jsi čekal. Jeden nebo dva znaky navíc jsou konec řádku nebo dvojice uvozovek.

Root uživatel v Dockeru, který se nikdy nevytvořil znovu

Oficiální image mongo vytváří svého root uživatele z MONGO_INITDB_ROOT_USERNAME a MONGO_INITDB_ROOT_PASSWORD, v databázi admin, a to jen když je datový adresář při prvním startu prázdný. Změň ty proměnné později a nestane se nic: stávající volume stále drží starého uživatele se starým heslem. Pokud jsi heslo změnil v compose souboru a autentizace teď selhává, je to tím. Buď změň heslo uvnitř databáze pomocí changeUserPassword, nebo volume záměrně zahoď a začni znovu - což smaže data. Pamatuj také, že MONGO_INITDB_DATABASE v dané databázi uživatele nevytvoří; root uživatel stále žije v admin, takže platí authSource=admin.

mongodb vs mongodb+srv a co to mění u autentizace#

Řetězec mongodb+srv:// způsobí, že driver hledá seznam serverů v DNS. Čte také záznam TXT na stejném jménu a ten smí nastavit přesně tři volby: authSource, replicaSet a loadBalanced. Takhle spravované služby jako MongoDB Atlas docílí, že se authSource=admin objeví, aniž bys ho psal. Pokud rozebereš řetězec ve stylu Atlasu a přepíšeš ho na prostý seznam hostů mongodb://, o tyto výchozí hodnoty přijdeš a výsledek autentizací neprojde, dokud authSource=admin sám nepřidáš zpět. Volby v URI přebíjejí záznam TXT, takže authSource přidaný ručně vždy vyhraje.

Druhou výchozí hodnotou u +srv je TLS, které je zapnuté. Převod na mongodb:// bez přidání tls=true dá spojení, které server odmítne, a to se projeví jako timeout, ne jako chyba autentizace.

U Atlasu konkrétně většinu hlášení „bad auth“ vysvětlí několik bodů:

  • Uživatel v URI musí být databázový uživatel ze stránky Database Access projektu, ne e-mail a heslo, kterými se přihlašuješ na web Atlasu. Jsou to nesouvisející účty.
  • Nově vytvořenému nebo upravenému databázovému uživateli může chvíli trvat, než se rozšíří do clusteru; okamžité selhání po změně si zaslouží jeden opakovaný pokus, než začneš ladit.
  • Adresa chybějící v seznamu síťového přístupu bad auth nevyvolá. Vyvolá timeout při výběru serveru, protože spojení je zahozeno dřív, než autentizace začne.

Když lidé hledají „mongodb get connection string“, odpověď závisí na tom, kde databáze běží. Atlas ti jeden dá v dialogu Connect u clusteru, ve formě +srv. Self-hosted nebo pronajatý samostatný server ti dá host, port, uživatele a heslo a mongodb://user:pass@host:port/db?authSource=... si sestavíš sám. Samostatný server nemá žádné SRV záznamy, takže +srv se nepoužije.

Volby TLS: když selhání není autentizace#

Problémy s TLS nastanou dřív, než server uvidí jakékoli heslo, takže nikdy nevyvolají kód 18. Vyvolají ServerSelectionTimeoutError nebo chybu handshaku zmiňující certifikáty. Volby je přesto dobré znát, protože změna nastavení TLS při honbě za chybou autentizace často přidá k první chybě druhou.

VolbaVýchozíPoužití
tlsfalse (true s +srv)Šifrovat spojení
tlsCAFilesystémové úložištěCA, která podepsala privátní certifikát
tlsCertificateKeyFiležádnýKlientský certifikát, pro X.509
tlsAllowInvalidHostnamesfalsePřeskočit kontrolu jména - jen pro testování
tlsAllowInvalidCertificatesfalsePřeskočit všechny kontroly - jen pro testování

ssl=true je starší alias pro tls=true a stále se přijímá. Pokud server používá certifikát od privátní CA, nasměruj tlsCAFile na tuto CA, místo abys sahal po tlsAllowInvalidCertificates, které přijme jakýkoli certifikát od kohokoli a maří smysl šifrování. Nastavení tls=true proti serveru, který pro TLS není nakonfigurovaný, selže při handshaku bez ohledu na přihlašovací údaje.

Jedna metoda autentizace na TLS závisí: klientské certifikáty X.509. S authMechanism=MONGODB-X509 je přihlašovacím údajem certifikát, žádné heslo neexistuje a authSource musí být $external, virtuální databáze, která drží externě autentizované uživatele. Také LDAP (PLAIN), Kerberos (GSSAPI) a AWS IAM (MONGODB-AWS) se autentizují proti $external. Pokud jsi zkopíroval URI s authSource=$external a používáš běžné uživatelské jméno a heslo, je chyba právě tam.

Mechanismy SCRAM

Běžní uživatelé s heslem se autentizují přes SCRAM. Od MongoDB 4.0 dostávají noví uživatelé ve výchozím nastavení přihlašovací údaje SCRAM-SHA-1 i SCRAM-SHA-256 a drivery vyjednají silnější z nich. Problémy nastanou, když URI vynutí authMechanism=SCRAM-SHA-1 a uživatel byl vytvořen jen se SHA-256 (nebo naopak), nebo když driver příliš starý na to, aby znal SHA-256, narazí na uživatele, který má jen SHA-256. Oprava je téměř vždy smazat authMechanism z URI a nechat driver vyjednat. usersInfo ukáže pole mechanisms daného uživatele, pokud to potřebuješ ověřit.

Test připojení pomocí mongosh#

mongosh je referenční klient. Pokud se s nějakou sadou hodnot připojí, jsou tyto hodnoty správné a cokoli, co se stejnými hodnotami selže, je problém aplikace nebo jejího prostředí. Testuj v tomto pořadí a měň vždy jednu věc.

  1. Připoj se bez přihlašovacích údajů, abys dokázal síťovou cestu: mongosh "mongodb://db.example.net:27017" --eval 'db.runCommand({ ping: 1 })'. Odpověď { ok: 1 } znamená, že host, port i TLS jsou v pořádku; timeout znamená, že přestaň koukat na hesla.
  2. Připoj se se samostatnými přepínači a heslem zadaným na výzvu, aby do toho nemohlo zasáhnout kódování ani uvozovky v shellu.
  3. Jakmile to funguje, připoj se s celým URI v jednoduchých uvozovkách. Pokud krok 2 uspěje a krok 3 selže, problém je v URI - kódování, authSource nebo nějaká volba.
  4. Nakonec spusť aplikaci se stejným URI. Pokud selže tady, aplikace dostává něco jiného.
bash
$ mongosh --host db.example.net --port 27017 \    -u shopapp -p --authenticationDatabase admin shop

-p bez hodnoty se zeptá na heslo. --authenticationDatabase je název authSource na příkazové řádce, což je častý zdroj zmatků při přepínání mezi oběma formami.

Přihlašovací údaje můžeš také otestovat zevnitř existující relace, což se hodí, když jsi už připojen jako administrátor a chceš zkusit jiného uživatele:

javascript
db.getSiblingDB("admin").auth("shopapp", passwordPrompt())db.runCommand({ connectionStatus: 1 }).authInfo

auth() při úspěchu vrátí { ok: 1 }, jinak vyhodí chybu Authentication failed; databáze, na které ho voláš, je testovaný authSource. connectionStatus pak ukáže, které uživatele a role relace drží. Compass se chová stejně: v pokročilých volbách má záložka Authentication pole „Authentication Database“, což je authSource pod jiným jménem.

Pasti jednotlivých driverů#

Každý driver přijímá stejné URI, ale každý má vlastní způsob, jak vzít přihlašovací údaje jako samostatné volby, a každý má zvláštnost, která vyvolá kód 18.

Driver pro Node.js a Mongoose

Driver pro Node bere přihlašovací údaje buď v URI, nebo jako auth: { username, password }, přičemž authSource je volba nejvyšší úrovně. Samostatné volby se nedekódují, takže tam předej surové heslo.

javascript
const client = new MongoClient("mongodb://db.example.net:27017/shop", {  auth: { username: "shopapp", password: process.env.MONGO_PASSWORD },  authSource: "admin",});

Mongoose přidává volby dbName, user a pass. dbName mění, kterou databázi používají tvé modely, ale snadno se stane, že cesta v URI a dbName se neshodnou v tom, která databáze se myslí. Kdykoli používáš dbName, nastav authSource výslovně, místo abys spoléhal na jakoukoli výchozí hodnotu, a otázka už nevznikne.

PyMongo

PyMongo přijímá klíčové argumenty username, password a authSource, které se kódování úplně vyhnou. Pokud URI přece jen sestavuješ, urllib.parse.quote_plus je funkce, kterou používá dokumentace PyMongo. PyMongo hlásí selhání jako OperationFailure s code 18 a codeName: AuthenticationFailed v plné chybě; zachytávat místo toho ServerSelectionTimeoutError a logovat „auth failed“ je běžný způsob, jak schovat síťový problém za špatnou hlášku.

python
client = MongoClient(    "db.example.net", 27017,    username="shopapp",    password=os.environ["MONGO_PASSWORD"],    authSource="admin",)

Go, Java a PHP

Driver pro Go aplikuje URI a pak ti dovolí přepsat přihlašovací údaje pomocí options.Credential, jehož pole AuthSource má výchozí hodnotu stejně jako URI. Pokud zavoláš SetAuth s přihlašovacími údaji, které nechávají AuthSource prázdné, zkontroluj, co říká cesta v URI. V Javě MongoCredential.createScramSha256Credential(user, source, password) bere autentizační databázi jako druhý argument - předej tam ze zvyku aplikační databázi a klasickou chybu jsi právě zopakoval v novém jazyce. Knihovna pro PHP bere volby URI jako druhý argument new MongoDB\Client($uri, $uriOptions), kde lze username, password a authSource nastavit bez kódování.

Frameworky, které URI sestavují za tebe

Spring Boot, balíčky pro Laravel a podobné obaly často skládají URI z oddělených nastavení, jako jsou host, databáze, uživatel a heslo. Přečti si, jak to dělá ten tvůj. Několik z nich dává nakonfigurovanou databázi do cesty v URI, čímž se stane výchozím authSource, a nabízí zvláštní nastavení pro autentizační databázi, které se snadno přehlédne. Když je zapojen framework, zaloguj finální URI (maskované) jednou při startu; odtud je to obvykle zřejmé.

Reset hesla nebo obnovení přístupu#

Pokud uživatel existuje a ty jen neznáš jeho heslo, administrátor mu může nastavit nové, aniž by sáhl na cokoli jiného:

javascript
db.getSiblingDB("admin").changeUserPassword("shopapp", passwordPrompt())

Spusť to v databázi, do které uživatel patří - tady admin, nebo shop, pokud tam uživatel žije. Stávající spojení zůstanou autentizovaná, takže aplikaci poté restartuj, abys ověřil, že nové heslo funguje.

Na serveru, který provozuješ sám a kde jsi ztratil hesla všech administrátorů, je zdokumentovaná cesta restartovat mongod s vypnutou kontrolou přístupu (odstraň nebo zakomentuj security.authorization: enabled v mongod.conf), navázaný jen na 127.0.0.1, vytvořit nebo resetovat administrátora a pak autorizaci zase zapnout. Dělej to tak, aby byla databáze po celou dobu zvenčí nedostupná, protože po tyto minuty je každý, kdo se dokáže připojit, fakticky root.

Když neexistují žádní uživatelé, MongoDB povolí připojení z localhostu k vytvoření prvního - výjimka pro localhost. Jakmile existuje jakýkoli uživatel, výjimka se zavře. Proto čerstvá self-hosted instalace někdy přijímá neautentizované příkazy z vlastního serveru, ale odmítá je odkudkoli jinde.

U RE:NODE hostingu databází se server MongoDB vytvoří s přihlašovacími údaji vygenerovanými pro daný server a dosahuje se na něj přímo na hostu a portu uvedených u tarifu - žádná proxy před ním není, takže pasti s TLS a +srv výše nevstupují do hry, pokud si je sám nenastavíš. Pokud autentizace selhává, obvyklí podezřelí jsou ti z tohoto článku: databáze, do které uživatel patří, a způsob, jakým se heslo dostalo do tvého URI. O tom, jak panel nakládá s přihlašovacími údaji databází, se dočteš v sekci o databázích v návodech.

FAQ#

Proč MongoDB hlásí authentication failed, když je moje heslo správné?

Protože server hledal uživatele v jiné databázi, než ve které byl vytvořen. Driver prohledává databázi pojmenovanou v authSource a když chybí, použije databázi z cesty v URI. Uživatel vytvořený v admin a URI končící /shop selže pokaždé, dokud nepřidáš ?authSource=admin.

Co znamená 'bad auth : Authentication failed' v Atlasu?

Je to formulace Atlasu pro stejný chybový kód. Zkontroluj, že používáš databázového uživatele ze stránky Database Access, a ne své přihlášení do Atlasu, že heslo v URI je procentově zakódované a že jsi řetězec +srv nepřepsal na prostý seznam hostů bez přidání authSource=admin zpět.

Jak zjistím, do které databáze uživatel MongoDB patří?

Připoj se jako administrátor a spusť db.adminCommand({ usersInfo: { forAllDBs: true } }). Každý záznam má pole user a db; hodnota db je to, co patří do authSource. Bez administrátorského přístupu ukáže řádek v logu serveru k neúspěšnému pokusu, na kterou databázi se klient ptal.

Musím heslo URL-kódovat, když ho předávám zvlášť?

Ne, a nesmíš. Kódování se týká jen URI. Volby driveru jako password a výzva -p v mongosh berou surové heslo; zakódovaná hodnota tam se odešle s nedotčenými sekvencemi % a selže.

Je authSource totéž co databáze, kterou chci používat?

Jen když byl uživatel vytvořen v té databázi. Databáze v cestě URI je místo, kam míří tvoje dotazy ve výchozím stavu; authSource je místo, kde server hledá tvůj účet. Často se liší, což je celý důvod, proč tato volba existuje.

Můžu použít mongodb+srv s jedním serverem, který si pronajímám?

Jen pokud pro něj někdo vytvořil SRV záznamy, a jeden pronajatý server je obvykle nemá. Použij mongodb:// s hostem a portem. Pokud chceš čitelné jméno, stačí záznam A ukazující na adresu.