mongodb / mongo-php-driver

The Official MongoDB PHP driver
https://pecl.php.net/package/mongodb
Apache License 2.0
889 stars 204 forks source link

MongoDB\Driver\Exception\AuthenticationException with no credentials? #966

Closed klickers closed 4 years ago

klickers commented 5 years ago

Description

I'm trying to connect to MongoDB through PHP, but I get the error: "MongoDB\Driver\Exception\AuthenticationException Authentication failed." I don't have a username or password set up, so I'm pretty confused. I can connect to MongoDB through shell and compass, both without entering any username/password. I already tried looking up the issue in several different places (stackoverflow, mongodb-user groups.google.com, etc.) for several days but I can't find any help so I decided to post an issue here. I'm using the Yii2 framework but I don't think this problem is related to that.

Environment

Windows 10 (64-bit) PHP version 7.2.2 on XAMPP MongoDB 4.0.6 Mongo PHP Driver 1.6.0alpha1 for PHP 7.2 Threaded x86

phpinfo(): MongoDB support enabled
MongoDB extension version 1.6.0alpha1
MongoDB extension stability alpha
libbson bundled version 1.13.0
libmongoc bundled version 1.13.0
libmongoc SSL enabled
libmongoc SSL library OpenSSL
libmongoc crypto enabled
libmongoc crypto library libcrypto
libmongoc crypto system profile disabled
libmongoc SASL enabled
libmongoc ICU disabled
libmongoc compression disabled

Expected and Actual Behavior

Expected connection to MongoDB, but resulted in error thrown.

I would include more info if I knew what to include. This is my first time working with MongoDB (and NoSQL in general) so I'm pretty unknowledgeable in this area.

Thanks in advance. :)

jmikola commented 5 years ago

We'll need at least two things to help make sense of this issue:

  1. A backtrace of the exception. You can use Exception::getTrace() to capture this and then var_dump() it to share the raw output as a code snippet in a GitHub comment. That should also include any constructs/methods and their arguments, which should provide context for reproducing the problem.

  2. The original arguments used to construct MongoDB\Driver\Manager. This is the root connection option of the driver and, as noted in the constructor, it does not perform any IO on its own. The authentication exception will likely point back to the first database operation performed by your application, but we'll also want to see how the Manager was constructed. If you're using the mongodb/mongodb Composer package atop the low-level driver, note that the MongoDB\Client object basically wraps the driver's Manager object (most of the Client args are passed down to the driver). This may require some investigation on your end if you're relying on some Yii2 integration to construct the driver or library objects for you.

Lastly, I'll note that you're using version 1.6.0alpha1 of the driver. The most recent stable release is 1.5.3. I don't think that's relevant to your problem, as both versions happen to use the same versions of libmongoc (that will be bumped before the final 1.6.0 release); however, it's worth pointing this out and reminding you that any production application should stick to a stable release.

jmikola commented 5 years ago

Cross-referencing this with what I believe is a duplicate thread on the mongodb-user listserv: https://groups.google.com/d/msg/mongodb-user/nVyDeZqFVQM/FOOwkYZxAwAJ

klickers commented 5 years ago

Here's my code in the controller (MVC):

        $connection = new \yii\mongodb\Connection([
            'dsn' => 'mongodb://@localhost:27017',
        ]);
        $connection->open();
        $database = $connection->getDatabase('fsa');
        $collection = $database->getCollection('message');
        $collection->insert(['sender_id' => $id, 'content' => '<p>randomContent</p>']);
        $connection->close();

Code for the open() function - regarding the MongoDB\Driver\Manager - is at https://github.com/yiisoft/yii2-mongodb/blob/7f8d9862ac6c133d63afda171860ca9b61563d27/src/Connection.php#L344.

