Overview

Packages

  • awl
    • caldav-client-v2
    • RRule
  • davical
    • authentication
      • drivers
    • caldav
    • DAViCalSession
    • DAVTicket
    • external-bind
    • feed
    • HTTPAuthSession
    • iSchedule
    • iSchedule-POST
    • logging
    • metrics
    • Principal
    • propfind
    • PublicSession
    • Request
    • Resource
    • tzservice
  • None
  • PHP

Classes

  • HTTPAuthSession
  • Overview
  • Package
  • Class
  • Tree
  • Deprecated
  • Todo
  1: <?php
  2: /**
  3: * A Class for handling HTTP Authentication
  4: *
  5: * @package davical
  6: * @subpackage HTTPAuthSession
  7: * @author Andrew McMillan <andrew@catalyst.net.nz>
  8: * @copyright Catalyst .Net Ltd
  9: * @license   http://gnu.org/copyleft/gpl.html GNU GPL v2
 10: */
 11: 
 12: /**
 13: * A Class for handling a session using HTTP Basic Authentication
 14: *
 15: * @package davical
 16: */
 17: class HTTPAuthSession {
 18:   /**#@+
 19:   * @access private
 20:   */
 21: 
 22:   /**
 23:   * Username
 24:   * @var username string
 25:   */
 26:   public $username;
 27: 
 28:   /**
 29:   * User ID number
 30:   * @var user_no int
 31:   */
 32:   public $user_no;
 33: 
 34:   /**
 35:   * Principal ID
 36:   * @var principal_id int
 37:   */
 38:   public $principal_id;
 39: 
 40:   /**
 41:   * User e-mail
 42:   * @var email string
 43:   */
 44:   public $email;
 45: 
 46:   /**
 47:   * User full name
 48:   * @var fullname string
 49:   */
 50:   public $fullname;
 51: 
 52:   /**
 53:   * Group rights (not implemented)
 54:   * @todo
 55:   * @var groups array
 56:   */
 57:   public $groups;
 58:   /**#@-*/
 59: 
 60:   /**
 61:   * The constructor, which just calls the type supplied or configured
 62:   */
 63:   function HTTPAuthSession() {
 64:     global $c;
 65: 
 66:     if ( ! empty($_SERVER['PHP_AUTH_DIGEST'])) {
 67:       $this->DigestAuthSession();
 68:     }
 69:     else if ( isset($_SERVER['PHP_AUTH_USER']) || isset($_SERVER["AUTHORIZATION"]) ) {
 70:       $this->BasicAuthSession();
 71:     }
 72:     else if ( isset($c->http_auth_mode) && $c->http_auth_mode == "Digest" ) {
 73:       $this->DigestAuthSession();
 74:     }
 75:     else {
 76:       $this->BasicAuthSession();
 77:     }
 78:   }
 79: 
 80:   /**
 81:   * Authorisation failed, so we send some headers to say so.
 82:   *
 83:   * @param string $auth_header The WWW-Authenticate header details.
 84:   */
 85:   function AuthFailedResponse( $auth_header = "" ) {
 86:     global $c;
 87:     if ( $auth_header == "" ) {
 88:       $auth_realm = $c->system_name;
 89:       if ( isset($c->per_principal_realm) && $c->per_principal_realm && !empty($_SERVER['PATH_INFO']) ) {
 90:         $principal_name = preg_replace( '{^/(.*?)/.*$}', '$1', $_SERVER['PATH_INFO']);
 91:         if ( $principal_name != $_SERVER['PATH_INFO'] ) {
 92:           $auth_realm .= ' - ' . $principal_name;
 93:         }
 94:       }
 95:       dbg_error_log( "HTTPAuth", ":AuthFailedResponse Requesting authentication in the '%s' realm", $auth_realm );
 96:       $auth_header = sprintf( 'WWW-Authenticate: Basic realm="%s"', $auth_realm );
 97:     }
 98: 
 99:     header('HTTP/1.1 401 Unauthorized', true, 401 );
100:     header('Content-type: text/plain; ; charset="utf-8"' );
101:     header( $auth_header );
102:     echo 'Please log in for access to this system.';
103:     if ( isset($_SERVER['PHP_AUTH_USER']) ) {
104:       dbg_error_log( "ERROR", "authentication failure for user '%s' from host [%s]", $_SERVER['PHP_AUTH_USER'], $_SERVER['REMOTE_ADDR'] );
105:     } else {
106:       dbg_error_log( "HTTPAuth", ":Session: User is not authorised: %s ", $_SERVER['REMOTE_ADDR'] );
107:     }
108:     @ob_flush();   exit(0);
109:   }
110: 
111: 
112:   /**
113:   * Handle Basic HTTP Authentication (not secure unless https)
114:   */
115:   function BasicAuthSession() {
116:     global $c;
117: 
118:     /**
119:     * Get HTTP Auth to work with PHP+FastCGI
120:     */
121:     if ( !isset($_SERVER['AUTHORIZATION']) && isset($_SERVER['HTTP_AUTHORIZATION']) && !empty($_SERVER['HTTP_AUTHORIZATION']))
122:       $_SERVER['AUTHORIZATION'] = $_SERVER['HTTP_AUTHORIZATION'];
123:     if (isset($_SERVER['AUTHORIZATION']) && !empty($_SERVER['AUTHORIZATION'])) {
124:       list ($type, $cred) = explode(" ", $_SERVER['AUTHORIZATION']);
125:       if ($type == 'Basic') {
126:         list ($user, $pass) = explode(":", base64_decode($cred), 2);
127:         $_SERVER['PHP_AUTH_USER'] = $user;
128:         $_SERVER['PHP_AUTH_PW'] = $pass;
129:       }
130:     }
131:     else if ( isset($c->authenticate_hook['server_auth_type'])
132:               && (  ( isset($_SERVER["REMOTE_USER"]) && !empty($_SERVER["REMOTE_USER"]) )  ||
133:                     ( isset($_SERVER["REDIRECT_REMOTE_USER"]) && !empty($_SERVER["REDIRECT_REMOTE_USER"]) )  )   ) {
134:       if ( ( is_array($c->authenticate_hook['server_auth_type'])
135:                     && in_array( strtolower($_SERVER['AUTH_TYPE']), array_map('strtolower', $c->authenticate_hook['server_auth_type'])) )
136:          ||
137:            ( !is_array($c->authenticate_hook['server_auth_type'])
138:                     && strtolower($c->authenticate_hook['server_auth_type']) == strtolower($_SERVER['AUTH_TYPE']) )
139:          ) {
140:         /**
141:         * The authentication has happened in the server, and we should accept it.
142:         */
143:         if (isset($_SERVER["REMOTE_USER"]))
144:           $_SERVER['PHP_AUTH_USER'] = $_SERVER['REMOTE_USER'];
145:         else
146:           $_SERVER['PHP_AUTH_USER'] = $_SERVER['REDIRECT_REMOTE_USER'];
147:         $_SERVER['PHP_AUTH_PW'] = 'Externally Authenticated';
148:         if ( ! isset($c->authenticate_hook['call']) ) {
149:           /**
150:           * Since we still need to get the user's details from somewhere.  We change the default
151:           * authentication hook to auth_external which simply retrieves a user row from the DB
152:           * and does no password checking.
153:           */
154:           $c->authenticate_hook['call'] = 'auth_external';
155:         }
156:       }
157:     }
158: 
159: 
160:     /**
161:     * Fall through to the normal PHP authentication variables.
162:     */
163:     if ( isset($_SERVER['PHP_AUTH_USER']) ) {
164:       if ( $p = $this->CheckPassword( $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'] ) ) {
165:         if ( isset($p->active) && !isset($p->user_active) ) {
166:           trace_bug('Some authentication failed to return a dav_principal record and needs fixing.');
167:           $p->user_active = $p->active;
168:         }
169: 
170:         /**
171:          * Maybe some external authentication didn't return false for an inactive
172:          * user, so we'll be pedantic here.
173:          */
174:         if ( $p->user_active ) {
175:           $this->AssignSessionDetails($p);
176:           return;
177:         }
178:       }
179:     }
180: 
181:     if ( isset($c->allow_unauthenticated) && $c->allow_unauthenticated ) {
182:       $this->AssignSessionDetails('unauthenticated');
183:       $this->logged_in = false;
184:       return;
185:     }
186: 
187:     $this->AuthFailedResponse();
188:     // Does not return
189:   }
190: 
191: 
192:   /**
193:   * Handle Digest HTTP Authentication (no passwords were harmed in this transaction!)
194:   *
195:   * Note that this will not actually work, unless we can either:
196:   *   (A) store the password plain text in the database
197:   *   (B) store an md5( username || realm || password ) in the database
198:   *
199:   * The problem is that potentially means that the administrator can collect the sorts
200:   * of things people use as passwords.  I believe this is quite a bad idea.  In scenario (B)
201:   * while they cannot see the password itself, they can see a hash which only varies when
202:   * the password varies, so can see when two users have the same password, or can use
203:   * some of the reverse lookup sites to attempt to reverse the hash.  I think this is a
204:   * less bad idea, but not ideal.  Probably better than running Basic auth of HTTP though!
205:   */
206:   function DigestAuthSession() {
207:     global $c;
208: 
209:     $realm = $c->system_name;
210:     $opaque = $realm;
211:     if ( isset($_SERVER['HTTP_USER_AGENT']) ) $opaque .= $_SERVER['HTTP_USER_AGENT'];
212:     if ( isset($_SERVER['REMOTE_ADDR']) )     $opaque .= $_SERVER['REMOTE_ADDR'];
213:     $opaque = sha1($opaque);
214: 
215:     if ( ! empty($_SERVER['PHP_AUTH_DIGEST'])) {
216:       // analyze the PHP_AUTH_DIGEST variable
217:       if ( $data = $this->ParseDigestHeader($_SERVER['PHP_AUTH_DIGEST']) ) {
218: 
219:         if ( $data['uri'] != $_SERVER['REQUEST_URI'] ) {
220:           dbg_error_log( "ERROR", " DigestAuth: WTF! URI is '%s' and request URI is '%s'!?!" );
221:           $this->AuthFailedResponse();
222:           // Does not return
223:         }
224: 
225:         // generate the valid response
226:         $test_user = new Principal('username', $data['username']);
227: 
228:         if ( preg_match( '{\*(Digest)?\*(.*)}', $test_user->password, $matches ) ) {
229:           if ( $matches[1] == 'Digest' )
230:             $A1 = $matches[2];
231:           else {
232: //            dbg_error_log( "HTTPAuth", "Constructing A1 from md5(%s:%s:%s)", $data['username'], $realm, $matches[2] );
233:             $A1 = md5($data['username'] . ':' . $realm . ':' . $matches[2]);
234:           }
235:           $A2 = md5($_SERVER['REQUEST_METHOD'].':'.$data['uri']);
236:           $auth_string = $A1.':'.$data['nonce'].':'.$data['nc'].':'.$data['cnonce'].':'.$data['qop'].':'.$A2;
237: //          dbg_error_log( "HTTPAuth", "DigestAuthString: %s", $auth_string);
238:           $valid_response = md5($auth_string);
239: //          dbg_error_log( "HTTPAuth", "DigestResponse: %s", $valid_response);
240: 
241:           if ( $data['response'] == $valid_response ) {
242:             $this->AssignSessionDetails($test_user);
243: //            dbg_error_log( "HTTPAuth", "Success!!!" );
244:             return;
245:           }
246:         }
247:         else {
248:           // Their account is not configured for Digest auth so we need to use Basic.
249:           $this->AuthFailedResponse();
250:           // Does not return
251:         }
252:       }
253:     }
254: 
255:     $nonce = sha1(uniqid('',true));
256:     $authheader = sprintf('WWW-Authenticate: Digest realm="%s", qop="auth", nonce="%s", opaque="%s", algorithm="MD5"',
257:                                      $realm, $nonce, $opaque );
258:     dbg_error_log( "HTTPAuth", $authheader );
259:     $this->AuthFailedResponse( $authheader );
260:     // Does not return
261:   }
262: 
263: 
264:   /**
265:   * Parse the HTTP Digest Auth Header
266:   *  - largely sourced from the PHP documentation
267:   */
268:   function ParseDigestHeader($auth_header) {
269:     // protect against missing data
270:     $needed_parts = array('nonce'=>1, 'nc'=>1, 'cnonce'=>1, 'qop'=>1, 'username'=>1, 'uri'=>1, 'response'=>1);
271:     $data = array();
272: 
273:     preg_match_all('{(\w+)="([^"]+)"}', $auth_header, $matches, PREG_SET_ORDER);
274:     foreach ($matches as $m) {
275: //      dbg_error_log( "HTTPAuth", 'Match: "%s"', $m[0] );
276:       $data[$m[1]] = $m[2];
277:       unset($needed_parts[$m[1]]);
278:       dbg_error_log( "HTTPAuth", 'Received: %s: %s', $m[1], $m[2] );
279:     }
280: 
281:     preg_match_all('{(\w+)=([^" ,]+)}', $auth_header, $matches, PREG_SET_ORDER);
282:     foreach ($matches as $m) {
283: //      dbg_error_log( "HTTPAuth", 'Match: "%s"', $m[0] );
284:       $data[$m[1]] = $m[2];
285:       unset($needed_parts[$m[1]]);
286:       dbg_error_log( "HTTPAuth", 'Received: %s: %s', $m[1], $m[2] );
287:     }
288: 
289: 
290:     @dbg_error_log( "HTTPAuth", 'Received: nonce: %s, nc: %s, cnonce: %s, qop: %s, username: %s, uri: %s, response: %s',
291:         $data['nonce'], $data['nc'], $data['cnonce'], $data['qop'], $data['username'], $data['uri'], $data['response']
292:       );
293:     return $needed_parts ? false : $data;
294:   }
295: 
296: 
297:   /**
298:   * CheckPassword does all of the password checking and
299:   * returns a user record object, or false if it all ends in tears.
300:   */
301:   function CheckPassword( $username, $password ) {
302:     global $c;
303: 
304:     if(isset($c->login_append_domain_if_missing) && $c->login_append_domain_if_missing && !preg_match('/@/',$username))
305:       $username.='@'.$c->domain_name;
306: 
307:     if ( !isset($c->authenticate_hook) || !isset($c->authenticate_hook['call'])
308:                       || !function_exists($c->authenticate_hook['call'])
309:                       || (isset($c->authenticate_hook['optional']) && $c->authenticate_hook['optional']) )
310:     {
311:       if ( $principal = new Principal('username', $username) ) {
312:         if ( isset($c->dbg['password']) ) dbg_error_log( "password", ":CheckPassword: Name:%s, Pass:%s, File:%s, Active:%s", $username, $password, $principal->password, ($principal->user_active?'Yes':'No') );
313:         if ( $principal->user_active && session_validate_password( $password, $principal->password ) ) {
314:           return $principal;
315:         }
316:       }
317:     }
318: 
319:     if ( isset($c->authenticate_hook) && isset($c->authenticate_hook['call']) && function_exists($c->authenticate_hook['call']) ) {
320:       /**
321:       * The authenticate hook needs to:
322:       *   - Accept a username / password
323:       *   - Confirm the username / password are correct
324:       *   - Create (or update) a 'usr' record in our database
325:       *   - Return the 'usr' record as an object
326:       *   - Return === false when authentication fails
327:       *
328:       * It can expect that:
329:       *   - Configuration data will be in $c->authenticate_hook['config'], which might be an array, or whatever is needed.
330:       */
331:       $principal = call_user_func( $c->authenticate_hook['call'], $username, $password );
332:       if ( $principal !== false && !($principal instanceof Principal) ) {
333:         $principal = new Principal('username', $username);
334:       }
335:       return $principal;
336:     }
337: 
338:     return false;
339:   }
340: 
341: 
342:   /**
343:   * Checks whether a user is allowed to do something.
344:   *
345:   * The check is performed to see if the user has that role.
346:   *
347:   * @param string $whatever The role we want to know if the user has.
348:   * @return boolean Whether or not the user has the specified role.
349:   */
350:   function AllowedTo ( $whatever ) {
351:     return ( isset($this->logged_in) && $this->logged_in && isset($this->roles[$whatever]) && $this->roles[$whatever] );
352:   }
353: 
354: 
355:   /**
356:   * Internal function used to get the user's roles from the database.
357:   */
358:   function GetRoles () {
359:     $this->roles = array();
360:     $qry = new AwlQuery( 'SELECT role_name FROM role_member m join roles r ON r.role_no = m.role_no WHERE user_no = :user_no ',
361:                                 array( ':user_no' => $this->user_no) );
362:     if ( $qry->Exec('BasicAuth') && $qry->rows() > 0 ) {
363:       while( $role = $qry->Fetch() ) {
364:         $this->roles[$role->role_name] = true;
365:       }
366:     }
367:   }
368: 
369: 
370:   /**
371:   * Internal function used to assign the session details to a user's new session.
372:   * @param object $u The user+session object we (probably) read from the database.
373:   */
374:   function AssignSessionDetails( $principal ) {
375:     if ( is_string($principal) ) $principal = new Principal('username',$principal);
376:     if ( get_class($principal) != 'Principal' ) {
377:       $principal = new Principal('username',$principal->username);
378:     }
379: 
380:     // Assign each field in the selected record to the object
381:     foreach( $principal AS $k => $v ) {
382:       $this->{$k} = $v;
383:     }
384:     if ( !get_class($principal) == 'Principal' ) {
385:       throw new Exception('HTTPAuthSession::AssignSessionDetails could not find a Principal object');
386:     }
387:     $this->username = $principal->username();
388:     $this->user_no  = $principal->user_no();
389:     $this->principal_id = $principal->principal_id();
390:     $this->email = $principal->email();
391:     $this->fullname = $principal->fullname;
392:     $this->dav_name = $principal->dav_name();
393:     $this->principal = $principal;
394: 
395:     $this->GetRoles();
396:     $this->logged_in = true;
397:     if ( function_exists("awl_set_locale") && isset($this->locale) && $this->locale != "" ) {
398:       awl_set_locale($this->locale);
399:     }
400:   }
401: 
402: 
403: }
404: 
405: 
DAViCal API documentation generated by ApiGen 2.8.0