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