Skip to content

Commit

Permalink
Support authentication mechanism negotiation
Browse files Browse the repository at this point in the history
...by adding `saslSupportedMechs: <db.user>` to the *hello* command. This will return an
additional field hello.saslSupportedMechs in its result. The order the server returns the
mechanisms in is used as an indicator for its preference
  • Loading branch information
thekid committed Aug 18, 2023
1 parent 85f0067 commit 88d768d
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 23 deletions.
19 changes: 19 additions & 0 deletions src/main/php/com/mongodb/Authentication.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
use com\mongodb\auth\{Mechanism, ScramSHA1, ScramSHA256};
use lang\IllegalArgumentException;

/** @test com.mongodb.unittest.AuthenticationTest */
abstract class Authentication {
const MECHANISMS= ['SCRAM-SHA-1', 'SCRAM-SHA-256'];

/**
* Returns an authentication by a given mechanism name
Expand All @@ -17,4 +19,21 @@ public static function mechanism(string $name): Mechanism {
default: throw new IllegalArgumentException('Unknown authentication mechanism '.$name);
}
}

/**
* Negotiates one of the supported authentication mechansim from a list
* of given mechanisms.
*
* @throws lang.IllegalArgumentException if none are supported
*/
public static function negotiate(array $mechanisms): Mechanism {
if ($supported= array_intersect($mechanisms, self::MECHANISMS)) {
return self::mechanism(current($supported));
}

throw new IllegalArgumentException(sprintf(
'None of the authentication mechanisms %s is supported',
implode(', ', $mechanisms)
));
}
}
4 changes: 3 additions & 1 deletion src/main/php/com/mongodb/auth/Mechanism.class.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php namespace com\mongodb\auth;

interface Mechanism {


/** @return string */
public function name();
}
3 changes: 3 additions & 0 deletions src/main/php/com/mongodb/auth/Scram.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ public function __construct() {
$this->nonce= function() { return base64_encode(random_bytes(24)); };
}

/** @return string */
public function name() { return static::MECHANISM; }

