Passed
Push — master ( 4eab39...4163e3 )
by Petr
08:11
created

HttpDigest   A

Complexity

Total Complexity 14

Size/Duplication

Total Lines 86
Duplicated Lines 0 %

Test Coverage

Coverage 80%

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 32
c 1
b 0
f 1
dl 0
loc 86
ccs 28
cts 35
cp 0.8
rs 10
wmc 14

5 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A httpDigestParse() 0 16 4
A remove() 0 3 1
B process() 0 21 7
A authNotExists() 0 4 1
1
<?php
2
3
namespace kalanis\kw_auth\Methods;
4
5
6
use ArrayAccess;
7
use kalanis\kw_accounts\Interfaces\IAuthCert;
8
9
10
/**
11
 * Class HttpDigest
12
 * @package kalanis\kw_auth\AuthMethods
13
 * Authenticate via digest http method
14
 * @link https://serverpilot.io/docs/how-to-perform-http-digest-authentication-with-php/
15
 * @link https://www.php.net/manual/en/features.http-auth.php
16
 */
17
class HttpDigest extends AMethods
18
{
19
    const INPUT_METHOD = 'REQUEST_METHOD';
20
    const INPUT_DIGEST = 'PHP_AUTH_DIGEST';
21
22
    /** @var string */
23
    protected $realm = 'KWCMS_Http_Digest';
24
    /** @var IAuthCert */
25
    protected $authenticator;
26
    /** @var ArrayAccess<string, string|int> */
27
    protected $server = null;
28
29
    /**
30
     * @param IAuthCert $authenticator
31
     * @param AMethods|null $nextOne
32
     * @param ArrayAccess<string, string|int> $server
33
     */
34 6
    public function __construct(IAuthCert $authenticator, ?AMethods $nextOne, ArrayAccess $server)
35
    {
36 6
        parent::__construct($authenticator, $nextOne);
37 6
        $this->server = $server;
38 6
    }
39
40 6
    public function process(ArrayAccess $credentials): void
41
    {
42 6
        if (!$this->server->offsetExists(static::INPUT_DIGEST) || empty($this->server->offsetGet(static::INPUT_DIGEST))) {
43 2
            return;
44
        }
45 4
        $data = $this->httpDigestParse(strval($this->server->offsetGet(static::INPUT_DIGEST)));
46 4
        if (!empty($data)) {
47 3
            $wantedUser = $this->authenticator->getDataOnly(strval($data['username']));
48 3
            $wantedCert = $this->authenticator->getCertData(strval($data['username']));
49 3
            if (!$wantedCert || !$wantedUser) {
50 1
                return;
51
            }
52
53
            // verify
54 2
            $A1 = md5($data['username'] . ':' . $this->realm . ':' . $wantedCert->getPubKey()); // @todo: srsly, pubkey?! have nothing better?
55 2
            $A2 = md5($this->server->offsetGet(static::INPUT_METHOD) . ':' . $data['uri']);
56 2
            $valid_response = md5($A1 . ':' . $data['nonce'] . ':' . $data['nc'] . ':' . $data['cnonce'] . ':' . $data['qop'] . ':' . $A2);
57
58 2
            if ($data['response'] == $valid_response) {
59
                // OK
60 2
                $this->loggedUser = $wantedUser;
61
            }
62
        }
63 3
    }
64
65
    /**
66
     * @codeCoverageIgnore headers
67
     */
68
    public function remove(): void
69
    {
70
        $this->authNotExists();
71
    }
72
73
    /**
74
     * @codeCoverageIgnore headers
75
     */
76
    public function authNotExists(): void
77
    {
78
        header('HTTP/1.1 401 Unauthorized');
79
        header('WWW-Authenticate: Digest realm="' . $this->realm . '",qop="auth",nonce="' . uniqid() . '",opaque="' . md5($this->realm) . '"');
80
    }
81
82
    /**
83
     * Parse the http auth header
84
     * @param string $txt
85
     * @return array<string, string>
86
     */
87 4
    protected function httpDigestParse(string $txt): array
88
    {
89
        // protect against missing data
90 4
        $needed_parts = ['nonce' => 1, 'nc' => 1, 'cnonce' => 1, 'qop' => 1, 'username' => 1, 'uri' => 1, 'response' => 1];
91 4
        $data = [];
92 4
        $keys = implode('|', array_keys($needed_parts));
93
94 4
        preg_match_all('@(' . $keys . ')=(?:([\'"])([^\2]*?)\2|([^\s,]+))@', $txt, $matches, PREG_SET_ORDER);
95
96 4
        foreach ($matches as $m) {
97 3
            $data[strval($m[1])] = strval($m[3] ?: ( $m[4] ?? '' ));
98 3
            unset($needed_parts[$m[1]]);
99
        }
100
101
        // can be and shall be due that unset a few lines above
102 4
        return empty($needed_parts) ? $data : [];
103
    }
104
}
105