History | Log In     View a printable version of the current page.  
Issue Details (XML | Word | Printable)

Key: ZF-1114
Type: New Feature New Feature
Status: Open Open
Priority: Major Major
Assignee: Shahar Evron
Reporter: Jack Sleight
Votes: 1
Watchers: 4
Operations

If you were logged in you would be able to see more operations.
Google issue summary
Zend Framework

Support for digest authentication in Zend_Http_Client

Created: 22/Mar/07 11:55 AM   Updated: 20/Mar/09 03:49 AM
Component/s: Zend_Http_Client
Affects Version/s: None
Fix Version/s: None

Time Tracking:
Not Specified

Tags:
Participants: Benjamin Eberlei, Darby Felton, Jack Sleight and Shahar Evron


 Description  « Hide
Add support for digest authentication in Zend_Http_Client. Quote from a previous email of mine:

From what I can tell, the way the client currently works is that the end user specifies that they want to use basic or digest authentication, and then the client automatically sends an Authorization header, regardless of whether or not the server needs it. This isn't really the way it should work. The user should just specify their username and password, and upon the initial request, the server will respond with an authentication challenge (basic or digest) to authorize the user. This challenge dictates which authentication method is required by the server, and in the case of digest authentication, provides the variables needed to calculate the response.

Because there are server provided variables required to calculate the digest response, we can't just write out the Authorization header before any communication with the server has taken place, so the way the client currently works is unsuitable. Within the client, I think authentication should be handled in a similar way to redirects, where each server response is checked for an authentication challenge, and if provided, the client can automatically take care of it (so long as the user has provided a username and password).



 All   Comments   Work Log   Change History   FishEye   Crucible      Sort Order: Ascending order - Click to sort in descending order
Shahar Evron - 23/Mar/07 02:02 PM
note: look at the implementation of Zend_Auth_Http_Digest - I don't remember ever looking at it - might be useful

Shahar Evron - 23/Mar/07 02:03 PM
Sorry - Zend_Auth_Adapter_Http that is

Jack Sleight - 24/Mar/07 02:55 PM
This is the sample script that contains all the functions needed for digest authentication, it currently runs externally from Zend_Http_Client, and is not intended to be Zend Framework ready code (comments are missing and the structure will change when integrated with Zend_Http_Client):
<?php
// configure error reporting
error_reporting(E_ALL | E_STRICT);

// set include paths for zend framework
set_include_path(PATH_SEPARATOR . get_include_path()
	. PATH_SEPARATOR . './library/');

// load loader and set autoload function
require_once 'Zend/Loader.php';
spl_autoload_register(array('Zend_Loader', 'autoload'));
	
/*-----------------------------------------------------------*/

$client = new Zend_Http_Client('http://services.msn.com/svcs/hotmail/httpmail.asp', array(
	'useragent' => 'Outlook-Express/6.0',
	'maxredirects' => 0));
$client->setMethod('PROPFIND');

$digest = new Digest($client, 'PROPFIND', 'zftest@hotmail.co.uk', 'password123');
$digest->request();

/*-----------------------------------------------------------*/

class Digest
{
	private $client;
	private $method; // we shouldnt have to specify this, it should be possible to get it from the client
	private $username;
	private $password;
	
	private $nonce;
	private $nc = 1;
	private $a1;
	
	public function __construct(Zend_Http_Client $client, $method, $username, $password)
	{
		$this->client = $client;
		$this->method = $method;
		$this->username = $username;
		$this->password = $password;
	}
	
	public function request()
	{
		// make initial request		
		$response = $this->client->request();		
		Zend_Debug::dump($response->getHeaders(), 'Initial Request Response');
		
		// check the status
		if ($response->getStatus() != 401) {
			return $response;
		}
		
		// get the authenticate header (we might also want to try for proxy-authenticate)
		$resHeader = $response->getHeader('www-authenticate');
		
		// check that authentication digest has been requested
		if (!preg_match('/^Digest/i', $resHeader)) {
			throw new Zend_Exception('server does not require digest authentication');
		}
		
		// take the response header params and create a request header
		$resParams = $this->splitHeader($resHeader);
		$reqParams = $this->calculateParams($resParams, $response);
		$reqHeader = $this->joinHeader($reqParams);
		
		Zend_Debug::dump($reqHeader, 'Authorization Request Header');
		
		// set header and re-request
		$this->client->setHeaders('Authorization', $reqHeader);
		$response = $this->client->request();		
		Zend_Debug::dump($response->getHeaders(), 'Authorization Request Response');
			
		return $response;
	}
	
