Skip to content

Digest authorization #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 1 addition & 23 deletions src/AuthenticationMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
21 changes: 21 additions & 0 deletions src/AuthorizationResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
8 changes: 2 additions & 6 deletions src/BasicAuthorizationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' => '',
]);
}

Expand Down
25 changes: 23 additions & 2 deletions src/CredentialAdapter/ArrayUserPassword.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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],
]);
}

}
8 changes: 8 additions & 0 deletions src/CredentialAdapter/Exception/UsernameNotFoundException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace PhpMiddleware\HttpAuthentication\CredentialAdapter\Exception;

class UsernameNotFoundException extends \DomainException
{

}
8 changes: 8 additions & 0 deletions src/CredentialAdapter/HashProviderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace PhpMiddleware\HttpAuthentication\CredentialAdapter;

interface HashProviderInterface
{
public function getHash($username, $realm);
}
73 changes: 71 additions & 2 deletions src/DigestAuthorizationService.php
Original file line number Diff line number Diff line change
@@ -1,14 +1,83 @@
<?php


namespace PhpMiddleware\HttpAuthentication;

use PhpMiddleware\HttpAuthentication\CredentialAdapter\Exception\UsernameNotFoundException;
use PhpMiddleware\HttpAuthentication\CredentialAdapter\HashProviderInterface;
use Psr\Http\Message\ServerRequestInterface;

final class DigestAuthorizationService implements AuthorizationServiceInterface
{
private $hashProvider;
private $realm;

public function __construct(HashProviderInterface $hashProvider, $realm)
{
$this->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;
}
}
50 changes: 50 additions & 0 deletions src/RequestBuilder/Digest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace PhpMiddleware\HttpAuthentication\RequestBuilder;

use PhpMiddleware\HttpAuthentication\Util;
use Psr\Http\Message\RequestInterface;

/**
* @link https://tools.ietf.org/html/rfc2069
*/
final class Digest implements RequestBuilderInterface
{
private $username;
private $password;
private $realm;
private $nonce;

public function __construct($username, $password, $realm, $nonce)
{
$this->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);
}
}
55 changes: 55 additions & 0 deletions src/Util.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace PhpMiddleware\HttpAuthentication;

final class Util
{
/**
* @codeCoverageIgnore
*/
private function __construct()
{
}

/**
* @param array $params
*
* @return string md5
*/
public static function md5Implode(array $params)
{
return md5(implode(':', $params));
}

/**
* @param string $scheme
* @param array $challenges
*
* @return string
*/
public static function buildHeader($scheme, array $challenges)
{
$challenge = self::buildChallengeString($challenges);

if (empty($challenge)) {
return $scheme;
}

return sprintf('%s %s', $scheme, $challenge);
}

/**
* @param array $serviceChallenge
*
* @return string
*/
private static function buildChallengeString(array $serviceChallenge)
{
$challengePairs = [];

foreach ($serviceChallenge as $challenge => $value) {
$challengePairs[] = sprintf('%s="%s"', $challenge, addslashes($value));
}
return implode(', ', $challengePairs);
}
}
15 changes: 15 additions & 0 deletions test/CredentialAdapter/ArrayUserPasswordTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 [
Expand Down
Loading