Skip to content

Commit b73427a

Browse files
committed
Initial support for digest authorization
1 parent 2ab1014 commit b73427a

12 files changed

+360
-33
lines changed

src/AuthenticationMiddleware.php

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -70,28 +70,6 @@ public function getAuthorizationResult()
7070
*/
7171
private function buildWwwAuthenticateHeader(AuthorizationResultInterface $result)
7272
{
73-
$scheme = $result->getScheme();
74-
$challenge = $this->buildChallengeString($result->getChallenge());
75-
76-
if (empty($challenge)) {
77-
return $scheme;
78-
}
79-
80-
return sprintf('%s %s', $scheme, $challenge);
81-
}
82-
83-
/**
84-
* @param array $serviceChallenge
85-
*
86-
* @return type
87-
*/
88-
private function buildChallengeString(array $serviceChallenge)
89-
{
90-
$challengePairs = [];
91-
92-
foreach ($serviceChallenge as $challenge => $value) {
93-
$challengePairs[] = sprintf('%s="%s"', $challenge, addslashes($value));
94-
}
95-
return implode(', ', $challengePairs);
73+
return Util::buildHeader($result->getScheme(), $result->getChallenge());
9674
}
9775
}

src/AuthorizationResult.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,27 @@ public static function notAuthorized($scheme, array $challenge = [], array $attr
6464
return $instance;
6565
}
6666

67+
/**
68+
* @param string $scheme
69+
* @param array $challenge
70+
* @param array $attributes
71+
*
72+
* @return self
73+
*/
74+
public static function error($scheme, $error, $errorDescription, array $challenge = [], array $attributes = [])
75+
{
76+
$challenge['error'] = $error;
77+
$challenge['error_description'] = $errorDescription;
78+
79+
$instance = new self();
80+
$instance->isAuthorized = false;
81+
$instance->scheme = $scheme;
82+
$instance->challenge = $challenge;
83+
$instance->attributes = $attributes;
84+
85+
return $instance;
86+
}
87+
6788
/**
6889
* @return array
6990
*/