	private function calculateParams($params, $response)
	{	
		// generate a random client nonce value
		$cnonce = md5(microtime(true));
		
		// check we have the minumum requirements
		if (!isset($params['realm'])) {
			throw new Zend_Exception('authentication realm parameter missing');
		}
		if (!isset($params['nonce'])) {
			throw new Zend_Exception('authentication nonce parameter missing');
		}
		
		// check if we are retrying the nonce value
		if (isset($this->nonce)) {
			if ($this->nonce == $params['nonce']) {
				$this->nc++;
			} else {
				$this->nonce = $params['nonce'];
				$this->nc = 1;
			}
		} else {
			$this->nonce = $params['nonce'];
		}

		// convert decimal nc to hex
		$nc = dechex($this->nc);
		
		// set required values
		$result = array(
			'username'	=> '"' . $this->username . '"',
			'realm'		=> '"' . $params['realm'] . '"',
			'nonce'		=> '"' . $params['nonce'] . '"',
			'uri'		=> '"' . $this->client->getUri()->getPath() . '"',
		);
	
		// check for a qop value
		if (isset($params['qop'])) {
			$qops = preg_split('/;\s+/', $params['qop']);
			if (in_array('auth', $qops)) {
				$qop = 'auth';
			} 
			/* // todo
			elseif (in_array('auth-int', $qops)) {
				$qop = 'auth-int';
			}
			*/
		}
		
		// check for an algorithm value
		if (isset($params['algorithm'])) {
			if ($params['algorithm'] == 'MD5' || $params['algorithm'] == 'MD5-sess') {
				$algorithm = $params['algorithm'];
			}
		}
		
		// if qop is specified add parameters to result
		if(isset($qop)) {
			$result['qop']	  = $qop;
			$result['nc']	  = $nc;
			$result['cnonce'] = '"' . $cnonce . '"';
		}
		
		// if an algorithm is specified add parameter to result
		if(isset($algorithm)) {
			$result['algorithm'] = $algorithm;
		}
		
		// generate the A1 string based on the algorithm value
		if (!isset($algorithm) || $algorithm == 'MD5') {
			$a1 = $this->username . ':' . $params['realm'] . ':' . $this->password;
			$this->a1 = null;
		} elseif ($algorithm == 'MD5-sess') {
			if (isset($this->a1)) {
				$a1 = $this->a1;
			} else {
				$a1 = $this->h($this->username . ':' . $params['realm'] . ':' . $this->password) . ':' . $params['nonce'] . ':' . $cnonce;
				$this->a1 = $a1;
			}
		}
		
		// generate secret value
		$secret = $this->h($a1);
		
		// generate the A2 string based on the qop value
		if (!isset($qop) || $qop == 'auth') {
			$a2 = $this->method . ':' . $this->client->getUri()->getPath();
		} 
		/* //todo
		elseif($qop == 'auth-int') { 
			$a2 = $this->method . ':' . $uri . ':' . *entity-body*;
		}
		*/
		
		// generate the response hash based on the qop value
		if(!isset($qop)) {
			$response = $this->kd($secret, $params['nonce'] . ':' . $a2);
		} elseif($qop == 'auth' || $qop == 'auth-int') {
			$response = $this->kd($secret, $params['nonce'] . ':' . $nc . ':' . $cnonce . ':' . $qop . ':' . $a2);
		}
		
		// add response value to result
		$result['response'] = '"' . $response . '"';
			
		// check if an opaque value was sent
		if (isset($params['opaque'])) {
			$result['opaque'] = '"' . $params['opaque'] . '"';
		}
		
		return $result;
	}
	
	private function h($data)
	{
		return md5($data);
	}
	
	private function kd($secret, $data)
	{
		return $this->h($secret . ':' . $data);
	}
	
	private function splitHeader($header)
	{	
		$params = array();
	
		// remove the leading Digest string
		$header = preg_replace('/^Digest\s+(.*)$/i', '$1', $header);
		
		// match all individual parts of the header
		preg_match_all('/([^=]+)=("[^"]+"|[^,]+)(?:,\s*|$)/', $header, $matches);
		
		// loop through matches remove quotes and add to array
		foreach ($matches[1] as $key => $name) {
			$params[$name] = trim($matches[2][$key], '"');
		}
		
		return $params;
	}
	
	private function joinHeader($params)
	{	
		$header  = 'Digest ';
		
		// loop through params and add to header
		foreach ($params as $name => $value) {
			$header .= $name . '=' . $value . ', ';
		}
		
		// trim trailing comma-space
		$header = rtrim($header, ', ');
		
		return $header;	
	}

}

Jack Sleight - 24/Mar/07 02:58 PM
As is probably obvious, the __construct() and request() functions in the class above are only there for the purposes of the test script, and don't play any part in the actual calculation of the digest response.

Darby Felton - 19/Jul/07 04:21 PM
Assigning to Shahar Evron to clear unassigned issues.

Jack Sleight - 19/Jul/07 06:49 PM
Modified description to include a proposal for changing the authentication behaviour of Zend_Http_Client. Also, in the code above, the only functions actually relevant to receiving and responding to a digest authentication challenge are the calculateParams(), h() and kd() functions. The rest are merely there to allow the script to function, and some (splitHeader()) already exist in one form or another in the Zend_Http_Client (from what I remember).

Benjamin Eberlei - 20/Mar/09 03:49 AM
Can this issue be closed in relation with ZF-3616? cURL supports Diggest authentication