1<?php 2 3namespace Sabre\HTTP\Auth; 4 5use Sabre\HTTP\RequestInterface; 6use Sabre\HTTP\ResponseInterface; 7 8/** 9 * HTTP Digest Authentication handler 10 * 11 * Use this class for easy http digest authentication. 12 * Instructions: 13 * 14 * 1. Create the object 15 * 2. Call the setRealm() method with the realm you plan to use 16 * 3. Call the init method function. 17 * 4. Call the getUserName() function. This function may return null if no 18 * authentication information was supplied. Based on the username you 19 * should check your internal database for either the associated password, 20 * or the so-called A1 hash of the digest. 21 * 5. Call either validatePassword() or validateA1(). This will return true 22 * or false. 23 * 6. To make sure an authentication prompt is displayed, call the 24 * requireLogin() method. 25 * 26 * @copyright Copyright (C) fruux GmbH (https://fruux.com/) 27 * @author Evert Pot (http://evertpot.com/) 28 * @license http://sabre.io/license/ Modified BSD License 29 */ 30class Digest extends AbstractAuth { 31 32 /** 33 * These constants are used in setQOP(); 34 */ 35 const QOP_AUTH = 1; 36 const QOP_AUTHINT = 2; 37 38 protected $nonce; 39 protected $opaque; 40 protected $digestParts; 41 protected $A1; 42 protected $qop = self::QOP_AUTH; 43 44 /** 45 * Initializes the object 46 */ 47 function __construct($realm = 'SabreTooth', RequestInterface $request, ResponseInterface $response) { 48 49 $this->nonce = uniqid(); 50 $this->opaque = md5($realm); 51 parent::__construct($realm, $request, $response); 52 53 } 54 55 /** 56 * Gathers all information from the headers 57 * 58 * This method needs to be called prior to anything else. 59 * 60 * @return void 61 */ 62 function init() { 63 64 $digest = $this->getDigest(); 65 $this->digestParts = $this->parseDigest($digest); 66 67 } 68 69 /** 70 * Sets the quality of protection value. 71 * 72 * Possible values are: 73 * Sabre\HTTP\DigestAuth::QOP_AUTH 74 * Sabre\HTTP\DigestAuth::QOP_AUTHINT 75 * 76 * Multiple values can be specified using logical OR. 77 * 78 * QOP_AUTHINT ensures integrity of the request body, but this is not 79 * supported by most HTTP clients. QOP_AUTHINT also requires the entire 80 * request body to be md5'ed, which can put strains on CPU and memory. 81 * 82 * @param int $qop 83 * @return void 84 */ 85 function setQOP($qop) { 86 87 $this->qop = $qop; 88 89 } 90 91 /** 92 * Validates the user. 93 * 94 * The A1 parameter should be md5($username . ':' . $realm . ':' . $password); 95 * 96 * @param string $A1 97 * @return bool 98 */ 99 function validateA1($A1) { 100 101 $this->A1 = $A1; 102 return $this->validate(); 103 104 } 105 106 /** 107 * Validates authentication through a password. The actual password must be provided here. 108 * It is strongly recommended not store the password in plain-text and use validateA1 instead. 109 * 110 * @param string $password 111 * @return bool 112 */ 113 function validatePassword($password) { 114 115 $this->A1 = md5($this->digestParts['username'] . ':' . $this->realm . ':' . $password); 116 return $this->validate(); 117 118 } 119 120 /** 121 * Returns the username for the request 122 * 123 * @return string 124 */ 125 function getUsername() { 126 127 return $this->digestParts['username']; 128 129 } 130 131 /** 132 * Validates the digest challenge 133 * 134 * @return bool 135 */ 136 protected function validate() { 137 138 $A2 = $this->request->getMethod() . ':' . $this->digestParts['uri']; 139 140 if ($this->digestParts['qop'] == 'auth-int') { 141 // Making sure we support this qop value 142 if (!($this->qop & self::QOP_AUTHINT)) return false; 143 // We need to add an md5 of the entire request body to the A2 part of the hash 144 $body = $this->request->getBody($asString = true); 145 $this->request->setBody($body); 146 $A2 .= ':' . md5($body); 147 } else { 148 149 // We need to make sure we support this qop value 150 if (!($this->qop & self::QOP_AUTH)) return false; 151 } 152 153 $A2 = md5($A2); 154 155 $validResponse = md5("{$this->A1}:{$this->digestParts['nonce']}:{$this->digestParts['nc']}:{$this->digestParts['cnonce']}:{$this->digestParts['qop']}:{$A2}"); 156 157 return $this->digestParts['response'] == $validResponse; 158 159 160 } 161 162 /** 163 * Returns an HTTP 401 header, forcing login 164 * 165 * This should be called when username and password are incorrect, or not supplied at all 166 * 167 * @return void 168 */ 169 function requireLogin() { 170 171 $qop = ''; 172 switch ($this->qop) { 173 case self::QOP_AUTH : 174 $qop = 'auth'; 175 break; 176 case self::QOP_AUTHINT : 177 $qop = 'auth-int'; 178 break; 179 case self::QOP_AUTH | self::QOP_AUTHINT : 180 $qop = 'auth,auth-int'; 181 break; 182 } 183 184 $this->response->addHeader('WWW-Authenticate', 'Digest realm="' . $this->realm . '",qop="' . $qop . '",nonce="' . $this->nonce . '",opaque="' . $this->opaque . '"'); 185 $this->response->setStatus(401); 186 187 } 188 189 190 /** 191 * This method returns the full digest string. 192 * 193 * It should be compatibile with mod_php format and other webservers. 194 * 195 * If the header could not be found, null will be returned 196 * 197 * @return mixed 198 */ 199 function getDigest() { 200 201 return $this->request->getHeader('Authorization'); 202 203 } 204 205 206 /** 207 * Parses the different pieces of the digest string into an array. 208 * 209 * This method returns false if an incomplete digest was supplied 210 * 211 * @param string $digest 212 * @return mixed 213 */ 214 protected function parseDigest($digest) { 215 216 // protect against missing data 217 $needed_parts = ['nonce' => 1, 'nc' => 1, 'cnonce' => 1, 'qop' => 1, 'username' => 1, 'uri' => 1, 'response' => 1]; 218 $data = []; 219 220 preg_match_all('@(\w+)=(?:(?:")([^"]+)"|([^\s,$]+))@', $digest, $matches, PREG_SET_ORDER); 221 222 foreach ($matches as $m) { 223 $data[$m[1]] = $m[2] ? $m[2] : $m[3]; 224 unset($needed_parts[$m[1]]); 225 } 226 227 return $needed_parts ? false : $data; 228 229 } 230 231} 232