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