/**
* Use the given function to generate nonce values
*
Expand Down
48 changes: 32 additions & 16 deletions src/main/php/com/mongodb/io/Connection.class.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php namespace com\mongodb\io;

use com\mongodb\{AuthenticationFailed, Error};
use com\mongodb\{Authentication, AuthenticationFailed, Error};
use lang\Throwable;
use peer\{Socket, ProtocolException, ConnectException};
use util\Secret;
Expand Down Expand Up @@ -93,19 +93,32 @@ public function establish($options= [], $auth= null) {
}

// Send hello package and determine connection kind
// https://docs.mongodb.com/v4.4/reference/command/hello/
// https://www.mongodb.com/docs/manual/reference/command/hello/
$hello= [
'hello' => 1,
'client' => [
'application' => ['name' => $options['params']['appName'] ?? $_SERVER['argv'][0] ?? 'php'],
'driver' => ['name' => 'XP MongoDB Connectivity', 'version' => '1.0.0'],
'os' => ['name' => php_uname('s'), 'type' => PHP_OS, 'architecture' => php_uname('m'), 'version' => php_uname('r')]
]
];

// If the optional field saslSupportedMechs is specified, the command also returns
// an array of SASL mechanisms used to create the specified user's credentials.
if (isset($options['user'])) {
$user= urldecode($options['user']);
$pass= urldecode($options['pass']);
$authSource= $options['params']['authSource'] ?? (isset($options['path']) ? ltrim($options['path'], '/') : 'admin');
$hello['saslSupportedMechs']= "{$authSource}.{$user}";
} else {
$authSource= null;
}

try {
$reply= $this->send(
self::OP_QUERY,
"\x00\x00\x00\x00admin.\$cmd\x00\x00\x00\x00\x00\x01\x00\x00\x00",
[
'hello' => 1,
'client' => [
'application' => ['name' => $options['params']['appName'] ?? $_SERVER['argv'][0] ?? 'php'],
'driver' => ['name' => 'XP MongoDB Connectivity', 'version' => '1.0.0'],
'os' => ['name' => php_uname('s'), 'type' => PHP_OS, 'architecture' => php_uname('m'), 'version' => php_uname('r')]
]
]
$hello
);
} catch (ProtocolException $e) {
throw new ConnectException('Server handshake failed @ '.$this->address(), $e);
Expand Down Expand Up @@ -134,15 +147,18 @@ public function establish($options= [], $auth= null) {
$this->server= ['$kind' => $kind] + $document;

// Optionally, perform authentication
if (null === $auth) return;
if (null === $authSource) return;

try {
$conversation= $auth->conversation(
urldecode($options['user']),
urldecode($options['pass']),
$options['params']['authSource'] ?? (isset($options['path']) ? ltrim($options['path'], '/') : 'admin')
);
if ($auth) {
// Use this explicitely specified mechanism
} else if ($supported= $document['saslSupportedMechs'] ?? null) {
$auth= Authentication::negotiate($supported);
} else {
$auth= Authentication::mechanism(Authentication::MECHANISMS[0]);
}

$conversation= $auth->conversation($user, $pass, $authSource);
do {
$result= $this->message($conversation->current(), null);
if (0 === (int)$result['body']['ok']) {
Expand Down
12 changes: 7 additions & 5 deletions src/main/php/com/mongodb/io/Protocol.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
* @test com.mongodb.unittest.ReplicaSetTest
*/
class Protocol {
private $options, $auth;
private $options;
protected $auth= null;
protected $conn= [];
public $nodes= null;
public $readPreference;
Expand Down Expand Up @@ -91,10 +92,11 @@ public function __construct($arg, $options= [], $bson= null, $dns= null) {
}

$this->readPreference= ['mode' => $this->options['params']['readPreference'] ?? 'primary'];
$this->auth= isset($this->options['user'])
? Authentication::mechanism($this->options['params']['authMechanism'] ?? 'SCRAM-SHA-1')
: null
;

// Check if an authentication mechanism was explicitely selected
if ($mechanism= $this->options['params']['authMechanism'] ?? null) {
$this->auth= Authentication::mechanism($mechanism);
}
}

/** @return [:var] */
Expand Down
42 changes: 41 additions & 1 deletion src/test/php/com/mongodb/unittest/AuthenticationTest.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,52 @@
class AuthenticationTest {

#[Test]
public function cram_sha_1() {
public function sha1_is_default_mechanism() {
Assert::equals('SCRAM-SHA-1', Authentication::MECHANISMS[0]);
}

#[Test]
public function scram_sha_1() {
Assert::instance(Mechanism::class, Authentication::mechanism('SCRAM-SHA-1'));
}

#[Test]
public function scram_sha_256() {
Assert::instance(Mechanism::class, Authentication::mechanism('SCRAM-SHA-256'));
}

#[Test, Expect(IllegalArgumentException::class)]
public function unknown() {
Authentication::mechanism('unknown');
}

#[Test]
public function negotiate_sha1() {
Assert::equals('SCRAM-SHA-1', Authentication::negotiate(['SCRAM-SHA-1'])->name());
}

#[Test]
public function negotiate_sha1_preferred() {
Assert::equals('SCRAM-SHA-1', Authentication::negotiate(['SCRAM-SHA-1', 'SCRAM-SHA-256'])->name());
}

#[Test]
public function negotiate_sha256_preferred() {
Assert::equals('SCRAM-SHA-256', Authentication::negotiate(['SCRAM-SHA-256', 'SCRAM-SHA-1'])->name());
}

#[Test]
public function negotiate_unsupported_plain_preferred() {
Assert::equals('SCRAM-SHA-1', Authentication::negotiate(['PLAIN', 'SCRAM-SHA-1'])->name());
}

#[Test, Expect(IllegalArgumentException::class)]
public function negotiate_none_supported() {
Authentication::negotiate(['PLAIN', 'AWS'])->name();
}

#[Test, Expect(IllegalArgumentException::class)]
public function negotiate_empty() {
Authentication::negotiate([])->name();
}
}

0 comments on commit 88d768d

Please sign in to comment.