diff --git a/test/Adapter/DbTableTest.php b/test/Adapter/DbTableTest.php new file mode 100644 index 0000000..e58b47b --- /dev/null +++ b/test/Adapter/DbTableTest.php @@ -0,0 +1,372 @@ +markTestSkipped('Tests are not enabled in TestConfiguration.php'); + return; + } elseif (!extension_loaded('pdo')) { + $this->markTestSkipped('PDO extension is not loaded'); + return; + } elseif (!in_array('sqlite', \PDO::getAvailableDrivers())) { + $this->markTestSkipped('SQLite PDO driver is not available'); + return; + } + + $this->_setupDbAdapter(); + $this->_setupAuthAdapter(); + } + + public function tearDown() + { + $this->_adapter = null; + if ($this->_db instanceof DbAdapter) { + $this->_db->query('DROP TABLE [users]'); + } + $this->_db = null; + } + + /** + * Ensures expected behavior for authentication success + */ + public function testAuthenticateSuccess() + { + $this->_adapter->setIdentity('my_username'); + $this->_adapter->setCredential('my_password'); + $result = $this->_adapter->authenticate(); + $this->assertTrue($result->isValid()); + } + + /** + * Ensures expected behavior for authentication success + */ + public function testAuthenticateSuccessWithTreatment() + { + $this->_adapter = new Adapter\DbTable($this->_db, 'users', 'username', 'password', '?'); + $this->_adapter->setIdentity('my_username'); + $this->_adapter->setCredential('my_password'); + $result = $this->_adapter->authenticate(); + $this->assertTrue($result->isValid()); + } + + /** + * Ensures expected behavior for for authentication failure + * reason: Identity not found. + */ + public function testAuthenticateFailureIdentityNotFound() + { + $this->_adapter->setIdentity('non_existent_username'); + $this->_adapter->setCredential('my_password'); + + $result = $this->_adapter->authenticate(); + $this->assertEquals(Authentication\Result::FAILURE_IDENTITY_NOT_FOUND, $result->getCode()); + } + + /** + * Ensures expected behavior for for authentication failure + * reason: Identity not found. + */ + public function testAuthenticateFailureIdentityAmbiguous() + { + $sqlInsert = 'INSERT INTO users (username, password, real_name) VALUES ("my_username", "my_password", "My Real Name")'; + $this->_db->query($sqlInsert, DbAdapter::QUERY_MODE_EXECUTE); + + $this->_adapter->setIdentity('my_username'); + $this->_adapter->setCredential('my_password'); + + $result = $this->_adapter->authenticate(); + $this->assertEquals(Authentication\Result::FAILURE_IDENTITY_AMBIGUOUS, $result->getCode()); + } + + /** + * Ensures expected behavior for authentication failure because of a bad password + */ + public function testAuthenticateFailureInvalidCredential() + { + $this->_adapter->setIdentity('my_username'); + $this->_adapter->setCredential('my_password_bad'); + $result = $this->_adapter->authenticate(); + $this->assertFalse($result->isValid()); + } + + /** + * Ensures that getResultRowObject() works for successful authentication + */ + public function testGetResultRow() + { + $this->_adapter->setIdentity('my_username'); + $this->_adapter->setCredential('my_password'); + $this->_adapter->authenticate(); + $resultRow = $this->_adapter->getResultRowObject(); + $this->assertEquals($resultRow->username, 'my_username'); + } + + /** + * Ensure that ResultRowObject returns only what told to be included + */ + public function testGetSpecificResultRow() + { + $this->_adapter->setIdentity('my_username'); + $this->_adapter->setCredential('my_password'); + $this->_adapter->authenticate(); + $resultRow = $this->_adapter->getResultRowObject(array('username', 'real_name')); + $this->assertEquals('O:8:"stdClass":2:{s:8:"username";s:11:"my_username";s:9:"real_name";s:12:"My Real Name";}', + serialize($resultRow)); + } + + /** + * Ensure that ResultRowObject returns an object has specific omissions + */ + public function testGetOmittedResultRow() + { + $this->_adapter->setIdentity('my_username'); + $this->_adapter->setCredential('my_password'); + $this->_adapter->authenticate(); + $resultRow = $this->_adapter->getResultRowObject(null, 'password'); + $this->assertEquals('O:8:"stdClass":3:{s:2:"id";s:1:"1";s:8:"username";s:11:"my_username";s:9:"real_name";s:12:"My Real Name";}', + serialize($resultRow)); + } + + /** + * @group ZF-5957 + */ + public function testAdapterCanReturnDbSelectObject() + { + $this->assertTrue($this->_adapter->getDbSelect() instanceof DBSelect); + } + + /** + * @group ZF-5957 + */ + public function testAdapterCanUseModifiedDbSelectObject() + { + $select = $this->_adapter->getDbSelect(); + $select->where('1 = 0'); + $this->_adapter->setIdentity('my_username'); + $this->_adapter->setCredential('my_password'); + + $result = $this->_adapter->authenticate(); + $this->assertEquals(Authentication\Result::FAILURE_IDENTITY_NOT_FOUND, $result->getCode()); + } + + /** + * @group ZF-5957 + */ + public function testAdapterReturnsASelectObjectWithoutAuthTimeModificationsAfterAuth() + { + $select = $this->_adapter->getDbSelect(); + $select->where('1 = 1'); + $this->_adapter->setIdentity('my_username'); + $this->_adapter->setCredential('my_password'); + $this->_adapter->authenticate(); + $selectAfterAuth = $this->_adapter->getDbSelect(); + $whereParts = $selectAfterAuth->where->getPredicates(); + $this->assertEquals(1, count($whereParts)); + + $lastWherePart = array_pop($whereParts); + $expressionData = $lastWherePart[1]->getExpressionData(); + $this->assertEquals('1 = 1', $expressionData[0][0]); + } + + /** + * Ensure that exceptions are caught + */ + public function testCatchExceptionNoTable() + { + $this->setExpectedException('Zend\Authentication\Adapter\Exception\RuntimeException', + 'A table must be supplied for'); + $adapter = new Adapter\DbTable($this->_db); + $adapter->authenticate(); + } + + /** + * Ensure that exceptions are caught + */ + public function testCatchExceptionNoIdentityColumn() + { + $this->setExpectedException('Zend\Authentication\Adapter\Exception\RuntimeException', + 'An identity column must be supplied for the'); + $adapter = new Adapter\DbTable($this->_db, 'users'); + $adapter->authenticate(); + } + + /** + * Ensure that exceptions are caught + */ + public function testCatchExceptionNoCredentialColumn() + { + $this->setExpectedException('Zend\Authentication\Adapter\Exception\RuntimeException', + 'A credential column must be supplied'); + $adapter = new Adapter\DbTable($this->_db, 'users', 'username'); + $adapter->authenticate(); + } + + /** + * Ensure that exceptions are caught + */ + public function testCatchExceptionNoIdentity() + { + $this->setExpectedException('Zend\Authentication\Adapter\Exception\RuntimeException', + 'A value for the identity was not provided prior'); + $this->_adapter->authenticate(); + } + + /** + * Ensure that exceptions are caught + */ + public function testCatchExceptionNoCredential() + { + $this->setExpectedException('Zend\Authentication\Adapter\Exception\RuntimeException', + 'A credential value was not provided prior'); + $this->_adapter->setIdentity('my_username'); + $this->_adapter->authenticate(); + } + + /** + * Ensure that exceptions are caught + */ + public function testCatchExceptionBadSql() + { + $this->setExpectedException('Zend\Authentication\Adapter\Exception\RuntimeException', + 'The supplied parameters to'); + $this->_adapter->setTableName('bad_table_name'); + $this->_adapter->setIdentity('value'); + $this->_adapter->setCredential('value'); + $this->_adapter->authenticate(); + } + + /** + * Test to see same usernames with different passwords can not authenticate + * when flag is not set. This is the current state of + * Zend_Auth_Adapter_DbTable (up to ZF 1.10.6) + * + * @group ZF-7289 + */ + public function testEqualUsernamesDifferentPasswordShouldNotAuthenticateWhenFlagIsNotSet() + { + $sqlInsert = 'INSERT INTO users (username, password, real_name) ' + . 'VALUES ("my_username", "my_otherpass", "Test user 2")'; + $this->_db->query($sqlInsert, DbAdapter::QUERY_MODE_EXECUTE); + + // test if user 1 can authenticate + $this->_adapter->setIdentity('my_username') + ->setCredential('my_password'); + $result = $this->_adapter->authenticate(); + $this->assertTrue(in_array('More than one record matches the supplied identity.', + $result->getMessages())); + $this->assertFalse($result->isValid()); + } + + /** + * Test to see same usernames with different passwords can authenticate when + * a flag is set + * + * @group ZF-7289 + */ + public function testEqualUsernamesDifferentPasswordShouldAuthenticateWhenFlagIsSet() + { + $sqlInsert = 'INSERT INTO users (username, password, real_name) ' + . 'VALUES ("my_username", "my_otherpass", "Test user 2")'; + $this->_db->query($sqlInsert, DbAdapter::QUERY_MODE_EXECUTE); + + // test if user 1 can authenticate + $this->_adapter->setIdentity('my_username') + ->setCredential('my_password') + ->setAmbiguityIdentity(true); + $result = $this->_adapter->authenticate(); + $this->assertFalse(in_array('More than one record matches the supplied identity.', + $result->getMessages())); + $this->assertTrue($result->isValid()); + $this->assertEquals('my_username', $result->getIdentity()); + + $this->_adapter = null; + $this->_setupAuthAdapter(); + + // test if user 2 can authenticate + $this->_adapter->setIdentity('my_username') + ->setCredential('my_otherpass') + ->setAmbiguityIdentity(true); + $result2 = $this->_adapter->authenticate(); + $this->assertFalse(in_array('More than one record matches the supplied identity.', + $result->getMessages())); + $this->assertTrue($result2->isValid()); + $this->assertEquals('my_username', $result2->getIdentity()); + } + + + protected function _setupDbAdapter($optionalParams = array()) + { + $params = array('driver' => 'pdo_sqlite', + 'dbname' => TESTS_ZEND_AUTH_ADAPTER_DBTABLE_PDO_SQLITE_DATABASE); + + if (!empty($optionalParams)) { + $params['options'] = $optionalParams; + } + + $this->_db = new DbAdapter($params); + + $sqlCreate = 'CREATE TABLE IF NOT EXISTS [users] ( ' + . '[id] INTEGER NOT NULL PRIMARY KEY, ' + . '[username] VARCHAR(50) NOT NULL, ' + . '[password] VARCHAR(32) NULL, ' + . '[real_name] VARCHAR(150) NULL)'; + $this->_db->query($sqlCreate, DbAdapter::QUERY_MODE_EXECUTE); + + $sqlDelete = 'DELETE FROM users'; + $this->_db->query($sqlDelete, DbAdapter::QUERY_MODE_EXECUTE); + + $sqlInsert = 'INSERT INTO users (username, password, real_name) ' + . 'VALUES ("my_username", "my_password", "My Real Name")'; + $this->_db->query($sqlInsert, DbAdapter::QUERY_MODE_EXECUTE); + } + + protected function _setupAuthAdapter() + { + $this->_adapter = new Adapter\DbTable($this->_db, 'users', 'username', 'password'); + } + +} + diff --git a/test/Adapter/DigestTest.php b/test/Adapter/DigestTest.php new file mode 100644 index 0000000..5352c9d --- /dev/null +++ b/test/Adapter/DigestTest.php @@ -0,0 +1,229 @@ +_filesPath = __DIR__ . '/TestAsset/Digest'; + } + + /** + * Ensures that the adapter throws an exception when authentication is attempted before + * setting a required option + * + * @return void + */ + public function testOptionRequiredException() + { + $adapter = new Adapter\Digest(); + try { + $adapter->authenticate(); + $this->fail('Expected Zend_Auth_Adapter_Exception not thrown upon authentication attempt before setting ' + . 'a required option'); + } catch (Adapter\Exception\ExceptionInterface $e) { + $this->assertContains('must be set before authentication', $e->getMessage()); + } + } + + /** + * Ensures that an exception is thrown upon authenticating against a nonexistent file + * + * @return void + */ + public function testFileNonExistentException() + { + $adapter = new Adapter\Digest('nonexistent', 'realm', 'username', 'password'); + try { + $adapter->authenticate(); + $this->fail('Expected Zend_Auth_Adapter_Exception not thrown upon authenticating against nonexistent ' + . 'file'); + } catch (Adapter\Exception\ExceptionInterface $e) { + $this->assertContains('Cannot open', $e->getMessage()); + } + } + + /** + * Ensures expected behavior upon realm not found for existing user + * + * @return void + */ + public function testUserExistsRealmNonexistent() + { + $filename = "$this->_filesPath/htdigest.1"; + $realm = 'Nonexistent Realm'; + $username = 'someUser'; + $password = 'somePassword'; + + $adapter = new Adapter\Digest($filename, $realm, $username, $password); + + $result = $adapter->authenticate(); + + $this->assertFalse($result->isValid()); + + $messages = $result->getMessages(); + $this->assertEquals(1, count($messages)); + $this->assertEquals($result->getCode(), Authentication\Result::FAILURE_IDENTITY_NOT_FOUND); + $this->assertContains('combination not found', $messages[0]); + + $identity = $result->getIdentity(); + $this->assertEquals($identity['realm'], $realm); + $this->assertEquals($identity['username'], $username); + } + + /** + * Ensures expected behavior upon user not found in existing realm + * + * @return void + */ + public function testUserNonexistentRealmExists() + { + $filename = "$this->_filesPath/htdigest.1"; + $realm = 'Some Realm'; + $username = 'nonexistentUser'; + $password = 'somePassword'; + + $adapter = new Adapter\Digest($filename, $realm, $username, $password); + + $result = $adapter->authenticate(); + + $this->assertFalse($result->isValid()); + $this->assertEquals($result->getCode(), Authentication\Result::FAILURE_IDENTITY_NOT_FOUND); + + $messages = $result->getMessages(); + $this->assertEquals(1, count($messages)); + $this->assertContains('combination not found', $messages[0]); + + $identity = $result->getIdentity(); + $this->assertEquals($identity['realm'], $realm); + $this->assertEquals($identity['username'], $username); + } + + /** + * Ensures expected behavior upon incorrect password + * + * @return void + */ + public function testIncorrectPassword() + { + $filename = "$this->_filesPath/htdigest.1"; + $realm = 'Some Realm'; + $username = 'someUser'; + $password = 'incorrectPassword'; + + $adapter = new Adapter\Digest($filename, $realm, $username, $password); + + $result = $adapter->authenticate(); + + $this->assertFalse($result->isValid()); + $this->assertEquals($result->getCode(), Authentication\Result::FAILURE_CREDENTIAL_INVALID); + + $messages = $result->getMessages(); + $this->assertEquals(1, count($messages)); + $this->assertContains('Password incorrect', $messages[0]); + + $identity = $result->getIdentity(); + $this->assertEquals($identity['realm'], $realm); + $this->assertEquals($identity['username'], $username); + } + + /** + * Ensures that successful authentication works as expected + * + * @return void + */ + public function testAuthenticationSuccess() + { + $filename = "$this->_filesPath/htdigest.1"; + $realm = 'Some Realm'; + $username = 'someUser'; + $password = 'somePassword'; + + $adapter = new Adapter\Digest($filename, $realm, $username, $password); + + $result = $adapter->authenticate(); + + $this->assertTrue($result->isValid()); + $this->assertEquals($result->getCode(), Authentication\Result::SUCCESS); + + $this->assertEquals(array(), $result->getMessages()); + + $identity = $result->getIdentity(); + $this->assertEquals($identity['realm'], $realm); + $this->assertEquals($identity['username'], $username); + } + + /** + * Ensures that getFilename() returns expected default value + * + * @return void + */ + public function testGetFilename() + { + $adapter = new Adapter\Digest(); + $this->assertEquals(null, $adapter->getFilename()); + } + + /** + * Ensures that getRealm() returns expected default value + * + * @return void + */ + public function testGetRealm() + { + $adapter = new Adapter\Digest(); + $this->assertEquals(null, $adapter->getRealm()); + } + + /** + * Ensures that getUsername() returns expected default value + * + * @return void + */ + public function testGetUsername() + { + $adapter = new Adapter\Digest(); + $this->assertEquals(null, $adapter->getUsername()); + } + + /** + * Ensures that getPassword() returns expected default value + * + * @return void + */ + public function testGetPassword() + { + $adapter = new Adapter\Digest(); + $this->assertEquals(null, $adapter->getPassword()); + } +} diff --git a/test/Adapter/Http/AuthTest.php b/test/Adapter/Http/AuthTest.php new file mode 100644 index 0000000..a2672f4 --- /dev/null +++ b/test/Adapter/Http/AuthTest.php @@ -0,0 +1,507 @@ +_filesPath = __DIR__ . '/TestAsset'; + $this->_basicResolver = new Http\FileResolver("{$this->_filesPath}/htbasic.1"); + $this->_digestResolver = new Http\FileResolver("{$this->_filesPath}/htdigest.3"); + $this->_basicConfig = array( + 'accept_schemes' => 'basic', + 'realm' => 'Test Realm' + ); + $this->_digestConfig = array( + 'accept_schemes' => 'digest', + 'realm' => 'Test Realm', + 'digest_domains' => '/ http://localhost/', + 'nonce_timeout' => 300 + ); + $this->_bothConfig = array( + 'accept_schemes' => 'basic digest', + 'realm' => 'Test Realm', + 'digest_domains' => '/ http://localhost/', + 'nonce_timeout' => 300 + ); + } + + public function testBasicChallenge() + { + // Trying to authenticate without sending an Authorization header + // should result in a 401 reply with a Www-Authenticate header, and a + // false result. + + // The expected Basic Www-Authenticate header value + $basic = array( + 'type' => 'Basic ', + 'realm' => 'realm="' . $this->_bothConfig['realm'] . '"', + ); + + $data = $this->_doAuth('', 'basic'); + $this->_checkUnauthorized($data, $basic); + } + + public function testDigestChallenge() + { + // Trying to authenticate without sending an Authorization header + // should result in a 401 reply with a Www-Authenticate header, and a + // false result. + + // The expected Digest Www-Authenticate header value + $digest = $this->_digestChallenge(); + + $data = $this->_doAuth('', 'digest'); + $this->_checkUnauthorized($data, $digest); + } + + public function testBothChallenges() + { + // Trying to authenticate without sending an Authorization header + // should result in a 401 reply with at least one Www-Authenticate + // header, and a false result. + + $result = $status = $headers = null; + $data = $this->_doAuth('', 'both'); + extract($data); // $result, $status, $headers + + // The expected Www-Authenticate header values + $basic = 'Basic realm="' . $this->_bothConfig['realm'] . '"'; + $digest = $this->_digestChallenge(); + + // Make sure the result is false + $this->assertInstanceOf('Zend\\Authentication\\Result', $result); + $this->assertFalse($result->isValid()); + + // Verify the status code and the presence of both challenges + $this->assertEquals(401, $status); + $this->assertTrue($headers->has('Www-Authenticate')); + $wwwAuthenticate = $headers->get('Www-Authenticate'); + $this->assertEquals(2, count($wwwAuthenticate)); + + // Check to see if the expected challenges match the actual + $basicFound = $digestFound = false; + foreach ($wwwAuthenticate as $header) { + $value = $header->getFieldValue(); + if (preg_match('/^Basic/', $value)) { + $basicFound = true; + } + if (preg_match('/^Digest/', $value)) { + $digestFound = true; + } + } + $this->assertTrue($basicFound); + $this->assertTrue($digestFound); + } + + public function testBasicAuthValidCreds() + { + // Attempt Basic Authentication with a valid username and password + + $data = $this->_doAuth('Basic ' . base64_encode('Bryce:ThisIsNotMyPassword'), 'basic'); + $this->_checkOK($data); + } + + public function testBasicAuthBadCreds() + { + // Ensure that credentials containing invalid characters are treated as + // a bad username or password. + + // The expected Basic Www-Authenticate header value + $basic = array( + 'type' => 'Basic ', + 'realm' => 'realm="' . $this->_basicConfig['realm'] . '"', + ); + + $data = $this->_doAuth('Basic ' . base64_encode("Bad\tChars:In:Creds"), 'basic'); + $this->_checkUnauthorized($data, $basic); + } + + public function testBasicAuthBadUser() + { + // Attempt Basic Authentication with a nonexistant username and + // password + + // The expected Basic Www-Authenticate header value + $basic = array( + 'type' => 'Basic ', + 'realm' => 'realm="' . $this->_basicConfig['realm'] . '"', + ); + + $data = $this->_doAuth('Basic ' . base64_encode('Nobody:NotValid'), 'basic'); + $this->_checkUnauthorized($data, $basic); + } + + public function testBasicAuthBadPassword() + { + // Attempt Basic Authentication with a valid username, but invalid + // password + + // The expected Basic Www-Authenticate header value + $basic = array( + 'type' => 'Basic ', + 'realm' => 'realm="' . $this->_basicConfig['realm'] . '"', + ); + + $data = $this->_doAuth('Basic ' . base64_encode('Bryce:Invalid'), 'basic'); + $this->_checkUnauthorized($data, $basic); + } + + public function testDigestAuthValidCreds() + { + // Attempt Digest Authentication with a valid username and password + + $data = $this->_doAuth($this->_digestReply('Bryce', 'ThisIsNotMyPassword'), 'digest'); + $this->_checkOK($data); + } + + public function testDigestAuthDefaultAlgo() + { + // If the client omits the aglorithm argument, it should default to MD5, + // and work just as above + + $cauth = $this->_digestReply('Bryce', 'ThisIsNotMyPassword'); + $cauth = preg_replace('/algorithm="MD5", /', '', $cauth); + + $data = $this->_doAuth($cauth, 'digest'); + $this->_checkOK($data); + } + + public function testDigestAuthQuotedNC() + { + // The nonce count isn't supposed to be quoted, but apparently some + // clients do anyway. + + $cauth = $this->_digestReply('Bryce', 'ThisIsNotMyPassword'); + $cauth = preg_replace('/nc=00000001/', 'nc="00000001"', $cauth); + + $data = $this->_doAuth($cauth, 'digest'); + $this->_checkOK($data); + } + + public function testDigestAuthBadCreds() + { + // Attempt Digest Authentication with a bad username and password + + // The expected Digest Www-Authenticate header value + $digest = $this->_digestChallenge(); + + $data = $this->_doAuth($this->_digestReply('Nobody', 'NotValid'), 'digest'); + $this->_checkUnauthorized($data, $digest); + } + + public function testDigestAuthBadCreds2() + { + // Formerly, a username with invalid characters would result in a 400 + // response, but now should result in 401 response. + + // The expected Digest Www-Authenticate header value + $digest = $this->_digestChallenge(); + + $data = $this->_doAuth($this->_digestReply('Bad:chars', 'NotValid'), 'digest'); + $this->_checkUnauthorized($data, $digest); + } + + public function testDigestTampered() + { + // Create the tampered header value + $tampered = $this->_digestReply('Bryce', 'ThisIsNotMyPassword'); + $tampered = preg_replace( + '/ nonce="[a-fA-F0-9]{32}", /', + ' nonce="'.str_repeat('0', 32).'", ', + $tampered + ); + + // The expected Digest Www-Authenticate header value + $digest = $this->_digestChallenge(); + + $data = $this->_doAuth($tampered, 'digest'); + $this->_checkUnauthorized($data, $digest); + } + + public function testBadSchemeRequest() + { + // Sending a request for an invalid authentication scheme should result + // in a 400 Bad Request response. + + $data = $this->_doAuth('Invalid ' . base64_encode('Nobody:NotValid'), 'basic'); + $this->_checkBadRequest($data); + } + + public function testBadDigestRequest() + { + // If any of the individual parts of the Digest Authorization header + // are bad, it results in a 400 Bad Request. But that's a lot of + // possibilities, so we're just going to pick one for now. + $bad = $this->_digestReply('Bryce', 'ThisIsNotMyPassword'); + $bad = preg_replace( + '/realm="([^"]+)"/', // cut out the realm + '', $bad + ); + + $data = $this->_doAuth($bad, 'digest'); + $this->_checkBadRequest($data); + } + + /** + * Acts like a client sending the given Authenticate header value. + * + * @param string $clientHeader Authenticate header value + * @param string $scheme Which authentication scheme to use + * @return array Containing the result, response headers, and the status + */ + protected function _doAuth($clientHeader, $scheme) + { + // Set up stub request and response objects + $request = new Request; + $response = new Response; + $response->setStatusCode(200); + + // Set stub method return values + $request->setUri('http://localhost/'); + $request->setMethod('GET'); + + $headers = $request->getHeaders(); + $headers->addHeaderLine('Authorization', $clientHeader); + $headers->addHeaderLine('User-Agent', 'PHPUnit'); + + // Select an Authentication scheme + switch ($scheme) { + case 'basic': + $use = $this->_basicConfig; + break; + case 'digest': + $use = $this->_digestConfig; + break; + case 'both': + default: + $use = $this->_bothConfig; + } + + // Create the HTTP Auth adapter + $a = new HTTP($use); + $a->setBasicResolver($this->_basicResolver); + $a->setDigestResolver($this->_digestResolver); + + // Send the authentication request + $a->setRequest($request); + $a->setResponse($response); + $result = $a->authenticate(); + + $return = array( + 'result' => $result, + 'status' => $response->getStatusCode(), + 'headers' => $response->getHeaders(), + ); + return $return; + } + + /** + * Constructs a local version of the digest challenge we expect to receive + * + * @return string + */ + protected function _digestChallenge() + { + return array( + 'type' => 'Digest ', + 'realm' => 'realm="' . $this->_digestConfig['realm'] . '"', + 'domain' => 'domain="' . $this->_bothConfig['digest_domains'] . '"', + ); + } + + /** + * Constructs a client digest Authorization header + * + * @return string + */ + protected function _digestReply($user, $pass) + { + $nc = '00000001'; + $timeout = ceil(time() / 300) * 300; + $nonce = md5($timeout . ':PHPUnit:Zend\Authentication\Adapter\Http'); + $opaque = md5('Opaque Data:Zend\\Authentication\\Adapter\\Http'); + $cnonce = md5('cnonce'); + $response = md5(md5($user . ':' . $this->_digestConfig['realm'] . ':' . $pass) . ":$nonce:$nc:$cnonce:auth:" + . md5('GET:/')); + $cauth = 'Digest ' + . 'username="Bryce", ' + . 'realm="' . $this->_digestConfig['realm'] . '", ' + . 'nonce="' . $nonce . '", ' + . 'uri="/", ' + . 'response="' . $response . '", ' + . 'algorithm="MD5", ' + . 'cnonce="' . $cnonce . '", ' + . 'opaque="' . $opaque . '", ' + . 'qop="auth", ' + . 'nc=' . $nc; + + return $cauth; + } + + /** + * Checks for an expected 401 Unauthorized response + * + * @param array $data Authentication results + * @param string $expected Expected Www-Authenticate header value + * @return void + */ + protected function _checkUnauthorized($data, $expected) + { + $result = $status = $headers = null; + extract($data); // $result, $status, $headers + + // Make sure the result is false + $this->assertInstanceOf('Zend\\Authentication\\Result', $result); + $this->assertFalse($result->isValid()); + + // Verify the status code and the presence of the challenge + $this->assertEquals(401, $status); + $this->assertTrue($headers->has('Www-Authenticate')); + + // Check to see if the expected challenge matches the actual + $headers = $headers->get('Www-Authenticate'); + $this->assertTrue($headers instanceof \ArrayIterator); + $this->assertEquals(1, count($headers)); + $header = $headers[0]->getFieldValue(); + $this->assertContains($expected['type'], $header, $header); + $this->assertContains($expected['realm'], $header, $header); + if (isset($expected['domain'])) { + $this->assertContains($expected['domain'], $header, $header); + $this->assertContains('algorithm="MD5"', $header, $header); + $this->assertContains('qop="auth"', $header, $header); + $this->assertRegExp('/nonce="[a-fA-F0-9]{32}"/', $header, $header); + $this->assertRegExp('/opaque="[a-fA-F0-9]{32}"/', $header, $header); + } + } + + /** + * Checks for an expected 200 OK response + * + * @param array $data Authentication results + * @return void + */ + protected function _checkOK($data) + { + $result = $status = $headers = null; + extract($data); // $result, $status, $headers + + // Make sure the result is true + $this->assertInstanceOf('Zend\\Authentication\\Result', $result); + $this->assertTrue($result->isValid(), var_export($result, 1)); + + // Verify we got a 200 response + $this->assertEquals(200, $status); + } + + /** + * Checks for an expected 400 Bad Request response + * + * @param array $data Authentication results + * @return void + */ + protected function _checkBadRequest($data) + { + $result = $status = $headers = null; + extract($data); // $result, $status, $headers + + // Make sure the result is false + $this->assertInstanceOf('Zend\\Authentication\\Result', $result); + $this->assertFalse($result->isValid()); + + // Make sure it set the right HTTP code + $this->assertEquals(400, $status); + } + + public function testBasicAuthValidCredsWithCustomIdentityObjectResolverReturnsAuthResult() + { + $this->_basicResolver = new TestAsset\BasicAuthObjectResolver(); + + $result = $this->_doAuth('Basic ' . base64_encode('Bryce:ThisIsNotMyPassword'), 'basic'); + $result = $result['result']; + + $this->assertInstanceOf('Zend\\Authentication\\Result', $result); + $this->assertTrue($result->isValid()); + } + + public function testBasicAuthInvalidCredsWithCustomIdentityObjectResolverReturnsUnauthorizedResponse() + { + $this->_basicResolver = new TestAsset\BasicAuthObjectResolver(); + $data = $this->_doAuth('Basic ' . base64_encode('David:ThisIsNotMyPassword'), 'basic'); + + $expected = array( + 'type' => 'Basic ', + 'realm' => 'realm="' . $this->_bothConfig['realm'] . '"', + ); + + $this->_checkUnauthorized($data, $expected); + } +} diff --git a/test/Adapter/Http/FileResolverTest.php b/test/Adapter/Http/FileResolverTest.php new file mode 100644 index 0000000..098fc45 --- /dev/null +++ b/test/Adapter/Http/FileResolverTest.php @@ -0,0 +1,230 @@ +_filesPath = __DIR__ . '/TestAsset'; + $this->_validPath = "$this->_filesPath/htdigest.3"; + $this->_badPath = 'doesnotexist'; + $this->_resolver = new Http\FileResolver($this->_validPath); + } + + /** + * Ensures that setFile() works as expected for valid input + * + * @return void + */ + public function testSetFileValid() + { + $this->_resolver->setFile($this->_validPath); + $this->assertEquals($this->_validPath, $this->_resolver->getFile()); + } + + /** + * Ensures that setFile() works as expected for invalid input + * + * @return void + */ + public function testSetFileInvalid() + { + $this->setExpectedException('Zend\\Authentication\\Adapter\\Http\\Exception\\ExceptionInterface', 'Path not readable'); + $this->_resolver->setFile($this->_badPath); + } + + /** + * Ensures that __construct() works as expected for valid input + * + * @return void + */ + public function testConstructValid() + { + $v = new Http\FileResolver($this->_validPath); + $this->assertEquals($this->_validPath, $v->getFile()); + } + + /** + * Ensures that __construct() works as expected for invalid input + * + * @return void + */ + public function testConstructInvalid() + { + $this->setExpectedException('Zend\\Authentication\\Adapter\\Http\\Exception\\ExceptionInterface', 'Path not readable'); + $v = new Http\FileResolver($this->_badPath); + } + + /** + * Ensures that resolve() works as expected for empty username + * + * @return void + */ + public function testResolveUsernameEmpty() + { + $this->setExpectedException('Zend\\Authentication\\Adapter\\Http\\Exception\\ExceptionInterface', 'Username is required'); + $this->_resolver->resolve('', ''); + } + + /** + * Ensures that resolve() works as expected for empty realm + * + * @return void + */ + public function testResolveRealmEmpty() + { + $this->setExpectedException('Zend\\Authentication\\Adapter\\Http\\Exception\\ExceptionInterface', 'Realm is required'); + $this->_resolver->resolve('username', ''); + } + + /** + * Ensures that resolve() works as expected for invalid username + * + * @return void + */ + public function testResolveUsernameInvalid() + { + try { + $this->_resolver->resolve('bad:name', 'realm'); + $this->fail('Accepted malformed username with colon'); + } catch (Http\Exception\ExceptionInterface $e) { + $this->assertContains('Username must consist', $e->getMessage()); + } + try { + $this->_resolver->resolve("badname\n", 'realm'); + $this->fail('Accepted malformed username with newline'); + } catch (Http\Exception\ExceptionInterface $e) { + $this->assertContains('Username must consist', $e->getMessage()); + } + } + + /** + * Ensures that resolve() works as expected for invalid realm + * + * @return void + */ + public function testResolveRealmInvalid() + { + try { + $this->_resolver->resolve('username', 'bad:realm'); + $this->fail('Accepted malformed realm with colon'); + } catch (Http\Exception\ExceptionInterface $e) { + $this->assertContains('Realm must consist', $e->getMessage()); + } + try { + $this->_resolver->resolve('username', "badrealm\n"); + $this->fail('Accepted malformed realm with newline'); + } catch (Http\Exception\ExceptionInterface $e) { + $this->assertContains('Realm must consist', $e->getMessage()); + } + } + + /** + * Ensures that resolve() works as expected when a previously readable file becomes unreadable + * + * @return void + */ + public function testResolveFileDisappearsMystery() + { + if (rename("$this->_filesPath/htdigest.3", "$this->_filesPath/htdigest.3.renamed")) { + try { + $this->_resolver->resolve('username', 'realm'); + $this->fail('Expected thrown exception upon resolve() after moving valid file'); + } catch (Http\Exception\ExceptionInterface $e) { + $this->assertContains('Unable to open password file', $e->getMessage()); + } + rename("$this->_filesPath/htdigest.3.renamed", "$this->_filesPath/htdigest.3"); + } + } + + /** + * Ensures that resolve() works as expected when provided valid credentials + * + * @return void + */ + public function testResolveValid() + { + $this->assertEquals( + $this->_resolver->resolve('Bryce', 'Test Realm'), + 'd5b7c330d5685beb782a9e22f0f20579', + 'Rejected valid credentials' + ); + } + + /** + * Ensures that resolve() works as expected when provided nonexistent realm + * + * @return void + */ + public function testResolveRealmNonexistent() + { + $this->assertFalse( + $this->_resolver->resolve('Bryce', 'nonexistent'), + 'Accepted a valid user in the wrong realm' + ); + } + + /** + * Ensures that resolve() works as expected when provided nonexistent user + * + * @return void + */ + public function testResolveUserNonexistent() + { + $this->assertFalse( + $this->_resolver->resolve('nonexistent', 'Test Realm'), + 'Accepted a nonexistent user from an existing realm' + ); + } +} diff --git a/test/Adapter/Http/ObjectTest.php b/test/Adapter/Http/ObjectTest.php new file mode 100644 index 0000000..24f6321 --- /dev/null +++ b/test/Adapter/Http/ObjectTest.php @@ -0,0 +1,242 @@ +_filesPath = __DIR__ . '/TestAsset'; + $this->_basicResolver = new Http\FileResolver("$this->_filesPath/htbasic.1"); + $this->_digestResolver = new Http\FileResolver("$this->_filesPath/htdigest.3"); + $this->_basicConfig = array( + 'accept_schemes' => 'basic', + 'realm' => 'Test Realm' + ); + $this->_digestConfig = array( + 'accept_schemes' => 'digest', + 'realm' => 'Test Realm', + 'digest_domains' => '/ http://localhost/', + 'nonce_timeout' => 300 + ); + $this->_bothConfig = array( + 'accept_schemes' => 'basic digest', + 'realm' => 'Test Realm', + 'digest_domains' => '/ http://localhost/', + 'nonce_timeout' => 300 + ); + } + + public function testValidConfigs() + { + $configs = array ( + $this->_basicConfig, + $this->_digestConfig, + $this->_bothConfig, + ); + foreach($configs as $config) + new Adapter\Http($config); + } + + public function testInvalidConfigs() + { + $badConfigs = array( + 'bad1' => array( + 'auth_type' => 'bogus', + 'realm' => 'Test Realm' + ), + 'bad2' => array( + 'auth_type' => 'digest', + 'realm' => 'Bad: "Chars"'."\n", + 'digest_domains' => '/ /admin', + 'nonce_timeout' => 300 + ), + 'bad3' => array( + 'auth_type' => 'digest', + 'realm' => 'Test Realm', + 'digest_domains' => 'no"quotes'."\tor tabs", + 'nonce_timeout' => 300 + ), + 'bad4' => array( + 'auth_type' => 'digest', + 'realm' => 'Test Realm', + 'digest_domains' => '/ /admin', + 'nonce_timeout' => 'junk' + ) + ); + + foreach ($badConfigs as $cfg) { + $t = null; + try { + $t = new Adapter\Http($cfg); + $this->fail('Accepted an invalid config'); + } catch (Adapter\Exception\ExceptionInterface $e) { + // Good, it threw an exception + } + } + } + + public function testAuthenticateArgs() + { + $a = new Adapter\Http($this->_basicConfig); + + try { + $a->authenticate(); + $this->fail('Attempted authentication without request/response objects'); + } catch (Adapter\Exception\ExceptionInterface $e) { + // Good, it threw an exception + } + + $request = new Request; + $response = new Response; + + // If this throws an exception, it fails + $a->setRequest($request) + ->setResponse($response) + ->authenticate(); + } + + public function testNoResolvers() + { + // Stub request for Basic auth + $headers = new Headers; + $headers->addHeaderLine('Authorization', 'Basic setHeaders($headers); + $response = new Response; + + // Once for Basic + try { + $a = new Adapter\Http($this->_basicConfig); + $a->setRequest($request) + ->setResponse($response); + $result = $a->authenticate(); + $this->fail("Tried Basic authentication without a resolver.\n" . \Zend\Debug::dump($result->getMessages(),null,false)); + } catch (Adapter\Exception\ExceptionInterface $e) { + // Good, it threw an exception + unset($a); + } + + // Stub request for Digest auth, must be reseted (recreated) + $headers = new Headers; + $headers->addHeaderLine('Authorization', 'Digest setHeaders($headers); + + // Once for Digest + try { + $a = new Adapter\Http($this->_digestConfig); + $a->setRequest($request) + ->setResponse($response); + $result = $a->authenticate(); + $this->fail("Tried Digest authentication without a resolver.\n" . \Zend\Debug::dump($result->getMessages(),null,false)); + } catch (Adapter\Exception\ExceptionInterface $e) { + // Good, it threw an exception + unset($a); + } + } + + public function testWrongResolverUsed() + { + $response = new Response(); + $headers = new Headers(); + $request = new Request(); + + $headers->addHeaderLine('Authorization', 'Basic setHeaders($headers); + + // Test a Digest auth process while the request is containing a Basic auth header + $adapter = new Adapter\Http($this->_digestConfig); + $adapter->setDigestResolver($this->_digestResolver) + ->setRequest($request) + ->setResponse($response); + $result = $adapter->authenticate(); + + $this->assertEquals($result->getCode(), Authentication\Result::FAILURE_CREDENTIAL_INVALID); + } + + public function testUnsupportedScheme() + { + $response = new Response(); + $headers = new Headers(); + $request = new Request(); + + $headers->addHeaderLine('Authorization', 'NotSupportedScheme setHeaders($headers); + + $a = new Adapter\Http($this->_digestConfig); + $a->setDigestResolver($this->_digestResolver) + ->setRequest($request) + ->setResponse($response); + $result = $a->authenticate(); + $this->assertEquals($result->getCode(),Authentication\Result::FAILURE_UNCATEGORIZED); + } +} diff --git a/test/Adapter/Http/ProxyTest.php b/test/Adapter/Http/ProxyTest.php new file mode 100644 index 0000000..b3babf5 --- /dev/null +++ b/test/Adapter/Http/ProxyTest.php @@ -0,0 +1,471 @@ +_filesPath = __DIR__ . '/TestAsset'; + $this->_basicResolver = new Http\FileResolver("{$this->_filesPath}/htbasic.1"); + $this->_digestResolver = new Http\FileResolver("{$this->_filesPath}/htdigest.3"); + $this->_basicConfig = array( + 'accept_schemes' => 'basic', + 'realm' => 'Test Realm', + 'proxy_auth' => true + ); + $this->_digestConfig = array( + 'accept_schemes' => 'digest', + 'realm' => 'Test Realm', + 'digest_domains' => '/ http://localhost/', + 'nonce_timeout' => 300, + 'proxy_auth' => true + ); + $this->_bothConfig = array( + 'accept_schemes' => 'basic digest', + 'realm' => 'Test Realm', + 'digest_domains' => '/ http://localhost/', + 'nonce_timeout' => 300, + 'proxy_auth' => true + ); + } + + public function testBasicChallenge() + { + // Trying to authenticate without sending an Proxy-Authorization header + // should result in a 407 reply with a Proxy-Authenticate header, and a + // false result. + + // The expected Basic Proxy-Authenticate header value + $basic = array( + 'type' => 'Basic ', + 'realm' => 'realm="' . $this->_bothConfig['realm'] . '"', + ); + + $data = $this->_doAuth('', 'basic'); + $this->_checkUnauthorized($data, $basic); + } + + public function testDigestChallenge() + { + // Trying to authenticate without sending an Proxy-Authorization header + // should result in a 407 reply with a Proxy-Authenticate header, and a + // false result. + + // The expected Digest Proxy-Authenticate header value + $digest = $this->_digestChallenge(); + + $data = $this->_doAuth('', 'digest'); + $this->_checkUnauthorized($data, $digest); + } + + public function testBothChallenges() + { + // Trying to authenticate without sending an Proxy-Authorization header + // should result in a 407 reply with at least one Proxy-Authenticate + // header, and a false result. + + $data = $this->_doAuth('', 'both'); + extract($data); // $result, $status, $headers + + // The expected Proxy-Authenticate header values + $basic = 'Basic realm="' . $this->_bothConfig['realm'] . '"'; + $digest = $this->_digestChallenge(); + + // Make sure the result is false + $this->assertInstanceOf('Zend\\Authentication\\Result', $result); + $this->assertFalse($result->isValid()); + + // Verify the status code and the presence of both challenges + $this->assertEquals(407, $status); + $this->assertTrue($headers->has('Proxy-Authenticate')); + $authHeader = $headers->get('Proxy-Authenticate'); + $this->assertEquals(2, count($authHeader), var_export($authHeader, 1)); + + // Check to see if the expected challenges match the actual + $basicFound = $digestFound = false; + foreach ($authHeader as $header) { + $value = $header->getFieldValue(); + if (preg_match('/^Basic/', $value)) { + $basicFound = true; + } + if (preg_match('/^Digest/', $value)) { + $digestFound = true; + } + } + $this->assertTrue($basicFound); + $this->assertTrue($digestFound); + } + + public function testBasicAuthValidCreds() + { + // Attempt Basic Authentication with a valid username and password + + $data = $this->_doAuth('Basic ' . base64_encode('Bryce:ThisIsNotMyPassword'), 'basic'); + $this->_checkOK($data); + } + + public function testBasicAuthBadCreds() + { + // Ensure that credentials containing invalid characters are treated as + // a bad username or password. + + // The expected Basic WWW-Authenticate header value + $basic = array( + 'type' => 'Basic ', + 'realm' => 'realm="' . $this->_basicConfig['realm'] . '"', + ); + + $data = $this->_doAuth('Basic ' . base64_encode("Bad\tChars:In:Creds"), 'basic'); + $this->_checkUnauthorized($data, $basic); + } + + public function testBasicAuthBadUser() + { + // Attempt Basic Authentication with a bad username and password + + // The expected Basic Proxy-Authenticate header value + $basic = array( + 'type' => 'Basic ', + 'realm' => 'realm="' . $this->_basicConfig['realm'] . '"', + ); + + $data = $this->_doAuth('Basic ' . base64_encode('Nobody:NotValid'), 'basic'); + $this->_checkUnauthorized($data, $basic); + } + + public function testBasicAuthBadPassword() + { + // Attempt Basic Authentication with a valid username, but invalid + // password + + // The expected Basic WWW-Authenticate header value + $basic = array( + 'type' => 'Basic ', + 'realm' => 'realm="' . $this->_basicConfig['realm'] . '"', + ); + + $data = $this->_doAuth('Basic ' . base64_encode('Bryce:Invalid'), 'basic'); + $this->_checkUnauthorized($data, $basic); + } + + public function testDigestAuthValidCreds() + { + // Attempt Digest Authentication with a valid username and password + + $data = $this->_doAuth($this->_digestReply('Bryce', 'ThisIsNotMyPassword'), 'digest'); + $this->_checkOK($data); + } + + public function testDigestAuthDefaultAlgo() + { + // If the client omits the aglorithm argument, it should default to MD5, + // and work just as above + + $cauth = $this->_digestReply('Bryce', 'ThisIsNotMyPassword'); + $cauth = preg_replace('/algorithm="MD5", /', '', $cauth); + + $data = $this->_doAuth($cauth, 'digest'); + $this->_checkOK($data); + } + + public function testDigestAuthQuotedNC() + { + // The nonce count isn't supposed to be quoted, but apparently some + // clients do anyway. + + $cauth = $this->_digestReply('Bryce', 'ThisIsNotMyPassword'); + $cauth = preg_replace('/nc=00000001/', 'nc="00000001"', $cauth); + + $data = $this->_doAuth($cauth, 'digest'); + $this->_checkOK($data); + } + + public function testDigestAuthBadCreds() + { + // Attempt Digest Authentication with a bad username and password + + // The expected Digest Proxy-Authenticate header value + $digest = $this->_digestChallenge(); + + $data = $this->_doAuth($this->_digestReply('Nobody', 'NotValid'), 'digest'); + $this->_checkUnauthorized($data, $digest); + } + + public function testDigestTampered() + { + // Create the tampered header value + $tampered = $this->_digestReply('Bryce', 'ThisIsNotMyPassword'); + $tampered = preg_replace( + '/ nonce="[a-fA-F0-9]{32}", /', + ' nonce="' . str_repeat('0', 32).'", ', + $tampered + ); + + // The expected Digest Proxy-Authenticate header value + $digest = $this->_digestChallenge(); + + $data = $this->_doAuth($tampered, 'digest'); + $this->_checkUnauthorized($data, $digest); + } + + public function testBadSchemeRequest() + { + // Sending a request for an invalid authentication scheme should result + // in a 400 Bad Request response. + + $data = $this->_doAuth('Invalid ' . base64_encode('Nobody:NotValid'), 'basic'); + $this->_checkBadRequest($data); + } + + public function testBadDigestRequest() + { + // If any of the individual parts of the Digest Proxy-Authorization header + // are bad, it results in a 400 Bad Request. But that's a lot of + // possibilities, so we're just going to pick one for now. + $bad = $this->_digestReply('Bryce', 'ThisIsNotMyPassword'); + $bad = preg_replace( + '/realm="([^"]+)"/', // cut out the realm + '', $bad + ); + + $data = $this->_doAuth($bad, 'digest'); + $this->_checkBadRequest($data); + } + + /** + * Acts like a client sending the given Authenticate header value. + * + * @param string $clientHeader Authenticate header value + * @param string $scheme Which authentication scheme to use + * @return array Containing the result, the response headers, and the status + */ + public function _doAuth($clientHeader, $scheme) + { + // Set up stub request and response objects + $response = new Response; + $response->setStatusCode(200); + + $headers = new Headers(); + $headers->addHeaderLine('Proxy-Authorization', $clientHeader); + $headers->addHeaderLine('User-Agent', 'PHPUnit'); + + $request = new Request(); + $request->setUri('http://localhost/'); + $request->setMethod('GET'); + $request->setHeaders($headers); + + // Select an Authentication scheme + switch ($scheme) { + case 'basic': + $use = $this->_basicConfig; + break; + case 'digest': + $use = $this->_digestConfig; + break; + case 'both': + default: + $use = $this->_bothConfig; + } + + // Create the HTTP Auth adapter + $a = new \Zend\Authentication\Adapter\Http($use); + $a->setBasicResolver($this->_basicResolver); + $a->setDigestResolver($this->_digestResolver); + + // Send the authentication request + $a->setRequest($request); + $a->setResponse($response); + $result = $a->authenticate(); + + $return = array( + 'result' => $result, + 'status' => $response->getStatusCode(), + 'headers' => $response->getHeaders(), + ); + return $return; + } + + /** + * Constructs a local version of the digest challenge we expect to receive + * + * @return string + */ + protected function _digestChallenge() + { + return array( + 'type' => 'Digest ', + 'realm' => 'realm="' . $this->_digestConfig['realm'] . '"', + 'domain' => 'domain="' . $this->_bothConfig['digest_domains'] . '"', + ); + } + + /** + * Constructs a client digest Proxy-Authorization header + * + * @param string $user + * @param string $pass + * @return string + */ + protected function _digestReply($user, $pass) + { + $nc = '00000001'; + $timeout = ceil(time() / 300) * 300; + $nonce = md5($timeout . ':PHPUnit:Zend\\Authentication\\Adapter\\Http'); + $opaque = md5('Opaque Data:Zend\\Authentication\\Adapter\\Http'); + $cnonce = md5('cnonce'); + $response = md5(md5($user . ':' . $this->_digestConfig['realm'] . ':' . $pass) . ":$nonce:$nc:$cnonce:auth:" + . md5('GET:/')); + $cauth = 'Digest ' + . 'username="Bryce", ' + . 'realm="' . $this->_digestConfig['realm'] . '", ' + . 'nonce="' . $nonce . '", ' + . 'uri="/", ' + . 'response="' . $response . '", ' + . 'algorithm="MD5", ' + . 'cnonce="' . $cnonce . '", ' + . 'opaque="' . $opaque . '", ' + . 'qop="auth", ' + . 'nc=' . $nc; + + return $cauth; + } + + /** + * Checks for an expected 407 Proxy-Unauthorized response + * + * @param array $data Authentication results + * @param string $expected Expected Proxy-Authenticate header value + * @return void + */ + protected function _checkUnauthorized($data, $expected) + { + extract($data); // $result, $status, $headers + + // Make sure the result is false + $this->assertInstanceOf('Zend\\Authentication\\Result', $result); + $this->assertFalse($result->isValid()); + + // Verify the status code and the presence of the challenge + $this->assertEquals(407, $status); + $this->assertTrue($headers->has('Proxy-Authenticate')); + + // Check to see if the expected challenge matches the actual + $headers = $headers->get('Proxy-Authenticate'); + $this->assertTrue($headers instanceof \ArrayIterator); + $this->assertEquals(1, count($headers)); + $header = $headers[0]->getFieldValue(); + $this->assertContains($expected['type'], $header, $header); + $this->assertContains($expected['realm'], $header, $header); + if (isset($expected['domain'])) { + $this->assertContains($expected['domain'], $header, $header); + $this->assertContains('algorithm="MD5"', $header, $header); + $this->assertContains('qop="auth"', $header, $header); + $this->assertRegExp('/nonce="[a-fA-F0-9]{32}"/', $header, $header); + $this->assertRegExp('/opaque="[a-fA-F0-9]{32}"/', $header, $header); + } + } + + /** + * Checks for an expected 200 OK response + * + * @param array $data Authentication results + * @return void + */ + protected function _checkOK($data) + { + extract($data); // $result, $status, $headers + + // Make sure the result is true + $this->assertInstanceOf('Zend\\Authentication\\Result', $result); + $this->assertTrue($result->isValid(), var_export($result->getMessages(), 1)); + + // Verify we got a 200 response + $this->assertEquals(200, $status); + } + + /** + * Checks for an expected 400 Bad Request response + * + * @param array $data Authentication results + * @return void + */ + protected function _checkBadRequest($data) + { + extract($data); // $result, $status, $headers + + // Make sure the result is false + $this->assertInstanceOf('Zend\\Authentication\\Result', $result); + $this->assertFalse($result->isValid()); + + // Make sure it set the right HTTP code + $this->assertEquals(400, $status); + } +} diff --git a/test/Adapter/Http/TestAsset/htbasic.1 b/test/Adapter/Http/TestAsset/htbasic.1 new file mode 100644 index 0000000..7f18f96 --- /dev/null +++ b/test/Adapter/Http/TestAsset/htbasic.1 @@ -0,0 +1,3 @@ +Bryce:Test Realm:ThisIsNotMyPassword +Mufasa:Test Realm:Circle Of Life +Bad Chars:In:Creds diff --git a/test/Adapter/Http/TestAsset/htdigest.3 b/test/Adapter/Http/TestAsset/htdigest.3 new file mode 100644 index 0000000..f9f4944 --- /dev/null +++ b/test/Adapter/Http/TestAsset/htdigest.3 @@ -0,0 +1,2 @@ +Bryce:Test Realm:d5b7c330d5685beb782a9e22f0f20579 +Mufasa:Test Realm:200dc292ecb68e04c95bb74ae2ce3c80 diff --git a/test/Adapter/Ldap/OfflineTest.php b/test/Adapter/Ldap/OfflineTest.php new file mode 100644 index 0000000..55696b1 --- /dev/null +++ b/test/Adapter/Ldap/OfflineTest.php @@ -0,0 +1,93 @@ +adapter = new Adapter\Ldap(); + } + + public function testGetSetLdap() + { + if (!extension_loaded('ldap')) { + $this->markTestSkipped('LDAP is not enabled'); + } + $this->adapter->setLdap(new Ldap\Ldap()); + $this->assertInstanceOf('Zend\Ldap\Ldap', $this->adapter->getLdap()); + } + + public function testUsernameIsNullIfNotSet() + { + $this->assertNull($this->adapter->getUsername()); + } + + public function testPasswordIsNullIfNotSet() + { + $this->assertNull($this->adapter->getPassword()); + } + + public function testSetAndGetUsername() + { + $usernameExpected = 'someUsername'; + $usernameActual = $this->adapter->setUsername($usernameExpected) + ->getUsername(); + $this->assertSame($usernameExpected, $usernameActual); + } + + public function testSetAndGetPassword() + { + $passwordExpected = 'somePassword'; + $passwordActual = $this->adapter->setPassword($passwordExpected) + ->getPassword(); + $this->assertSame($passwordExpected, $passwordActual); + } + + public function testSetIdentityProxiesToSetUsername() + { + $usernameExpected = 'someUsername'; + $usernameActual = $this->adapter->setIdentity($usernameExpected) + ->getUsername(); + $this->assertSame($usernameExpected, $usernameActual); + } + + public function testSetCredentialProxiesToSetPassword() + { + $passwordExpected = 'somePassword'; + $passwordActual = $this->adapter->setCredential($passwordExpected) + ->getPassword(); + $this->assertSame($passwordExpected, $passwordActual); + } +} diff --git a/test/Adapter/Ldap/OnlineTest.php b/test/Adapter/Ldap/OnlineTest.php new file mode 100644 index 0000000..67c9ab1 --- /dev/null +++ b/test/Adapter/Ldap/OnlineTest.php @@ -0,0 +1,192 @@ +markTestSkipped('LDAP online tests are not enabled'); + } + $this->options = array( + 'host' => TESTS_ZEND_LDAP_HOST, + 'username' => TESTS_ZEND_LDAP_USERNAME, + 'password' => TESTS_ZEND_LDAP_PASSWORD, + 'baseDn' => TESTS_ZEND_LDAP_BASE_DN, + ); + if (defined('TESTS_ZEND_LDAP_PORT')) + $this->options['port'] = TESTS_ZEND_LDAP_PORT; + if (defined('TESTS_ZEND_LDAP_USE_START_TLS')) + $this->options['useStartTls'] = TESTS_ZEND_LDAP_USE_START_TLS; + if (defined('TESTS_ZEND_LDAP_USE_SSL')) + $this->options['useSsl'] = TESTS_ZEND_LDAP_USE_SSL; + if (defined('TESTS_ZEND_LDAP_BIND_REQUIRES_DN')) + $this->options['bindRequiresDn'] = TESTS_ZEND_LDAP_BIND_REQUIRES_DN; + if (defined('TESTS_ZEND_LDAP_ACCOUNT_FILTER_FORMAT')) + $this->options['accountFilterFormat'] = TESTS_ZEND_LDAP_ACCOUNT_FILTER_FORMAT; + if (defined('TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME')) + $this->options['accountDomainName'] = TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME; + if (defined('TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT')) + $this->options['accountDomainNameShort'] = TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT; + + if (defined('TESTS_ZEND_LDAP_ALT_USERNAME')) { + $this->names[Ldap\Ldap::ACCTNAME_FORM_USERNAME] = TESTS_ZEND_LDAP_ALT_USERNAME; + if (defined('TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME')) { + $this->names[Ldap\Ldap::ACCTNAME_FORM_PRINCIPAL] = + TESTS_ZEND_LDAP_ALT_USERNAME . '@' . TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME; + } + if (defined('TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT')) { + $this->names[Ldap\Ldap::ACCTNAME_FORM_BACKSLASH] = + TESTS_ZEND_LDAP_ACCOUNT_DOMAIN_NAME_SHORT . '\\' . TESTS_ZEND_LDAP_ALT_USERNAME; + } + } + } + + public function testSimpleAuth() + { + $adapter = new Adapter\Ldap( + array($this->options), + TESTS_ZEND_LDAP_ALT_USERNAME, + TESTS_ZEND_LDAP_ALT_PASSWORD + ); + + $result = $adapter->authenticate(); + + $this->assertTrue($result instanceof Authentication\Result); + $this->assertTrue($result->isValid()); + $this->assertTrue($result->getCode() == Authentication\Result::SUCCESS); + } + + public function testCanonAuth() + { + /* This test authenticates with each of the account name forms + * (uname, uname@example.com, EXAMPLE\uname) AND it does so with + * the accountCanonicalForm set to each of the account name forms + * (e.g. authenticate with uname@example.com but getIdentity() returns + * EXAMPLE\uname). A total of 9 authentications are performed. + */ + foreach ($this->names as $form => $formName) { + $options = $this->options; + $options['accountCanonicalForm'] = $form; + $adapter = new Adapter\Ldap(array($options)); + $adapter->setPassword(TESTS_ZEND_LDAP_ALT_PASSWORD); + foreach ($this->names as $username) { + $adapter->setUsername($username); + $result = $adapter->authenticate(); + $this->assertTrue($result instanceof Authentication\Result); + $this->assertTrue($result->isValid()); + $this->assertTrue($result->getCode() == Authentication\Result::SUCCESS); + $this->assertTrue($result->getIdentity() === $formName); + } + } + } + + public function testInvalidPassAuth() + { + $adapter = new Adapter\Ldap( + array($this->options), + TESTS_ZEND_LDAP_ALT_USERNAME, + 'invalid' + ); + + $result = $adapter->authenticate(); + $this->assertTrue($result instanceof Authentication\Result); + $this->assertTrue($result->isValid() === false); + $this->assertTrue($result->getCode() == Authentication\Result::FAILURE_CREDENTIAL_INVALID); + } + + public function testInvalidUserAuth() + { + $adapter = new Adapter\Ldap( + array($this->options), + 'invalid', + 'doesntmatter' + ); + + $result = $adapter->authenticate(); + $this->assertTrue($result instanceof Authentication\Result); + $this->assertTrue($result->isValid() === false); + $this->assertTrue( + $result->getCode() == Authentication\Result::FAILURE_IDENTITY_NOT_FOUND || + $result->getCode() == Authentication\Result::FAILURE_CREDENTIAL_INVALID + ); + } + + public function testMismatchDomainAuth() + { + $adapter = new Adapter\Ldap( + array($this->options), + 'EXAMPLE\\doesntmatter', + 'doesntmatter' + ); + + $result = $adapter->authenticate(); + $this->assertTrue($result instanceof Authentication\Result); + $this->assertFalse($result->isValid()); + $this->assertThat($result->getCode(), $this->lessThanOrEqual(Authentication\Result::FAILURE)); + $messages = $result->getMessages(); + $this->assertContains('not found', $messages[0]); + } + + public function testAccountObjectRetrieval() + { + $adapter = new Adapter\Ldap( + array($this->options), + TESTS_ZEND_LDAP_ALT_USERNAME, + TESTS_ZEND_LDAP_ALT_PASSWORD + ); + + $result = $adapter->authenticate(); + $account = $adapter->getAccountObject(); + + //$this->assertTrue($result->isValid()); + $this->assertInternalType('object', $account); + $this->assertEquals(TESTS_ZEND_LDAP_ALT_DN, $account->dn); + } + + public function testAccountObjectRetrievalWithOmittedAttributes() + { + $adapter = new Adapter\Ldap( + array($this->options), + TESTS_ZEND_LDAP_ALT_USERNAME, + TESTS_ZEND_LDAP_ALT_PASSWORD + ); + + $result = $adapter->authenticate(); + $account = $adapter->getAccountObject(array(), array('userPassword')); + + $this->assertInternalType('object', $account); + $this->assertFalse(isset($account->userpassword)); + } +} diff --git a/test/Adapter/TestAsset/Digest/htdigest.1 b/test/Adapter/TestAsset/Digest/htdigest.1 new file mode 100644 index 0000000..ff62927 --- /dev/null +++ b/test/Adapter/TestAsset/Digest/htdigest.1 @@ -0,0 +1,2 @@ +someUser:Some Realm:fde17b91c3a510ecbaf7dbd37f59d4f8 +someOtherUser:Some Other Realm:1911c62b21a85c85c4c1a57785893b94 diff --git a/test/Adapter/TestAsset/OpenId/.gitignore b/test/Adapter/TestAsset/OpenId/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/test/Adapter/TestAsset/OpenId/assoc.lock b/test/Adapter/TestAsset/OpenId/assoc.lock new file mode 100644 index 0000000..e69de29 diff --git a/test/Adapter/TestAsset/OpenId/discovery.lock b/test/Adapter/TestAsset/OpenId/discovery.lock new file mode 100644 index 0000000..e69de29 diff --git a/test/Adapter/TestAsset/OpenId/nonce.lock b/test/Adapter/TestAsset/OpenId/nonce.lock new file mode 100644 index 0000000..e69de29 diff --git a/test/Adapter/TestAsset/OpenId/nonce_9f11599cc1f088b7c358f33610cb126c b/test/Adapter/TestAsset/OpenId/nonce_9f11599cc1f088b7c358f33610cb126c new file mode 100644 index 0000000..83c598f --- /dev/null +++ b/test/Adapter/TestAsset/OpenId/nonce_9f11599cc1f088b7c358f33610cb126c @@ -0,0 +1 @@ +http://www.myopenid.com/;2007-08-14T12:52:33Z46c1a59124ffe \ No newline at end of file diff --git a/test/Adapter/TestAsset/OpenIdResponseHelper.php b/test/Adapter/TestAsset/OpenIdResponseHelper.php new file mode 100644 index 0000000..6a39bb6 --- /dev/null +++ b/test/Adapter/TestAsset/OpenIdResponseHelper.php @@ -0,0 +1,40 @@ +_canSendHeaders = $canSendHeaders; + } + + public function canSendHeaders($throw = false) + { + return $this->_canSendHeaders; + } + + public function sendResponse() + { + } +} diff --git a/test/AuthenticationServiceTest.php b/test/AuthenticationServiceTest.php new file mode 100644 index 0000000..edbf926 --- /dev/null +++ b/test/AuthenticationServiceTest.php @@ -0,0 +1,90 @@ +auth = new AuthenticationService(); + } + + /** + * Ensures that getStorage() returns Zend_Auth_Storage_Session + * + * @return void + */ + public function testGetStorage() + { + $storage = $this->auth->getStorage(); + $this->assertTrue($storage instanceof Auth\Storage\Session); + } + + public function testAdapter() + { + $this->assertNull($this->auth->getAdapter()); + $successAdapter = new TestAsset\SuccessAdapter(); + $ret = $this->auth->setAdapter($successAdapter); + $this->assertSame($ret, $this->auth); + $this->assertSame($successAdapter, $this->auth->getAdapter()); + } + + /** + * Ensures expected behavior for successful authentication + * + * @return void + */ + public function testAuthenticate() + { + $result = $this->authenticate(); + $this->assertTrue($result instanceof Auth\Result); + $this->assertTrue($this->auth->hasIdentity()); + $this->assertEquals('someIdentity', $this->auth->getIdentity()); + } + + public function testAuthenticateSetAdapter() + { + $result = $this->authenticate(new TestAsset\SuccessAdapter()); + $this->assertTrue($result instanceof Auth\Result); + $this->assertTrue($this->auth->hasIdentity()); + $this->assertEquals('someIdentity', $this->auth->getIdentity()); + } + + /** + * Ensures expected behavior for clearIdentity() + * + * @return void + */ + public function testClearIdentity() + { + $this->authenticate(); + $this->auth->clearIdentity(); + $this->assertFalse($this->auth->hasIdentity()); + $this->assertEquals(null, $this->auth->getIdentity()); + } + + protected function authenticate($adapter = null) + { + if ($adapter === null) { + $adapter = new TestAsset\SuccessAdapter(); + } + return $this->auth->authenticate($adapter); + } +} diff --git a/test/TestAsset/SuccessAdapter.php b/test/TestAsset/SuccessAdapter.php new file mode 100644 index 0000000..e0b5f71 --- /dev/null +++ b/test/TestAsset/SuccessAdapter.php @@ -0,0 +1,22 @@ +