Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
99.35% covered (success)
99.35%
152 / 153
98.00% covered (success)
98.00%
49 / 50
CRAP
0.00% covered (danger)
0.00%
0 / 1
Digest
99.35% covered (success)
99.35%
152 / 153
98.00% covered (success)
98.00%
49 / 50
86
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 create
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createFromHeader
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
8
 createFromWwwAuth
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
11
 setWwwAuth
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setRealm
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setUsername
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setPassword
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setUri
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setNonce
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setNonceCount
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setClientNonce
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setMethod
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setAlgorithm
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setQop
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setOpaque
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setBody
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setStale
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getWwwAuth
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRealm
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUsername
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPassword
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUri
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNonce
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNonceCount
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getClientNonce
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMethod
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAlgorithm
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getQop
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOpaque
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBody
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasWwwAuth
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasRealm
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasUsername
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasPassword
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasUri
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasNonce
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasNonceCount
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasClientNonce
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasMethod
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasAlgorithm
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasQop
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasOpaque
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasBody
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isStale
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isValid
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
13
 getErrors
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasErrors
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createDigestString
96.55% covered (success)
96.55%
28 / 29
0.00% covered (danger)
0.00%
0 / 1
8
 __toString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Http\Auth;
16
17use Pop\Mime\Part\Header;
18use Pop\Mime\Part\Header\Value;
19
20/**
21 * HTTP auth digest class
22 *
23 * @category   Pop
24 * @package    Pop\Http
25 * @author     Nick Sagona, III <nick@popphp.org>
26 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
27 * @license    https://www.popphp.org/license     New BSD License
28 * @version    6.0.0
29 */
30class Digest
31{
32
33    /**
34     * Digest constants
35     * @var string
36     */
37    const ALGO_MD5      = 'MD5';
38    const ALGO_MD5_SESS = 'MD5-sess';
39    const QOP_AUTH      = 'auth';
40    const QOP_AUTH_INT  = 'auth-int';
41
42    /**
43     * WWW-Auth Header
44     * @var ?Header
45     */
46    protected ?Header $wwwAuth = null;
47
48    /**
49     * Realm
50     * @var ?string
51     */
52    protected ?string $realm = null;
53
54    /**
55     * Username
56     * @var ?string
57     */
58    protected ?string $username = null;
59
60    /**
61     * Password
62     * @var ?string
63     */
64    protected ?string $password = null;
65
66    /**
67     * Uri string
68     * @var ?string
69     */
70    protected ?string $uri = null;
71
72    /**
73     * Nonce
74     * @var ?string
75     */
76    protected ?string $nonce = null;
77
78    /**
79     * Nonce count
80     * @var ?string
81     */
82    protected ?string $nonceCount = null;
83
84    /**
85     * Client nonce
86     * @var ?string
87     */
88    protected ?string $clientNonce = null;
89
90    /**
91     * Method
92     * @var string
93     */
94    protected string $method = 'GET';
95
96    /**
97     * Algorithm
98     * @var string
99     */
100    protected string $algorithm = self::ALGO_MD5;
101
102    /**
103     * QOP
104     * @var ?string
105     */
106    protected ?string $qop = null;
107
108    /**
109     * Opaque
110     * @var ?string
111     */
112    protected ?string $opaque = null;
113
114    /**
115     * Body
116     * @var ?string
117     */
118    protected ?string $body = null;
119
120    /**
121     * Stale flag
122     * @var bool
123     */
124    protected bool $stale = false;
125
126    /**
127     * Validation errors
128     * @var array
129     */
130    protected array $errors = [];
131
132    /**
133     * Constructor
134     *
135     * Instantiate the auth digest object
136     *
137     * @param  string $realm
138     * @param  string $username
139     * @param  string $password
140     * @param  string $uri
141     * @param  string $nonce
142     */
143    public function __construct(string $realm, string $username, string $password, string $uri, string $nonce)
144    {
145        $this->setRealm($realm);
146        $this->setUsername($username);
147        $this->setPassword($password);
148        $this->setUri($uri);
149        $this->setNonce($nonce);
150    }
151
152    /**
153     * Create digest
154     *
155     * @param  string $realm
156     * @param  string $username
157     * @param  string $password
158     * @param  string $uri
159     * @param  string $nonce
160     * @return Digest
161     */
162    public static function create(
163        string $realm, string $username, string $password, string $uri, string $nonce
164    ): Digest
165    {
166        return new static($realm, $username, $password, $uri, $nonce);
167    }
168
169    /**
170     * Create digest from client header
171     *
172     * @param  string|Header $header
173     * @return Digest
174     */
175    public static function createFromHeader(string|Header $header, $password)
176    {
177        if (is_string($header)) {
178            $header = Header::parse($header);
179            if (($header->getValue()->getScheme() === null) || (trim($header->getValue()->getScheme()) != 'Digest')) {
180                throw new Exception('Error: The auth header is not digest.');
181            }
182        }
183
184        $params = $header->getValue()->getParameters();
185
186        $realm    = $params['realm'] ?? null;
187        $nonce    = $params['nonce'] ?? null;
188        $uri      = $params['uri'] ?? null;
189        $username = $params['username'] ?? null;
190
191        if ($realm === null) {
192            throw new Exception('Error: The realm is not set.');
193        }
194        if ($username === null) {
195            throw new Exception('Error: The username is not set.');
196        }
197        if ($nonce === null) {
198            throw new Exception('Error: The nonce is not set.');
199        }
200        if ($uri === null) {
201            throw new Exception('Error: The URI is not set.');
202        }
203
204        return new static($realm, $username, $password, $uri, $nonce);
205    }
206
207    /**
208     * Create digest from WWW-auth server header
209     *
210     * @param  string|Header $wwwAuth
211     * @param  string $username
212     * @param  string $password
213     * @param  string $uri
214     * @throws Exception
215     * @return Digest
216     */
217    public static function createFromWwwAuth(
218        string|Header $wwwAuth, string $username, string $password, string $uri
219    ): Digest
220    {
221        if (is_string($wwwAuth)) {
222            $wwwAuth = Header::parse($wwwAuth);
223            if (($wwwAuth->getValue()->getScheme() === null) || (trim($wwwAuth->getValue()->getScheme()) != 'Digest')) {
224                throw new Exception('Error: The auth header is not digest.');
225            }
226        }
227
228        $params = $wwwAuth->getValue()->getParameters();
229
230        $realm  = $params['realm'] ?? null;
231        $qop    = $params['qop'] ?? null;
232        $nonce  = $params['nonce'] ?? null;
233        $opaque = $params['opaque'] ?? null;
234        $stale  = $params['stale'] ?? false;
235
236        if ($realm === null) {
237            throw new Exception('Error: The realm is not set.');
238        }
239        if ($nonce === null) {
240            throw new Exception('Error: The nonce is not set.');
241        }
242        if ($opaque === null) {
243            throw new Exception('Error: The opaque is not set.');
244        }
245
246        $digest = new static($realm, $username, $password, $uri, $nonce);
247        $digest->setWwwAuth($wwwAuth)
248            ->setOpaque($opaque);
249
250        if ($qop == 'auth-int') {
251            $digest->setQop(static::QOP_AUTH_INT);
252        } else if (!str_contains($qop, 'auth-int') && str_contains($qop, 'auth')) {
253            $digest->setQop(static::QOP_AUTH);
254        }
255        if ($stale) {
256            $digest->setStale(strtolower((string)$stale) === 'true');
257        }
258
259        return $digest;
260    }
261
262    /**
263     * Set the WWW auth header
264     *
265     * @param  Header $wwwAuth
266     * @return Digest
267     */
268    public function setWwwAuth(Header $wwwAuth): Digest
269    {
270        $this->wwwAuth = $wwwAuth;
271        return $this;
272    }
273
274    /**
275     * Set the realm
276     *
277     * @param  string $realm
278     * @return Digest
279     */
280    public function setRealm(string $realm): Digest
281    {
282        $this->realm = $realm;
283        return $this;
284    }
285
286    /**
287     * Set the username
288     *
289     * @param  string $username
290     * @return Digest
291     */
292    public function setUsername(string $username): Digest
293    {
294        $this->username = $username;
295        return $this;
296    }
297
298    /**
299     * Set the password
300     *
301     * @param  string $password
302     * @return Digest
303     */
304    public function setPassword(string $password): Digest
305    {
306        $this->password = $password;
307        return $this;
308    }
309
310    /**
311     * Set the URI
312     *
313     * @param  string $uri
314     * @return Digest
315     */
316    public function setUri(string $uri): Digest
317    {
318        $this->uri = $uri;
319        return $this;
320    }
321
322    /**
323     * Set the nonce
324     *
325     * @param  string $nonce
326     * @return Digest
327     */
328    public function setNonce(string $nonce): Digest
329    {
330        $this->nonce = $nonce;
331        return $this;
332    }
333
334    /**
335     * Set the nonce count
336     *
337     * @param  string $nonceCount
338     * @return Digest
339     */
340    public function setNonceCount(string $nonceCount): Digest
341    {
342        $this->nonceCount = $nonceCount;
343        return $this;
344    }
345
346    /**
347     * Set the client nonce
348     *
349     * @param  string $clientNonce
350     * @return Digest
351     */
352    public function setClientNonce(string $clientNonce): Digest
353    {
354        $this->clientNonce = $clientNonce;
355        return $this;
356    }
357
358    /**
359     * Set the method
360     *
361     * @param  string $method
362     * @return Digest
363     */
364    public function setMethod(string $method): Digest
365    {
366        $this->method = $method;
367        return $this;
368    }
369
370    /**
371     * Set the algorithm
372     *
373     * @param  string $algorithm
374     * @return Digest
375     */
376    public function setAlgorithm(string $algorithm): Digest
377    {
378        $this->algorithm = $algorithm;
379        return $this;
380    }
381
382    /**
383     * Set the QOP
384     *
385     * @param  string $qop
386     * @return Digest
387     */
388    public function setQop(string $qop): Digest
389    {
390        $this->qop = $qop;
391        return $this;
392    }
393
394    /**
395     * Set the opaque
396     *
397     * @param  string $opaque
398     * @return Digest
399     */
400    public function setOpaque(string $opaque): Digest
401    {
402        $this->opaque = $opaque;
403        return $this;
404    }
405
406    /**
407     * Set the body
408     *
409     * @param  string $body
410     * @return Digest
411     */
412    public function setBody(string $body): Digest
413    {
414        $this->body = $body;
415        return $this;
416    }
417
418    /**
419     * Set stale flag
420     *
421     * @param  bool $stale
422     * @return Digest
423     */
424    public function setStale(bool $stale = false): Digest
425    {
426        $this->stale = $stale;
427        return $this;
428    }
429
430    /**
431     * Get the WWW auth header
432     *
433     * @return string
434     */
435    public function getWwwAuth(): string
436    {
437        return (string)$this->wwwAuth;
438    }
439
440    /**
441     * Get the realm
442     *
443     * @return string
444     */
445    public function getRealm(): string
446    {
447        return $this->realm;
448    }
449
450    /**
451     * Get the $username
452     *
453     * @return string
454     */
455    public function getUsername(): string
456    {
457        return $this->username;
458    }
459
460    /**
461     * Get the password
462     *
463     * @return string
464     */
465    public function getPassword(): string
466    {
467        return $this->password;
468    }
469
470    /**
471     * Get the URI
472     *
473     * @return string
474     */
475    public function getUri(): string
476    {
477        return $this->uri;
478    }
479
480    /**
481     * Get the nonce
482     *
483     * @return string
484     */
485    public function getNonce(): string
486    {
487        return $this->nonce;
488    }
489
490    /**
491     * Get the nonce count
492     *
493     * @return string
494     */
495    public function getNonceCount(): string
496    {
497        return $this->nonceCount;
498    }
499
500    /**
501     * Get the client nonce
502     *
503     * @return string
504     */
505    public function getClientNonce(): string
506    {
507        return $this->clientNonce;
508    }
509
510    /**
511     * Get the method
512     *
513     * @return string
514     */
515    public function getMethod(): string
516    {
517        return $this->method;
518    }
519
520    /**
521     * Get the algorithm
522     *
523     * @return string
524     */
525    public function getAlgorithm(): string
526    {
527        return $this->algorithm;
528    }
529
530    /**
531     * Get the QOP
532     *
533     * @return string
534     */
535    public function getQop(): string
536    {
537        return $this->qop;
538    }
539
540    /**
541     * Get the opaque
542     *
543     * @return string
544     */
545    public function getOpaque(): string
546    {
547        return $this->opaque;
548    }
549
550    /**
551     * Get the body
552     *
553     * @return string
554     */
555    public function getBody(): string
556    {
557        return $this->body;
558    }
559
560    /**
561     * Has WWW auth header
562     *
563     * @return bool
564     */
565    public function hasWwwAuth(): bool
566    {
567        return ($this->wwwAuth !== null);
568    }
569
570    /**
571     * Has realm
572     *
573     * @return bool
574     */
575    public function hasRealm(): bool
576    {
577        return ($this->realm !== null);
578    }
579
580    /**
581     * Has $username
582     *
583     * @return bool
584     */
585    public function hasUsername(): bool
586    {
587        return ($this->username !== null);
588    }
589
590    /**
591     * Has password
592     *
593     * @return bool
594     */
595    public function hasPassword(): bool
596    {
597        return ($this->password !== null);
598    }
599
600    /**
601     * Has URI
602     *
603     * @return bool
604     */
605    public function hasUri(): bool
606    {
607        return ($this->uri !== null);
608    }
609
610    /**
611     * Has nonce
612     *
613     * @return bool
614     */
615    public function hasNonce(): bool
616    {
617        return ($this->nonce !== null);
618    }
619
620    /**
621     * Has nonce count
622     *
623     * @return bool
624     */
625    public function hasNonceCount(): bool
626    {
627        return ($this->nonceCount !== null);
628    }
629
630    /**
631     * Has client nonce
632     *
633     * @return bool
634     */
635    public function hasClientNonce(): bool
636    {
637        return ($this->clientNonce !== null);
638    }
639
640    /**
641     * Has method
642     *
643     * @return bool
644     */
645    public function hasMethod(): bool
646    {
647        return ($this->method !== '');
648    }
649
650    /**
651     * Has algorithm
652     *
653     * @return bool
654     */
655    public function hasAlgorithm(): bool
656    {
657        return ($this->algorithm !== '');
658    }
659
660    /**
661     * Has qop
662     *
663     * @return bool
664     */
665    public function hasQop(): bool
666    {
667        return ($this->qop !== null);
668    }
669
670    /**
671     * Has opaque
672     *
673     * @return bool
674     */
675    public function hasOpaque(): bool
676    {
677        return ($this->opaque !== null);
678    }
679
680    /**
681     * Has body
682     *
683     * @return bool
684     */
685    public function hasBody(): bool
686    {
687        return ($this->body !== null);
688    }
689
690    /**
691     * Is stale
692     *
693     * @return bool
694     */
695    public function isStale(): bool
696    {
697        return $this->stale;
698    }
699
700    /**
701     * Is valid
702     *
703     * @return bool
704     */
705    public function isValid(): bool
706    {
707        $result = true;
708
709        // Check basic required parameters
710        if (($this->realm === null) || ($this->username === null) ||
711            empty($this->password) || ($this->nonce === null)) {
712            $this->errors[] =
713                'Error: One or more of the basic parameters were not set (realm, username, password or nonce).';
714            $result = false;
715        }
716        // Check client nonce for MD5-sess algorithm
717        if (($this->algorithm == self::ALGO_MD5_SESS) && ($this->clientNonce === null)) {
718            $this->errors[] = 'Error: The client nonce was not set for the MD5-sess algorithm.';
719            $result = false;
720        }
721        // Check QOP auth-int and the entity body
722        if (($this->qop == self::QOP_AUTH_INT) && ($this->body === null)) {
723            $this->errors[] = 'Error: The entity body was not set for the auth-int QOP.';
724            $result = false;
725        }
726        // Check QOP auth/auth-int nonce count and client nonce for the response
727        if (!empty($this->qop) && (str_contains($this->qop, 'auth') && (($this->nonceCount === null) || ($this->clientNonce === null)))) {
728            $this->errors[] = 'Error: Either the nonce count or client nonce was not set for the auth QOP.';
729            $result = false;
730        }
731
732        return $result;
733    }
734
735    /**
736     * Get errors
737     *
738     * @return array
739     */
740    public function getErrors(): array
741    {
742        return $this->errors;
743    }
744
745    /**
746     * Has errors
747     *
748     * @return bool
749     */
750    public function hasErrors(): bool
751    {
752        return (!empty($this->errors));
753    }
754
755    /**
756     * Create digest value
757     *
758     * @param  Value $value
759     * @return string
760     */
761    public function createDigestString(Value $value = new Value()): string
762    {
763        $a1 = ($this->algorithm == self::ALGO_MD5_SESS) ?
764            md5(
765                md5($this->username . ':' . $this->realm . ':' . $this->password) .
766                ':' . $this->nonce . ':' . $this->clientNonce
767            ) :
768            md5($this->username . ':' . $this->realm . ':' . $this->password);
769
770        $a2 = ($this->qop == self::QOP_AUTH_INT) ?
771            md5($this->method . ':' . $this->uri . ':' . md5($this->body)) :
772            md5($this->method . ':' . $this->uri);
773
774        $response = ($this->qop !== null) ?
775            md5($a1 . ':' . $this->nonce . ':' .  $this->nonceCount . ':' .  $this->clientNonce . ':' . $this->qop . ':' . $a2) :
776            md5($a1 . ':' . $this->nonce . ':' . $a2);
777
778        $value->setDelimiter(',')
779            ->setScheme('Digest ')
780            ->setForceQuote(true)
781            ->addParameter('username', $this->username)
782            ->addParameter('realm', $this->realm)
783            ->addParameter('nonce', $this->nonce)
784            ->addParameter('uri', $this->uri)
785            ->addParameter('response', $response);
786
787        // A server can't verify the response above without knowing the qop/nc/cnonce it was
788        // computed from, since they all feed into the hash - RFC 2617 requires echoing them
789        // back. opaque, if the server sent one, must also be echoed back unchanged.
790        if ($this->qop !== null) {
791            $value->addParameter('qop', $this->qop);
792            if ($this->nonceCount !== null) {
793                $value->addParameter('nc', $this->nonceCount);
794            }
795            if ($this->clientNonce !== null) {
796                $value->addParameter('cnonce', $this->clientNonce);
797            }
798        }
799        if ($this->opaque !== null) {
800            $value->addParameter('opaque', $this->opaque);
801        }
802
803        return $value->render();
804    }
805
806    /**
807     * Render the header value string
808     *
809     * @return string
810     */
811    public function __toString(): string
812    {
813        return $this->createDigestString();
814    }
815
816}