src/BasicAuthorizationService.php

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,18 +51,14 @@ public function authorize(ServerRequestInterface $request)
5151
if ($result === true) {
5252
return AuthorizationResult::authorized(self::SCHEME, [], ['user-ID' => $userId]);
5353
} elseif ($result === false) {
54-
return AuthorizationResult::notAuthorized(self::SCHEME, [
54+
return AuthorizationResult::error(self::SCHEME, 'Invalid credentials', 'Login and/or password are invalid', [
5555
'realm' => $this->realm,
56-
'error' => 'Invalid credentials',
57-
'error_description' => 'Login and/or password are invalid',
5856
]);
5957
}
6058
throw new UnexpectedValueException(sprintf('%s\'s result must be a boolean value', UserPasswordInterface::class));
6159
}
62-
return AuthorizationResult::notAuthorized(self::SCHEME, [
60+
return AuthorizationResult::error(self::SCHEME, 'Invalid header', 'Cannot read user-ID and password from header', [
6361
'realm' => $this->realm,
64-
'error' => '',
65-
'error_description' => '',
6662
]);
6763
}
6864

src/CredentialAdapter/ArrayUserPassword.php

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
namespace PhpMiddleware\HttpAuthentication\CredentialAdapter;
44

5-
final class ArrayUserPassword implements UserPasswordInterface
5+
use PhpMiddleware\HttpAuthentication\CredentialAdapter\Exception\UsernameNotFoundException;
6+
use PhpMiddleware\HttpAuthentication\Util;
7+
8+
final class ArrayUserPassword implements UserPasswordInterface, HashProviderInterface
69
{
710
/**
811
* @var array
@@ -25,6 +28,24 @@ public function __construct(array $users)
2528
*/
2629
public function authenticate($username, $password)
2730
{
28-
return isset($this->users[$username]) && $this->users[$username] === $password;
31+
return $this->isUserNameExists($username) && $this->users[$username] === $password;
32+
}
33+
34+
private function isUserNameExists($username)
35+
{
36+
return isset($this->users[$username]);
2937
}
38+
39+
public function getHash($username, $realm)
40+
{
41+
if (!$this->isUserNameExists($username)) {
42+
throw new UsernameNotFoundException('Username does not exist');
43+
}
44+
return Util::md5Implode([
45+
$username,
46+
$realm,
47+
$this->users[$username],
48+
]);
49+
}
50+
3051
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace PhpMiddleware\HttpAuthentication\CredentialAdapter\Exception;
4+
5+
class UsernameNotFoundException extends \DomainException
6+
{
7+
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace PhpMiddleware\HttpAuthentication\CredentialAdapter;
4+
5+
interface HashProviderInterface
6+
{
7+
public function getHash($username, $realm);
8+
}

src/DigestAuthorizationService.php

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,83 @@
11
<?php
22

3-
43
namespace PhpMiddleware\HttpAuthentication;
54

5+
use PhpMiddleware\HttpAuthentication\CredentialAdapter\Exception\UsernameNotFoundException;
6+
use PhpMiddleware\HttpAuthentication\CredentialAdapter\HashProviderInterface;
67
use Psr\Http\Message\ServerRequestInterface;
78

89
final class DigestAuthorizationService implements AuthorizationServiceInterface
910
{
11+
private $hashProvider;
12+
private $realm;
13+
14+
public function __construct(HashProviderInterface $hashProvider, $realm)
15+
{
16+
$this->hashProvider = $hashProvider;
17+
$this->realm = $realm;
18+
}
19+
1020
public function authorize(ServerRequestInterface $request)
1121
{
12-
throw new \BadMethodCallException('Not implemented');
22+
$header = $request->getHeaderLine('Authorization');
23+
24+
$authorization = $this->parseAuthorizationHeader($header);
25+
26+
if (!$authorization) {
27+
return AuthorizationResult::error('digest', 'Invalid header', 'Cannot read data from Authorization header', [
28+
'realm' => $this->realm,
29+
]);
30+
}
31+
32+
$result = $this->checkAuthentication($authorization, $request->getMethod());
33+
34+
if ($result) {
35+
return AuthorizationResult::authorized('digest');
36+
}
37+
return AuthorizationResult::notAuthorized('digest', [], $authorization);
38+
}
39+
40+
private function checkAuthentication(array $authorization, $method)
41+
{
42+
if ($authorization['realm'] !== $this->realm) {
43+
return false;
44+
}
45+
try {
46+
$A1 = $this->hashProvider->getHash($authorization['username'], $this->realm);
47+
} catch (UsernameNotFoundException $exception) {
48+
return false;
49+
}
50+
51+
$A2 = Util::md5Implode([$method, $authorization['uri']]);
52+
53+
$realResponse = Util::md5Implode([$A1, $authorization['nonce'], $A2]);
54+
55+
return $authorization['response'] === $realResponse;
56+
}
57+
58+
private function parseAuthorizationHeader($header)
59+
{
60+
if (strpos($header, 'Digest') !== 0) {
61+
return false;
62+
}
63+
64+
$neededParts = ['nonce' => 1, 'realm' => 1, 'username' => 1, 'uri' => 1, 'response' => 1];
65+
$neededPartsString = implode('|', array_keys($neededParts));
66+
$data = [];
67+
68+
preg_match_all('@('.$neededPartsString.')=(?:([\'"])([^\2]+?)\2|([^\s,]+))@', substr($header, 7), $matches, PREG_SET_ORDER);
69+
70+
if (is_array($matches)) {
71+
foreach ($matches as $match) {
72+
$data[$match[1]] = $match[3] ?: $match[4];
73+
unset($neededParts[$match[1]]);
74+
}
75+
}
76+
77+
if (!empty($neededParts)) {
78+
return false;
79+
}
80+
81+
return $data;
1382
}
1483
}

src/RequestBuilder/Digest.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
namespace PhpMiddleware\HttpAuthentication\RequestBuilder;
4+
5+
use PhpMiddleware\HttpAuthentication\Util;
6+
use Psr\Http\Message\RequestInterface;
7+
8+
/**
9+
* @link https://tools.ietf.org/html/rfc2069
10+
*/
11+
final class Digest implements RequestBuilderInterface
12+
{
13+
private $username;
14+
private $password;
15+
private $realm;
16+
private $nonce;
17+
18+
public function __construct($username, $password, $realm, $nonce)
19+
{
20+
$this->username = $username;
21+
$this->password = $password;
22+
$this->realm = $realm;
23+
$this->nonce = $nonce;
24+
}
25+
26+
/**
27+
* @param RequestInterface $request
28+
*
29+
* @return RequestInterface
30+
*/
31+
public function authenticate(RequestInterface $request)
32+
{
33+
$uri = (string) $request->getUri();
34+
35+
$a1 = Util::md5Implode([$this->username, $this->realm, $this->password]);
36+
$a2 = Util::md5Implode([$request->getMethod(), $uri]);
37+
38+
$response = Util::md5Implode([$a1, $this->nonce, $a2]);
39+
40+
$value = Util::buildHeader('Digest', [
41+
'username' => $this->username,
42+
'realm' => $this->realm,
43+
'nonce' => $this->nonce,
44+
'uri' => $uri,
45+
'response' => $response,
46+
]);
47+
48+
return $request->withHeader('Authorization', $value);
49+
}
50+
}

src/Util.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
namespace PhpMiddleware\HttpAuthentication;
4+
5+
final class Util
6+
{
7+
/**
8+
* @codeCoverageIgnore
9+
*/
10+
private function __construct()
11+
{
12+
}
13+
14+
/**
15+
* @param array $params
16+
*
17+
* @return string md5
18+
*/
19+
public static function md5Implode(array $params)
20+
{
21+
return md5(implode(':', $params));
22+
}
23+
24+
/**
25+
* @param string $scheme
26+
* @param array $challenges
27+
*
28+
* @return string
29+
*/
30+
public static function buildHeader($scheme, array $challenges)
31+
{
32+
$challenge = self::buildChallengeString($challenges);
33+
34+
if (empty($challenge)) {
35+
return $scheme;
36+
}
37+
38+
return sprintf('%s %s', $scheme, $challenge);
39+
}
40+
41+
/**
42+
* @param array $serviceChallenge
43+
*
44+
* @return string
45+
*/
46+
private static function buildChallengeString(array $serviceChallenge)
47+
{
48+
$challengePairs = [];
49+
50+
foreach ($serviceChallenge as $challenge => $value) {
51+
$challengePairs[] = sprintf('%s="%s"', $challenge, addslashes($value));
52+
}
53+
return implode(', ', $challengePairs);
54+
}
55+
}

test/CredentialAdapter/ArrayUserPasswordTest.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PhpMiddlewareTest\HttpAuthentication\CredentialAdapter;
44

55
use PhpMiddleware\HttpAuthentication\CredentialAdapter\ArrayUserPassword;
6+
use PhpMiddleware\HttpAuthentication\CredentialAdapter\Exception\UsernameNotFoundException;
67
use PHPUnit_Framework_TestCase;
78

89
class ArrayUserPasswordTest extends PHPUnit_Framework_TestCase
@@ -38,6 +39,20 @@ public function testNotAuthenticate($username, $password)
3839
$this->assertFalse($result);
3940
}
4041

42+
public function testGetHash()
43+
{
44+
$result = $this->adapter->getHash('boo', 'any-realm');
45+
46+
$this->assertSame(32, strlen($result));
47+
}
48+
49+
public function testInvalidUsername()
50+
{
51+
$this->setExpectedException(UsernameNotFoundException::class);
52+
53+
$this->adapter->getHash('baz', 'any-realm');
54+
}
55+
4156
public function correctDataProvider()
4257
{
4358
return [

0 commit comments

Comments
 (0)