diff --git a/src/Authorize/Header/BasicMethod.php b/src/Authorize/Header/BasicMethod.php new file mode 100644 index 0000000..eda349f --- /dev/null +++ b/src/Authorize/Header/BasicMethod.php @@ -0,0 +1,35 @@ +arguments) ) { + throw new \RuntimeException("Base64 decoding of given username:password failed"); + } + + list($userName, $password) = explode(':', $decoded) + [ null, null ]; + + if ( empty($userName) ) { + throw new \RuntimeException("A username must be provided"); + } + elseif ( empty($password) ) { + throw new \RuntimeException("A password must be provided"); + } + + ( new Authorize($this->user) )->authenticate([ 'email' => $userName, 'username' => $userName ], $password); + + return $this->user->loggedIn(); + } +} \ No newline at end of file diff --git a/src/Authorize/Header/BearerMethod.php b/src/Authorize/Header/BearerMethod.php new file mode 100644 index 0000000..86147f4 --- /dev/null +++ b/src/Authorize/Header/BearerMethod.php @@ -0,0 +1,19 @@ +parseDigestArguments($this->arguments); + + if (empty($arguments['username'])) { + throw new \InvalidArgumentException("A 'username' key is required to authenticate using Digest"); + } + + $isSess = stripos($arguments['algorithm'] ?? "", "-SESS") !== false; + + $hashMethod = $this->getDigestAlgorithmHash($arguments); + + $arguments['nc'] = str_pad($arguments['nc'] ?? "1", 8, '0', STR_PAD_LEFT); + + $ha1 = $this->getSecretHash(); + + if ($isSess) { + $ha1 = hash($hashMethod, implode(':', [ + $ha1, $arguments['nonce'] , $arguments['cnonce'] + ])); + } + + switch($arguments['qop'] ?? 'auth') { + case 'auth-int': + $ha2 = hash($hashMethod, implode(':', [ + strtoupper($request->getMethod()), $arguments['uri'], hash($hashMethod, $body ?? "") + ])); + break; + + case 'auth': + default: + $ha2 = hash($hashMethod, implode(':', [ + strtoupper($request->getMethod()), $arguments['uri'] + ])); + break; + } + + if (isset($arguments['qop'])) { + $response = hash($hashMethod, implode(':', [ + $ha1, $arguments['nonce'], $arguments['nc'], $arguments['cnonce'], $arguments['qop'], $ha2 + ])); + } + else { + $response = hash($hashMethod, implode(':', [ + $ha1, $arguments['nonce'], $ha2 + ])); + } + + return $response === $arguments['response']; + } + + + protected function parseDigestArguments(string|array $arguments) : array + { + if (is_string($arguments)) { + $keys = [ 'nonce', 'nc', 'cnonce', 'qop', 'username', 'uri', 'realm', 'response', 'opaque', 'algorithm' ]; + + # From https://www.php.net/manual/en/features.http-auth.php + preg_match_all('@(' . implode('|', $keys) . ')=(?:([\'"])([^\2]+?)\2|([^\s,]+))@', $arguments, $matches, PREG_SET_ORDER); + + $arguments = []; + + foreach ($matches as $match) { + $arguments[$match[1]] = $match[3] ?: $match[4]; + } + } + + return $arguments; + } + + protected function getDigestAlgorithmHash(array $arguments) : string + { + switch(strtoupper($arguments['algorithm'] ?? "")) { + case "SHA-512-256": + case "SHA-512-256-SESS": + return "sha512/256"; + + case "SHA-256": + case "SHA-256-SESS": + return "sha256"; + + default: + case 'MD5-SESS': + case 'MD5': + return "md5"; + } + } + +} \ No newline at end of file diff --git a/src/Authorize/HeaderAuthentication.php b/src/Authorize/HeaderAuthentication.php index f47f001..e5e5b17 100644 --- a/src/Authorize/HeaderAuthentication.php +++ b/src/Authorize/HeaderAuthentication.php @@ -4,20 +4,28 @@ namespace Ulmus\User\Authorize; use Psr\Http\Message\ServerRequestInterface; use Ulmus\User\Common\AuthorizeContentTypeEnum; -use Ulmus\User\Entity\UserInterface; -use Ulmus\User\Lib\Authorize; +use Ulmus\User\Entity\{ DigestAuthUserInterface, UserInterface }; class HeaderAuthentication implements AuthorizeMethodInterface { public function connect(ServerRequestInterface $request, UserInterface $user): bool { - if ( null !== ( $auth = $request->getHeaderLine('Authorization') ) ) { + if (null !== ( $auth = $request->getHeaderLine('Authorization') )) { + list($method, $value) = explode(' ', $auth, 2) + [ null, null ]; - list($method, $userPass) = explode(' ', $auth, 2) + [ null, null ]; - - switch(strtolower(strtolower($method))) { + switch(strtolower($method)) { case "basic": - return $this->basicMethod($user, $userPass); + return (new Header\BasicMethod($user, $value))->execute($request); + + case "digest": + if (! $user instanceof DigestAuthUserInterface) { + throw new \RuntimeException("Your user entity must provide a valid hash of `user:realm:password` "); + } + + return (new Header\DigestMethod($user, $value))->execute($request); + + case "bearer": + return (new Header\BearerMethod($user, $value))->execute($request); default: throw new \InvalidArgumentException("An authentication method must be provided"); @@ -27,25 +35,6 @@ class HeaderAuthentication implements AuthorizeMethodInterface return false; } - protected function basicMethod(UserInterface $user, string $userPassword) : bool - { - if ( false === $decoded = base64_decode($userPassword) ) { - throw new \RuntimeException("Base64 decoding of given username:password failed"); - } - - list($userName, $password) = explode(':', $decoded) + [ null, null ]; - - if ( empty($userName) ) { - throw new \RuntimeException("A username must be provided"); - } - elseif ( empty($password) ) { - throw new \RuntimeException("A password must be provided"); - } - - ( new Authorize($user) )->authenticate([ 'email' => $userName, 'username' => $userName ], $password); - - return $user->isLoaded(); - } public function catchRequest(ServerRequestInterface $request) : bool { @@ -59,5 +48,4 @@ class HeaderAuthentication implements AuthorizeMethodInterface return false; } - } \ No newline at end of file diff --git a/src/Entity/DigestAuthUserInterface.php b/src/Entity/DigestAuthUserInterface.php new file mode 100644 index 0000000..7a4e04e --- /dev/null +++ b/src/Entity/DigestAuthUserInterface.php @@ -0,0 +1,9 @@ +authenticator->catchRequest($request) ) { - if ( ! $this->authenticator->connect($request, $this->entity) ) { - return call_user_func($this->loginFailedResponse, [ 'api.process' => "Auth failed" ]); + try { + if ( $this->authenticator->catchRequest($request) ) { + if ( ! $this->authenticator->connect($request, $this->entity) ) { + return call_user_func($this->loginFailedResponse, [ 'api.process' => "Auth failed" ]); + } } } + catch(\Exception $e) { + return call_user_func($this->loginFailedResponse, [ 'api.error_message' => $e->getMessage() ]); + } return $handler->handle($request); }