diff --git a/src/AuthenticationMiddleware.php b/src/AuthenticationMiddleware.php index 978a528..2177f21 100644 --- a/src/AuthenticationMiddleware.php +++ b/src/AuthenticationMiddleware.php @@ -70,28 +70,6 @@ public function getAuthorizationResult() */ private function buildWwwAuthenticateHeader(AuthorizationResultInterface $result) { - $scheme = $result->getScheme(); - $challenge = $this->buildChallengeString($result->getChallenge()); - - if (empty($challenge)) { - return $scheme; - } - - return sprintf('%s %s', $scheme, $challenge); - } - - /** - * @param array $serviceChallenge - * - * @return type - */ - private function buildChallengeString(array $serviceChallenge) - { - $challengePairs = []; - - foreach ($serviceChallenge as $challenge => $value) { - $challengePairs[] = sprintf('%s="%s"', $challenge, addslashes($value)); - } - return implode(', ', $challengePairs); + return Util::buildHeader($result->getScheme(), $result->getChallenge()); } } \ No newline at end of file diff --git a/src/AuthorizationResult.php b/src/AuthorizationResult.php index 320486f..41c6558 100644 --- a/src/AuthorizationResult.php +++ b/src/AuthorizationResult.php @@ -64,6 +64,27 @@ public static function notAuthorized($scheme, array $challenge = [], array $attr return $instance; } + /** + * @param string $scheme + * @param array $challenge + * @param array $attributes + * + * @return self + */ + public static function error($scheme, $error, $errorDescription, array $challenge = [], array $attributes = []) + { + $challenge['error'] = $error; + $challenge['error_description'] = $errorDescription; + + $instance = new self(); + $instance->isAuthorized = false; + $instance->scheme = $scheme; + $instance->challenge = $challenge; + $instance->attributes = $attributes; + + return $instance; + } + /** * @return array */ diff --git a/src/BasicAuthorizationService.php b/src/BasicAuthorizationService.php index 9916f39..efbb06e 100644 --- a/src/BasicAuthorizationService.php +++ b/src/BasicAuthorizationService.php @@ -51,18 +51,14 @@ public function authorize(ServerRequestInterface $request) if ($result === true) { return AuthorizationResult::authorized(self::SCHEME, [], ['user-ID' => $userId]); } elseif ($result === false) { - return AuthorizationResult::notAuthorized(self::SCHEME, [ + return AuthorizationResult::error(self::SCHEME, 'Invalid credentials', 'Login and/or password are invalid', [ 'realm' => $this->realm, - 'error' => 'Invalid credentials', - 'error_description' => 'Login and/or password are invalid', ]); } throw new UnexpectedValueException(sprintf('%s\'s result must be a boolean value', UserPasswordInterface::class)); } - return AuthorizationResult::notAuthorized(self::SCHEME, [ + return AuthorizationResult::error(self::SCHEME, 'Invalid header', 'Cannot read user-ID and password from header', [ 'realm' => $this->realm, - 'error' => '', - 'error_description' => '', ]); } diff --git a/src/CredentialAdapter/ArrayUserPassword.php b/src/CredentialAdapter/ArrayUserPassword.php index 1cfb753..f64aaba 100644 --- a/src/CredentialAdapter/ArrayUserPassword.php +++ b/src/CredentialAdapter/ArrayUserPassword.php @@ -2,7 +2,10 @@ namespace PhpMiddleware\HttpAuthentication\CredentialAdapter; -final class ArrayUserPassword implements UserPasswordInterface +use PhpMiddleware\HttpAuthentication\CredentialAdapter\Exception\UsernameNotFoundException; +use PhpMiddleware\HttpAuthentication\Util; + +final class ArrayUserPassword implements UserPasswordInterface, HashProviderInterface { /** * @var array @@ -25,6 +28,24 @@ public function __construct(array $users) */ public function authenticate($username, $password) { - return isset($this->users[$username]) && $this->users[$username] === $password; + return $this->isUserNameExists($username) && $this->users[$username] === $password; + } + + private function isUserNameExists($username) + { + return isset($this->users[$username]); } + + public function getHash($username, $realm) + { + if (!$this->isUserNameExists($username)) { + throw new UsernameNotFoundException('Username does not exist'); + } + return Util::md5Implode([ + $username, + $realm, + $this->users[$username], + ]); + } + } diff --git a/src/CredentialAdapter/Exception/UsernameNotFoundException.php b/src/CredentialAdapter/Exception/UsernameNotFoundException.php new file mode 100644 index 0000000..1e0b5de --- /dev/null +++ b/src/CredentialAdapter/Exception/UsernameNotFoundException.php @@ -0,0 +1,8 @@ +hashProvider = $hashProvider; + $this->realm = $realm; + } + public function authorize(ServerRequestInterface $request) { - throw new \BadMethodCallException('Not implemented'); + $header = $request->getHeaderLine('Authorization'); + + $authorization = $this->parseAuthorizationHeader($header); + + if (!$authorization) { + return AuthorizationResult::error('digest', 'Invalid header', 'Cannot read data from Authorization header', [ + 'realm' => $this->realm, + ]); + } + + $result = $this->checkAuthentication($authorization, $request->getMethod()); + + if ($result) { + return AuthorizationResult::authorized('digest'); + } + return AuthorizationResult::notAuthorized('digest', [], $authorization); + } + + private function checkAuthentication(array $authorization, $method) + { + if ($authorization['realm'] !== $this->realm) { + return false; + } + try { + $A1 = $this->hashProvider->getHash($authorization['username'], $this->realm); + } catch (UsernameNotFoundException $exception) { + return false; + } + + $A2 = Util::md5Implode([$method, $authorization['uri']]); + + $realResponse = Util::md5Implode([$A1, $authorization['nonce'], $A2]); + + return $authorization['response'] === $realResponse; + } + + private function parseAuthorizationHeader($header) + { + if (strpos($header, 'Digest') !== 0) { + return false; + } + + $neededParts = ['nonce' => 1, 'realm' => 1, 'username' => 1, 'uri' => 1, 'response' => 1]; + $neededPartsString = implode('|', array_keys($neededParts)); + $data = []; + + preg_match_all('@('.$neededPartsString.')=(?:([\'"])([^\2]+?)\2|([^\s,]+))@', substr($header, 7), $matches, PREG_SET_ORDER); + + if (is_array($matches)) { + foreach ($matches as $match) { + $data[$match[1]] = $match[3] ?: $match[4]; + unset($neededParts[$match[1]]); + } + } + + if (!empty($neededParts)) { + return false; + } + + return $data; } } diff --git a/src/RequestBuilder/Digest.php b/src/RequestBuilder/Digest.php new file mode 100644 index 0000000..29cfe10 --- /dev/null +++ b/src/RequestBuilder/Digest.php @@ -0,0 +1,50 @@ +username = $username; + $this->password = $password; + $this->realm = $realm; + $this->nonce = $nonce; + } + + /** + * @param RequestInterface $request + * + * @return RequestInterface + */ + public function authenticate(RequestInterface $request) + { + $uri = (string) $request->getUri(); + + $a1 = Util::md5Implode([$this->username, $this->realm, $this->password]); + $a2 = Util::md5Implode([$request->getMethod(), $uri]); + + $response = Util::md5Implode([$a1, $this->nonce, $a2]); + + $value = Util::buildHeader('Digest', [ + 'username' => $this->username, + 'realm' => $this->realm, + 'nonce' => $this->nonce, + 'uri' => $uri, + 'response' => $response, + ]); + + return $request->withHeader('Authorization', $value); + } +} diff --git a/src/Util.php b/src/Util.php new file mode 100644 index 0000000..af545da --- /dev/null +++ b/src/Util.php @@ -0,0 +1,55 @@ + $value) { + $challengePairs[] = sprintf('%s="%s"', $challenge, addslashes($value)); + } + return implode(', ', $challengePairs); + } +} diff --git a/test/CredentialAdapter/ArrayUserPasswordTest.php b/test/CredentialAdapter/ArrayUserPasswordTest.php index 6560c1e..de04a27 100644 --- a/test/CredentialAdapter/ArrayUserPasswordTest.php +++ b/test/CredentialAdapter/ArrayUserPasswordTest.php @@ -3,6 +3,7 @@ namespace PhpMiddlewareTest\HttpAuthentication\CredentialAdapter; use PhpMiddleware\HttpAuthentication\CredentialAdapter\ArrayUserPassword; +use PhpMiddleware\HttpAuthentication\CredentialAdapter\Exception\UsernameNotFoundException; use PHPUnit_Framework_TestCase; class ArrayUserPasswordTest extends PHPUnit_Framework_TestCase @@ -38,6 +39,20 @@ public function testNotAuthenticate($username, $password) $this->assertFalse($result); } + public function testGetHash() + { + $result = $this->adapter->getHash('boo', 'any-realm'); + + $this->assertSame(32, strlen($result)); + } + + public function testInvalidUsername() + { + $this->setExpectedException(UsernameNotFoundException::class); + + $this->adapter->getHash('baz', 'any-realm'); + } + public function correctDataProvider() { return [ diff --git a/test/DigestAuthorizationServiceTest.php b/test/DigestAuthorizationServiceTest.php new file mode 100644 index 0000000..0d4c1ff --- /dev/null +++ b/test/DigestAuthorizationServiceTest.php @@ -0,0 +1,73 @@ +hashProvider = new ArrayUserPassword([ + 'boo' => 'foo', + ]); + $this->service = new DigestAuthorizationService($this->hashProvider, 'realm'); + $this->serverRequest = new ServerRequest(); + } + + public function testNotAuthRequest() + { + $result = $this->service->authorize($this->serverRequest); + + $this->assertInstanceOf(AuthorizationResultInterface::class, $result); + $this->assertFalse($result->isAuthorized()); + } + + public function testAuthRequest() + { + $header = 'Digest username="boo", realm="realm", nonce="nonce", uri="/boo/bar", response="13f87e6ef7f79c68f8721d3e6b9e45e5"'; + $request = $this->serverRequest->withHeader('Authorization', $header); + + $result = $this->service->authorize($request); + + $this->assertTrue($result->isAuthorized()); + } + + public function testNotAuthRequestWithoutRealm() + { + $header = 'Digest username="boo", nonce="nonce", uri="/boo/bar", response="13f87e6ef7f79c68f8721d3e6b9e45e5"'; + $request = $this->serverRequest->withHeader('Authorization', $header); + + $result = $this->service->authorize($request); + + $this->assertFalse($result->isAuthorized()); + } + + public function testNotAuthRequestRealmIsDifferent() + { + $header = 'Digest username="boo", realm="realmus", nonce="nonce", uri="/boo/bar", response="13f87e6ef7f79c68f8721d3e6b9e45e5"'; + $request = $this->serverRequest->withHeader('Authorization', $header); + + $result = $this->service->authorize($request); + + $this->assertFalse($result->isAuthorized()); + } + + public function testNotAuthRequestUsernameNotExists() + { + $header = 'Digest username="booz", realm="realm", nonce="nonce", uri="/boo/bar", response="13f87e6ef7f79c68f8721d3e6b9e45e5"'; + $request = $this->serverRequest->withHeader('Authorization', $header); + + $result = $this->service->authorize($request); + + $this->assertFalse($result->isAuthorized()); + } +} diff --git a/test/RequestBuilder/DigestTest.php b/test/RequestBuilder/DigestTest.php new file mode 100644 index 0000000..5de3821 --- /dev/null +++ b/test/RequestBuilder/DigestTest.php @@ -0,0 +1,33 @@ +requestBuilder = new Digest('boo', 'foo', 'realm', 'nonce'); + $uri = new Uri('/boo/bar'); + $this->request = new Request($uri, 'GET'); + } + + public function testAuthorizeRequest() + { + $result = $this->requestBuilder->authenticate($this->request); + + $this->assertNotSame($this->request, $result); + $this->assertInstanceOf(RequestInterface::class, $result); + $expected = 'Digest username="boo", realm="realm", nonce="nonce", uri="/boo/bar", response="13f87e6ef7f79c68f8721d3e6b9e45e5"'; + + $this->assertSame($expected, $result->getHeaderLine('Authorization')); + } +}