1<?php 2// 3// +----------------------------------------------------------------------+ 4// | PHP Version 5 | 5// +----------------------------------------------------------------------+ 6// | Copyright (c) 1997-2012 The PHP Group | 7// +----------------------------------------------------------------------+ 8// | This source file is subject to version 3.01 of the PHP license, | 9// | that is bundled with this package in the file LICENSE, and is | 10// | available at through the world-wide-web at | 11// | http://www.php.net/license/3_01.txt. | 12// | If you did not receive a copy of the PHP license and are unable to | 13// | obtain it through the world-wide-web, please send a note to | 14// | license@php.net so we can mail you a copy immediately. | 15// +----------------------------------------------------------------------+ 16// | Authors: Martin Jansen <mj@php.net> | 17// | Rui Hirokawa <hirokawa@php.net> | 18// | David Costa <gurugeek@php.net> | 19// +----------------------------------------------------------------------+ 20// 21// $Id$ 22// 23 24require_once "Auth/Auth.php"; 25 26define('AUTH_HTTP_NONCE_TIME_LEN', 16); 27define('AUTH_HTTP_NONCE_HASH_LEN', 32); 28 29// {{{ class Auth_HTTP 30 31/** 32 * PEAR::Auth_HTTP 33 * 34 * The PEAR::Auth_HTTP class provides methods for creating an 35 * HTTP authentication system based on RFC-2617 using PHP. 36 * 37 * Instead of generating an HTML driven form like PEAR::Auth 38 * does, this class sends header commands to the clients which 39 * cause them to present a login box like they are e.g. used 40 * in Apache's .htaccess mechanism. 41 * 42 * This class requires the PEAR::Auth package. 43 * 44 * @notes The HTTP Digest Authentication part is based on 45 * authentication class written by Tom Pike <tom.pike@xiven.com> 46 * 47 * @author Martin Jansen <mj@php.net> 48 * @author Rui Hirokawa <hirokawa@php.net> 49 * @author David Costa <gurugeek@php.net> 50 * @package Auth_HTTP 51 * @extends Auth 52 * @version $Revision$ 53 */ 54class Auth_HTTP extends Auth 55{ 56 57 // {{{ properties 58 59 /** 60 * Authorization method: 'basic' or 'digest' 61 * 62 * @access public 63 * @var string 64 */ 65 var $authType = 'basic'; 66 67 /** 68 * Name of the realm for Basic Authentication 69 * 70 * @access public 71 * @var string 72 * @see drawLogin() 73 */ 74 var $realm = "protected area"; 75 76 /** 77 * Text to send if user hits cancel button 78 * 79 * @access public 80 * @var string 81 * @see drawLogin() 82 */ 83 var $CancelText = "Error 401 - Access denied"; 84 85 /** 86 * option array 87 * 88 * @access public 89 * @var array 90 */ 91 var $options = array(); 92 93 /** 94 * flag to indicate the nonce was stale. 95 * 96 * @access public 97 * @var bool 98 */ 99 var $stale = false; 100 101 /** 102 * opaque string for digest authentication 103 * 104 * @access public 105 * @var string 106 */ 107 var $opaque = 'dummy'; 108 109 /** 110 * digest URI 111 * 112 * @access public 113 * @var string 114 */ 115 var $uri = ''; 116 117 /** 118 * authorization info returned by the client 119 * 120 * @access public 121 * @var array 122 */ 123 var $auth = array(); 124 125 /** 126 * next nonce value 127 * 128 * @access public 129 * @var string 130 */ 131 var $nextNonce = ''; 132 133 /** 134 * nonce value 135 * 136 * @access public 137 * @var string 138 */ 139 var $nonce = ''; 140 141 /** 142 * Holds a reference to the global server variable 143 * @var array 144 */ 145 var $server; 146 147 /** 148 * Holds a reference to the global post variable 149 * @var array 150 */ 151 var $post; 152 153 /** 154 * Holds a reference to the global cookie variable 155 * @var array 156 */ 157 var $cookie; 158 159 160 // }}} 161 // {{{ Constructor 162 163 /** 164 * Constructor 165 * 166 * @param string Type of the storage driver 167 * @param mixed Additional options for the storage driver 168 * (example: if you are using DB as the storage 169 * driver, you have to pass the dsn string here) 170 * 171 * @return void 172 */ 173 function Auth_HTTP($storageDriver, $options = '') 174 { 175 /* set default values for options */ 176 $this->options = array('cryptType' => 'md5', 177 'algorithm' => 'MD5', 178 'qop' => 'auth-int,auth', 179 'opaquekey' => 'moo', 180 'noncekey' => 'moo', 181 'digestRealm' => 'protected area', 182 'forceDigestOnly' => false, 183 'nonceLife' => 300, 184 'sessionSharing' => false, 185 ); 186 187 if (!empty($options['authType'])) { 188 $this->authType = strtolower($options['authType']); 189 } 190 191 if (is_array($options)) { 192 foreach($options as $key => $value) { 193 if (array_key_exists( $key, $this->options)) { 194 $this->options[$key] = $value; 195 } 196 } 197 198 if (!empty($this->options['opaquekey'])) { 199 $this->opaque = md5($this->options['opaquekey']); 200 } 201 } 202 203 $this->Auth($storageDriver, $options); 204 } 205 206 // }}} 207 // {{{ assignData() 208 209 /** 210 * Assign values from $PHP_AUTH_USER and $PHP_AUTH_PW or 'Authorization' header 211 * to internal variables and sets the session id based 212 * on them 213 * 214 * @access public 215 * @return void 216 */ 217 function assignData() 218 { 219 if (method_exists($this, '_importGlobalVariable')) { 220 $this->server = &$this->_importGlobalVariable('server'); 221 } 222 223 224 if ($this->authType == 'basic') { 225 if (!empty($this->server['PHP_AUTH_USER'])) { 226 $this->username = $this->server['PHP_AUTH_USER']; 227 } 228 229 if (!empty($this->server['PHP_AUTH_PW'])) { 230 $this->password = $this->server['PHP_AUTH_PW']; 231 } 232 233 /** 234 * Try to get authentication information from IIS 235 */ 236 if (empty($this->username) && empty($this->password)) { 237 if (!empty($this->server['HTTP_AUTHORIZATION'])) { 238 list($this->username, $this->password) = 239 explode(':', base64_decode(substr($this->server['HTTP_AUTHORIZATION'], 6))); 240 } 241 } 242 } elseif ($this->authType == 'digest') { 243 $this->username = ''; 244 $this->password = ''; 245 246 $this->digest_header = null; 247 if (!empty($this->server['PHP_AUTH_DIGEST'])) { 248 $this->digest_header = $this->server['PHP_AUTH_DIGEST']; 249 $headers = getallheaders(); 250 } else { 251 $headers = getallheaders(); 252 if(isset($headers['Authorization']) && !empty($headers['Authorization'])) { 253 $this->digest_header = substr($headers['Authorization'], 254 strpos($headers['Authorization'],' ')+1); 255 } 256 } 257 258 if($this->digest_header) { 259 $authtemp = explode(',', $this->digest_header); 260 $auth = array(); 261 foreach($authtemp as $key => $value) { 262 $value = trim($value); 263 if(strpos($value,'=') !== false) { 264 $lhs = substr($value,0,strpos($value,'=')); 265 $rhs = substr($value,strpos($value,'=')+1); 266 if(substr($rhs,0,1) == '"' && substr($rhs,-1,1) == '"') { 267 $rhs = substr($rhs,1,-1); 268 } 269 $auth[$lhs] = $rhs; 270 } 271 } 272 } 273 if (!isset($auth['uri']) || !isset($auth['realm'])) { 274 return; 275 } 276 277 if ($this->selfURI() == $auth['uri']) { 278 $this->uri = $auth['uri']; 279 if (substr($headers['Authorization'],0,7) == 'Digest ') { 280 281 $this->authType = 'digest'; 282 283 if (!isset($auth['nonce']) || !isset($auth['username']) || 284 !isset($auth['response']) || !isset($auth['qop']) || 285 !isset($auth['nc']) || !isset($auth['cnonce'])){ 286 return; 287 } 288 289 if ($auth['qop'] != 'auth' && $auth['qop'] != 'auth-int') { 290 return; 291 } 292 293 $this->stale = $this->_judgeStale($auth['nonce']); 294 295 if ($this->nextNonce == false) { 296 return; 297 } 298 299 $this->username = $auth['username']; 300 $this->password = $auth['response']; 301 $this->auth['nonce'] = $auth['nonce']; 302 303 $this->auth['qop'] = $auth['qop']; 304 $this->auth['nc'] = $auth['nc']; 305 $this->auth['cnonce'] = $auth['cnonce']; 306 307 if (isset($auth['opaque'])) { 308 $this->auth['opaque'] = $auth['opaque']; 309 } 310 311 } elseif (substr($headers['Authorization'],0,6) == 'Basic ') { 312 if ($this->options['forceDigestOnly']) { 313 return; // Basic authentication is not allowed. 314 } 315 316 $this->authType = 'basic'; 317 list($username, $password) = 318 explode(':',base64_decode(substr($headers['Authorization'],6))); 319 $this->username = $username; 320 $this->password = $password; 321 } 322 } 323 } else { 324 include_once 'PEAR.php'; 325 return PEAR::throwError('authType is invalid.'); 326 } 327 328 if ($this->options['sessionSharing'] && 329 isset($this->username) && isset($this->password)) { 330 session_id(md5('Auth_HTTP' . $this->username . $this->password)); 331 } 332 333 /** 334 * set sessionName for AUTH, so that the sessionName is different 335 * for distinct realms 336 */ 337 $this->_sessionName = "_authhttp".md5($this->realm); 338 } 339 340 // }}} 341 // {{{ login() 342 343 /** 344 * Login function 345 * 346 * @access private 347 * @return void 348 */ 349 function login() 350 { 351 $login_ok = false; 352 if (method_exists($this, '_loadStorage')) { 353 $this->_loadStorage(); 354 } 355 $this->storage->_auth_obj->_sessionName =& $this->_sessionName; 356 357 /** 358 * When the user has already entered a username, 359 * we have to validate it. 360 */ 361 if (!empty($this->username) && !empty($this->password)) { 362 if ($this->authType == 'basic' && !$this->options['forceDigestOnly']) { 363 if (true === $this->storage->fetchData($this->username, $this->password)) { 364 $login_ok = true; 365 } 366 } else { /* digest authentication */ 367 368 if (!$this->getAuth() || $this->getAuthData('a1') == null) { 369 /* 370 * note: 371 * - only PEAR::DB is supported as container. 372 * - password should be stored in container as plain-text 373 * (if $options['cryptType'] == 'none') or 374 * A1 hashed form (md5('username:realm:password')) 375 * (if $options['cryptType'] == 'md5') 376 */ 377 $dbs = $this->storage; 378 if (!DB::isConnection($dbs->db)) { 379 $dbs->_connect($dbs->options['dsn']); 380 } 381 382 $query = 'SELECT '.$dbs->options['passwordcol']." FROM ".$dbs->options['table']. 383 ' WHERE '.$dbs->options['usernamecol']." = '". 384 $dbs->db->quoteString($this->username)."' "; 385 386 $pwd = $dbs->db->getOne($query); // password stored in container. 387 388 if (DB::isError($pwd)) { 389 include_once 'PEAR.php'; 390 return PEAR::throwError($pwd->getMessage(), $pwd->getCode()); 391 } 392 393 if ($this->options['cryptType'] == 'none') { 394 $a1 = md5($this->username.':'.$this->options['digestRealm'].':'.$pwd); 395 } else { 396 $a1 = $pwd; 397 } 398 399 $this->setAuthData('a1', $a1, true); 400 } else { 401 $a1 = $this->getAuthData('a1'); 402 } 403 404 $login_ok = $this->validateDigest($this->password, $a1); 405 if ($this->nextNonce == false) { 406 $login_ok = false; 407 } 408 } 409 410 if (!$login_ok && is_callable($this->loginFailedCallback)) { 411 call_user_func($this->loginFailedCallback,$this->username, $this); 412 } 413 } 414 415 if (!empty($this->username) && $login_ok) { 416 $this->setAuth($this->username); 417 if (is_callable($this->loginCallback)) { 418 call_user_func($this->loginCallback,$this->username, $this); 419 } 420 } 421 422 /** 423 * If the login failed or the user entered no username, 424 * output the login screen again. 425 */ 426 if (!empty($this->username) && !$login_ok) { 427 $this->status = AUTH_WRONG_LOGIN; 428 } 429 430 if ((empty($this->username) || !$login_ok) && $this->showLogin) { 431 $this->drawLogin($this->storage->activeUser); 432 return; 433 } 434 435 if (!empty($this->username) && $login_ok && $this->authType == 'digest' 436 && $this->auth['qop'] == 'auth') { 437 $this->authenticationInfo(); 438 } 439 } 440 441 // }}} 442 // {{{ drawLogin() 443 444 /** 445 * Launch the login box 446 * 447 * @param string $username Username 448 * @return void 449 * @access private 450 */ 451 function drawLogin($username = "") 452 { 453 /** 454 * Send the header commands 455 */ 456 if ($this->authType == 'basic') { 457 header("WWW-Authenticate: Basic realm=\"".$this->realm."\""); 458 header('HTTP/1.0 401 Unauthorized'); 459 } else if ($this->authType == 'digest') { 460 $this->nonce = $this->_getNonce(); 461 462 $wwwauth = 'WWW-Authenticate: Digest '; 463 $wwwauth .= 'qop="'.$this->options['qop'].'", '; 464 $wwwauth .= 'algorithm='.$this->options['algorithm'].', '; 465 $wwwauth .= 'realm="'.$this->options['digestRealm'].'", '; 466 $wwwauth .= 'nonce="'.$this->nonce.'", '; 467 if ($this->stale) { 468 $wwwauth .= 'stale=true, '; 469 } 470 if (!empty($this->opaque)) { 471 $wwwauth .= 'opaque="'.$this->opaque.'"' ; 472 } 473 $wwwauth .= "\r\n"; 474 if (!$this->options['forceDigestOnly']) { 475 $wwwauth .= 'WWW-Authenticate: Basic realm="'.$this->realm.'"'; 476 } 477 header($wwwauth); 478 header('HTTP/1.0 401 Unauthorized'); 479 } 480 481 /** 482 * This code is only executed if the user hits the cancel 483 * button or if he enters wrong data 3 times. 484 */ 485 if ($this->stale) { 486 echo 'Stale nonce value, please re-authenticate.'; 487 } else { 488 echo $this->CancelText; 489 } 490 exit; 491 } 492 493 // }}} 494 // {{{ setRealm() 495 496 /** 497 * Set name of the current realm 498 * 499 * @access public 500 * @param string $realm Name of the realm 501 * @param string $digestRealm Name of the realm for digest authentication 502 * @return void 503 */ 504 function setRealm($realm, $digestRealm = '') 505 { 506 $this->realm = $realm; 507 if (!empty($digestRealm)) { 508 $this->options['digestRealm'] = $digestRealm; 509 } 510 } 511 512 // }}} 513 // {{{ setCancelText() 514 515 /** 516 * Set the text to send if user hits the cancel button 517 * 518 * @access public 519 * @param string $text Text to send 520 * @return void 521 */ 522 function setCancelText($text) 523 { 524 $this->CancelText = $text; 525 } 526 527 // }}} 528 // {{{ validateDigest() 529 530 /** 531 * judge if the client response is valid. 532 * 533 * @access private 534 * @param string $response client response 535 * @param string $a1 password or hashed password stored in container 536 * @return bool true if success, false otherwise 537 */ 538 function validateDigest($response, $a1) 539 { 540 if (method_exists($this, '_importGlobalVariable')) { 541 $this->server = &$this->_importGlobalVariable('server'); 542 } 543 544 $a2unhashed = $this->server['REQUEST_METHOD'].":".$this->selfURI(); 545 if($this->auth['qop'] == 'auth-int') { 546 if(isset($GLOBALS["HTTP_RAW_POST_DATA"])) { 547 // In PHP < 4.3 get raw POST data from this variable 548 $body = $GLOBALS["HTTP_RAW_POST_DATA"]; 549 } else if($lines = @file('php://input')) { 550 // In PHP >= 4.3 get raw POST data from this file 551 $body = implode("\n", $lines); 552 } else { 553 if (method_exists($this, '_importGlobalVariable')) { 554 $this->post = &$this->_importGlobalVariable('post'); 555 } 556 $body = ''; 557 foreach($this->post as $key => $value) { 558 if($body != '') $body .= '&'; 559 $body .= rawurlencode($key) . '=' . rawurlencode($value); 560 } 561 } 562 563 $a2unhashed .= ':'.md5($body); 564 } 565 566 $a2 = md5($a2unhashed); 567 $combined = $a1.':'. 568 $this->auth['nonce'].':'. 569 $this->auth['nc'].':'. 570 $this->auth['cnonce'].':'. 571 $this->auth['qop'].':'. 572 $a2; 573 $expectedResponse = md5($combined); 574 575 if(!isset($this->auth['opaque']) || $this->auth['opaque'] == $this->opaque) { 576 if($response == $expectedResponse) { // password is valid 577 if(!$this->stale) { 578 return true; 579 } else { 580 $this->drawLogin(); 581 } 582 } 583 } 584 585 return false; 586 } 587 588 // }}} 589 // {{{ _judgeStale() 590 591 /** 592 * judge if nonce from client is stale. 593 * 594 * @access private 595 * @param string $nonce nonce value from client 596 * @return bool stale 597 */ 598 function _judgeStale($nonce) 599 { 600 $stale = false; 601 602 if(!$this->_decodeNonce($nonce, $time, $hash_cli)) { 603 $this->nextNonce = false; 604 $stale = true; 605 return $stale; 606 } 607 608 if ($time < time() - $this->options['nonceLife']) { 609 $this->nextNonce = $this->_getNonce(); 610 $stale = true; 611 } else { 612 $this->nextNonce = $nonce; 613 } 614 615 return $stale; 616 } 617 618 // }}} 619 // {{{ _nonceDecode() 620 621 /** 622 * decode nonce string 623 * 624 * @access private 625 * @param string $nonce nonce value from client 626 * @param string $time decoded time 627 * @param string $hash decoded hash 628 * @return bool false if nonce is invalid 629 */ 630 function _decodeNonce($nonce, &$time, &$hash) 631 { 632 if (method_exists($this, '_importGlobalVariable')) { 633 $this->server = &$this->_importGlobalVariable('server'); 634 } 635 636 if (strlen($nonce) != AUTH_HTTP_NONCE_TIME_LEN + AUTH_HTTP_NONCE_HASH_LEN) { 637 return false; 638 } 639 640 $time = base64_decode(substr($nonce, 0, AUTH_HTTP_NONCE_TIME_LEN)); 641 $hash_cli = substr($nonce, AUTH_HTTP_NONCE_TIME_LEN, AUTH_HTTP_NONCE_HASH_LEN); 642 643 $hash = md5($time . $this->server['HTTP_USER_AGENT'] . $this->options['noncekey']); 644 645 if ($hash_cli != $hash) { 646 return false; 647 } 648 649 return true; 650 } 651 652 // }}} 653 // {{{ _getNonce() 654 655 /** 656 * return nonce to detect timeout 657 * 658 * @access private 659 * @return string nonce value 660 */ 661 function _getNonce() 662 { 663 if (method_exists($this, '_importGlobalVariable')) { 664 $this->server = &$this->_importGlobalVariable('server'); 665 } 666 667 $time = time(); 668 $hash = md5($time . $this->server['HTTP_USER_AGENT'] . $this->options['noncekey']); 669 670 return base64_encode($time) . $hash; 671 } 672 673 // }}} 674 // {{{ authenticationInfo() 675 676 /** 677 * output HTTP Authentication-Info header 678 * 679 * @notes md5 hash of contents is required if 'qop' is 'auth-int' 680 * 681 * @access private 682 * @param string MD5 hash of content 683 */ 684 function authenticationInfo($contentMD5 = '') { 685 686 if($this->getAuth() && ($this->getAuthData('a1') != null)) { 687 $a1 = $this->getAuthData('a1'); 688 689 // Work out authorisation response 690 $a2unhashed = ":".$this->selfURI(); 691 if($this->auth['qop'] == 'auth-int') { 692 $a2unhashed .= ':'.$contentMD5; 693 } 694 $a2 = md5($a2unhashed); 695 $combined = $a1.':'. 696 $this->nonce.':'. 697 $this->auth['nc'].':'. 698 $this->auth['cnonce'].':'. 699 $this->auth['qop'].':'. 700 $a2; 701 702 // Send authentication info 703 $wwwauth = 'Authentication-Info: '; 704 if($this->nonce != $this->nextNonce) { 705 $wwwauth .= 'nextnonce="'.$this->nextNonce.'", '; 706 } 707 $wwwauth .= 'qop='.$this->auth['qop'].', '; 708 $wwwauth .= 'rspauth="'.md5($combined).'", '; 709 $wwwauth .= 'cnonce="'.$this->auth['cnonce'].'", '; 710 $wwwauth .= 'nc='.$this->auth['nc'].''; 711 header($wwwauth); 712 } 713 } 714 // }}} 715 // {{{ setOption() 716 /** 717 * set authentication option 718 * 719 * @access public 720 * @param mixed $name key of option 721 * @param mixed $value value of option 722 * @return void 723 */ 724 function setOption($name, $value = null) 725 { 726 if (is_array($name)) { 727 foreach($name as $key => $value) { 728 if (array_key_exists( $key, $this->options)) { 729 $this->options[$key] = $value; 730 } 731 } 732 } else { 733 if (array_key_exists( $name, $this->options)) { 734 $this->options[$name] = $value; 735 } 736 } 737 } 738 739 // }}} 740 // {{{ getOption() 741 /** 742 * get authentication option 743 * 744 * @access public 745 * @param string $name key of option 746 * @return mixed option value 747 */ 748 function getOption($name) 749 { 750 if (array_key_exists( $name, $this->options)) { 751 return $this->options[$name]; 752 } 753 if ($name == 'CancelText') { 754 return $this->CancelText; 755 } 756 if ($name == 'Realm') { 757 return $this->realm; 758 } 759 return false; 760 } 761 762 // }}} 763 // {{{ selfURI() 764 /** 765 * get self URI 766 * 767 * @access public 768 * @return string self URI 769 */ 770 function selfURI() 771 { 772 if (method_exists($this, '_importGlobalVariable')) { 773 $this->server = &$this->_importGlobalVariable('server'); 774 } 775 776 if (preg_match("/MSIE/",$this->server['HTTP_USER_AGENT'])) { 777 // query string should be removed for MSIE 778 $uri = preg_replace("/^(.*)\?/","\\1",$this->server['REQUEST_URI']); 779 } else { 780 $uri = $this->server['REQUEST_URI']; 781 } 782 return $uri; 783 } 784 785 // }}} 786 787} 788 789// }}} 790 791/* 792 * Local variables: 793 * tab-width: 4 794 * c-basic-offset: 4 795 * End: 796 */ 797?> 798