Stack Trace: (Full trace at https://app.box.com/s/u9rmr594nbrrec9yz9c1j6vh9fqoupbt - it was too long to copy-and-paste.)

An Error occurred while handling another error:
yii\web\HeadersAlreadySentException: Headers already sent in C:\xampp\htdocs\fsayii2\frontend\modules\dashboard\controllers\MessagesController.php on line 35. in C:\xampp\htdocs\fsayii2\vendor\yiisoft\yii2\web\Response.php:366
Stack trace:
#0 C:\xampp\htdocs\fsayii2\vendor\yiisoft\yii2\web\Response.php(339): yii\web\Response->sendHeaders()
#1 C:\xampp\htdocs\fsayii2\vendor\yiisoft\yii2\web\ErrorHandler.php(135): yii\web\Response->send()
#2 C:\xampp\htdocs\fsayii2\vendor\yiisoft\yii2\base\ErrorHandler.php(111): yii\web\ErrorHandler->renderException(Object(yii\mongodb\Exception))
#3 [internal function]: yii\base\ErrorHandler->handleException(Object(yii\mongodb\Exception))
#4 {main}

Previous exception:
MongoDB\Driver\Exception\AuthenticationException: Authentication failed. in C:\xampp\htdocs\fsayii2\vendor\yiisoft\yii2-mongodb\src\Command.php:240
Stack trace:
#0 C:\xampp\htdocs\fsayii2\vendor\yiisoft\yii2-mongodb\src\Command.php(240): MongoDB\Driver\Manager->executeBulkWrite('fsa.message', Object(MongoDB\Driver\BulkWrite), NULL)
#1 C:\xampp\htdocs\fsayii2\vendor\yiisoft\yii2-mongodb\src\Command.php(496): yii\mongodb\Command->executeBatch('message', Array)
#2 C:\xampp\htdocs\fsayii2\vendor\yiisoft\yii2-mongodb\src\Collection.php(266): yii\mongodb\Command->insert('message', Array, Array)
#3 C:\xampp\htdocs\fsayii2\frontend\modules\dashboard\controllers\MessagesController.php(44): yii\mongodb\Collection->insert(Array)
#4 [internal function]: app\modules\dashboard\controllers\MessagesController->actionInbox()
#5 C:\xampp\htdocs\fsayii2\vendor\yiisoft\yii2\base\InlineAction.php(57): call_user_func_array(Array, Array)
#6 C:\xampp\htdocs\fsayii2\vendor\yiisoft\yii2\base\Controller.php(157): yii\base\InlineAction->runWithParams(Array)
#7 C:\xampp\htdocs\fsayii2\vendor\yiisoft\yii2\base\Module.php(528): yii\base\Controller->runAction('inbox', Array)
#8 C:\xampp\htdocs\fsayii2\vendor\yiisoft\yii2\web\Application.php(103): yii\base\Module->runAction('dashboard/messa...', Array)
#9 C:\xampp\htdocs\fsayii2\vendor\yiisoft\yii2\base\Application.php(386): yii\web\Application->handleRequest(Object(yii\web\Request))
#10 C:\xampp\htdocs\fsayii2\frontend\web\index.php(17): yii\base\Application->run()
#11 {main}

Next yii\mongodb\Exception: Authentication failed. in C:\xampp\htdocs\fsayii2\vendor\yiisoft\yii2-mongodb\src\Command.php:245
Stack trace:
#0 C:\xampp\htdocs\fsayii2\vendor\yiisoft\yii2-mongodb\src\Command.php(496): yii\mongodb\Command->executeBatch('message', Array)
#1 C:\xampp\htdocs\fsayii2\vendor\yiisoft\yii2-mongodb\src\Collection.php(266): yii\mongodb\Command->insert('message', Array, Array)
#2 C:\xampp\htdocs\fsayii2\frontend\modules\dashboard\controllers\MessagesController.php(44): yii\mongodb\Collection->insert(Array)
#3 [internal function]: app\modules\dashboard\controllers\MessagesController->actionInbox()
#4 C:\xampp\htdocs\fsayii2\vendor\yiisoft\yii2\base\InlineAction.php(57): call_user_func_array(Array, Array)
#5 C:\xampp\htdocs\fsayii2\vendor\yiisoft\yii2\base\Controller.php(157): yii\base\InlineAction->runWithParams(Array)
#6 C:\xampp\htdocs\fsayii2\vendor\yiisoft\yii2\base\Module.php(528): yii\base\Controller->runAction('inbox', Array)
#7 C:\xampp\htdocs\fsayii2\vendor\yiisoft\yii2\web\Application.php(103): yii\base\Module->runAction('dashboard/messa...', Array)
#8 C:\xampp\htdocs\fsayii2\vendor\yiisoft\yii2\base\Application.php(386): yii\web\Application->handleRequest(Object(yii\web\Request))
#9 C:\xampp\htdocs\fsayii2\frontend\web\index.php(17): yii\base\Application->run()
#10 {main}

I have switched to the stable version and can't remember why I used the alpha in the beginning. Do I need to download any extra libraries from libmongoc or libbson, or are they already downloaded with the driver?

klickers commented 5 years ago

It's something on the Yii end, not the php driver. I discovered that if I executed commands without the Yii mongodb extension (but with the php driver) things work fine. So I'm going to close this issue for now.

jmikola commented 5 years ago

To answer your previous question, libmongoc and libbson are bundled with the PECL extension and statically compiled by default. Windows users should only need the DLL file available for download on the PECL website. Linux and macOS users have the option of using libmongoc/libbson as system libraries via extra configure options, which are discussed in the manual install docs.

I'm not familiar with the yii2-mongodb package, but I believe the exception originates from yii\mongodb\Command::executeBatch(). If we look at the PHP driver, we can see that AuthenticationException is only thrown for a very particular libmongoc error domain and code: MONGOC_ERROR_CLIENT_AUTHENTICATE (see: phongo_exception_from_mongoc_domain()). There is some more context for this error code in the libmongoc error reporting docs.

This error code appears in various places within libmongoc, but it more often than not comes with a verbose error message generated by libmongoc. The string "Authentication failed." does not appear in either libmongoc or the PHP driver, so I assume it originates from the server. I found one occurrence of it in a constant definition within authorization_manager.cpp, which is references in two places:

In both of those cases, there should be an accompanying message in the server log. Cross-referencing with that log message should reveal exactly which code point was reached. I'm not very familiar with the server internals, but I believe both of these paths would only be reached if a driver was authenticating with the server.

Coming back to libmongoc, I looked up two places where the driver might actually use the MONGOC_ERROR_CLIENT_AUTHENTICATE error code with a server-side error string:

I think this is enough to conclude that the driver and server were certainly engaging in some authentication exchange. I believe there is also a clue in your connection string:

$connection = new \yii\mongodb\Connection([
    'dsn' => 'mongodb://@localhost:27017',
]);

The @ symbol in the connection string is used to differentiate username/password credentials from a list of hosts. This syntax is also discussed in the connection string spec.

When this string is parsed by mongoc_uri_parse_before_slash(), libmongoc finds an empty string for the username and no password due to the absence of a : delimiter. Later, in mongoc_cluster_init(), libmongoc then concludes that authentication is required because username is not null. An immediate fix may be to remove @ from your connection string unless there was a valid excuse for it being there, which I might have missed.

Beyond that, I will open a libmongoc ticket to see if we can add some validation for this edge case and perhaps raise an error during URI parsing if the driver finds an empty string for the username.


One other tool that might have provided some context here (at the expense of a lot of output) is the mongodb.debug INI option. This allows the PHP driver to dump libmongoc's debug trace logs to a file or stderr. I'm not sure we need it at this point, as I think the problem has been diagnosed, but it would be helpful to keep this in mind for future use if you encounter another issue where you suspect the driver or libmongoc might be misbehaving.

jmikola commented 5 years ago

Opened:

lindelius commented 5 years ago

I'm not sure if there is any valid use case where an empty username would be accepted by the server. If not, perhaps we can consider adding some validation around this to raise a client-side error during URI parsing

If it turns out that there isn't any valid use cases for empty credentials, it would be great if the driver would just skip the username and password fields (rather than throwing an exception, for example). That's what we are doing right now, although in our application code.

if (empty($uriOptions['username'])) {
    unset($uriOptions['username']);
    unset($uriOptions['password']);
}

The "replicaSet" option has the same problem, by the way, so if you do end up implementing a fix for the credential fields (and unless there is a valid use case for empty replica set names, of course), it would be great if you could implement the same fix for that field, as well.

klickers commented 5 years ago

Edit After I created a user in the admin database and accessed the other DB with the user/pwd credentials, the Yii2 extension worked also. It looks like the extension throws an error if there are no credentials when accessing the database.

jmikola commented 5 years ago

After I created a user in the admin database and accessed the other DB with the user/pwd credentials, the Yii2 extension worked also. It looks like the extension throws an error if there are no credentials when accessing the database.

@CathChen003: If you've configured the server to require authentication by creating users and assigning roles, then an exception would be expected when failing to provide credentials in the driver or any other client (e.g. Compass, shell). Your original post indicated that you were not intending to use authentication, which is why I concluded that the @ symbol in the connection string was a mistake.

If it turns out that there isn't any valid use cases for empty credentials, it would be great if the driver would just skip the username and password fields (rather than throwing an exception, for example)

@lindelius: I understand the desire to simply ignore an empty string, but I think raising an error would be more consistent if we conclude that an empty string is an invalid username. Likewise with an empty "replicaSet" option, raising an exception would clearly highlight to the user that their connection string or URI options are malformed.

I'm not sure why your application code would need to work around this. Are you assigning the URI option directly from an environment variable, which may be empty? Alternatively, if this is coming from a library integration (e.g. Symfony bundle parsing some configuration file), perhaps the integration can be improved to detect when an option should be unset vs. an empty string.

In the meantime, I created PHPC-1347 to ensure we don't lose track of your suggestion for the replicaSet option.

klickers commented 5 years ago

@jmikola I don't recall requiring authentication while setting up MongoDB. I can connect fine through Compass or shell without providing user/pwd details...

lindelius commented 5 years ago

@jmikola Yupp, we are using Laravel so the values initially come from env files that are then directly assigned to a config file that consist of a PHP array with a specific, pre-defined structure. We would either have to add more obtrusive code to the beginning of those config files and dynamically change the structure of the array (i.e. adding the URI option fields to the config array based on whether the env values are empty), or keep the current code in the place where we construct the Manager instance.

https://github.com/laravel/laravel/blob/master/.env.example https://github.com/laravel/laravel/blob/master/config/database.php

This is a relatively new issue, by the way. I'm fairly sure it started when we upgraded to version 1.5. Before we upgraded, the driver (or the server, I'm not sure which server version we were running then or whether we upgraded our replica set at the same time) skipped these fields when they contained empty values.

The current functionality isn't so much of an issue (our current "work-around" works perfectly fine, and it's only like 5 rows of code), it's just not ideal.

jmikola commented 5 years ago

I don't recall requiring authentication while setting up MongoDB. I can connect fine through Compass or shell without providing user/pwd details...

@CathChen003: I think we're misunderstanding each other. In your previous comment, you stated that you "created a user in the admin database and accessed the other DB with the user/pwd credentials."

Enable Auth walks through the process for configure MongoDB to use authentication. after creating the first administrator user, the mongod process should be restarted with the --auth option (or security.authorization config file option) to ensure that future connections require authentication.

If you only created a user on the server without requiring authentication, then I presume the server will allow clients to authenticate if they attempt to do so; however, the server will not require authentication and clients that choose not to do so will be able to connect and access the database(s) as they wish. That may explain why Compass and the shell continued to work without authentication, even after you created a user.

I'm going to reiterate that the proper solution is to remove the lone @ symbol from your connection string, as that was causing the PHP driver to attempt to authenticate with an empty username and password. If you want to enable authentication on the server, I would suggest following the manual page I linked above and then adjusting your connection string accordingly. IMO, there is no reason to create users in the admin database (as you seem to have done) unless you plan to actually require authentication on the server for all connections.

klickers commented 5 years ago

@jmikola OK, I understand now. Thanks!

jmikola commented 4 years ago

Closing, as both referenced JIRA issues have since been fixed and released.