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:     dbg_error_log( "HTTPAuth", ":Session: User is not authorised: %s ", $_SERVER['REMOTE_ADDR'] );
104:     @ob_flush();   exit(0);
105:   }
106: 
107: 
108:   /**
109:   * Handle Basic HTTP Authentication (not secure unless https)
110:   */
111:   function BasicAuthSession() {
112:     global $c;
113: 
114:     /**
115:     * Get HTTP Auth to work with PHP+FastCGI
116:     */
117:     if ( !isset($_SERVER['AUTHORIZATION']) && isset($_SERVER['HTTP_AUTHORIZATION']) && !empty($_SERVER['HTTP_AUTHORIZATION']))
118:       $_SERVER['AUTHORIZATION'] = $_SERVER['HTTP_AUTHORIZATION'];
119:     if (isset($_SERVER['AUTHORIZATION']) && !empty($_SERVER['AUTHORIZATION'])) {
120:       list ($type, $cred) = explode(" ", $_SERVER['AUTHORIZATION']);
121:       if ($type == 'Basic') {
122:         list ($user, $pass) = explode(":", base64_decode($cred), 2);
123:         $_SERVER['PHP_AUTH_USER'] = $user;
124:         $_SERVER['PHP_AUTH_PW'] = $pass;
125:       }
126:     }
127:     else if ( isset($c->authenticate_hook['server_auth_type'])
128:               && (  ( isset($_SERVER["REMOTE_USER"]) && !empty($_SERVER["REMOTE_USER"]) )  ||
129:                     ( isset($_SERVER["REDIRECT_REMOTE_USER"]) && !empty($_SERVER["REDIRECT_REMOTE_USER"]) )  )   ) {
130:       if ( ( is_array($c->authenticate_hook['server_auth_type'])
131:                     && in_array( strtolower($_SERVER['AUTH_TYPE']), array_map('strtolower', $c->authenticate_hook['server_auth_type'])) )
132:          ||
133:            ( !is_array($c->authenticate_hook['server_auth_type'])
134:                     && strtolower($c->authenticate_hook['server_auth_type']) == strtolower($_SERVER['AUTH_TYPE']) )
135:          ) {
136:         /**
137:         * The authentication has happened in the server, and we should accept it.
138:         */
139:         if (isset($_SERVER["REMOTE_USER"]))
140:           $_SERVER['PHP_AUTH_USER'] = $_SERVER['REMOTE_USER'];
141:         else
142:           $_SERVER['PHP_AUTH_USER'] = $_SERVER['REDIRECT_REMOTE_USER'];
143:         $_SERVER['PHP_AUTH_PW'] = 'Externally Authenticated';
144:         if ( ! isset($c->authenticate_hook['call']) ) {
145:           /**
146:           * Since we still need to get the user's details from somewhere.  We change the default
147:           * authentication hook to auth_external which simply retrieves a user row from the DB
148:           * and does no password checking.
149:           */
150:           $c->authenticate_hook['call'] = 'auth_external';
151:         }
152:       }
153:     }
154: 
155: 
156:     /**
157:     * Fall through to the normal PHP authentication variables.
158:     */
159:     if ( isset($_SERVER['PHP_AUTH_USER']) ) {
160:       if ( $p = $this->CheckPassword( $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'] ) ) {
161:         if ( isset($p->active) && !isset($p->user_active) ) {
162:           trace_bug('Some authentication failed to return a dav_principal record and needs fixing.');
163:           $p->user_active = $p->active;
164:         }
165: 
166:         /**
167:          * Maybe some external authentication didn't return false for an inactive
168:          * user, so we'll be pedantic here.
169:          */
170:         if ( $p->user_active ) {
171:           $this->AssignSessionDetails($p);
172:           return;
173:         }
174:       }
175:     }
176: 
177:     if ( isset($c->allow_unauthenticated) && $c->allow_unauthenticated ) {
178:       $this->AssignSessionDetails('unauthenticated');
179:       $this->logged_in = false;
180:       return;
181:     }
182: 
183:     $this->AuthFailedResponse();
184:     // Does not return
185:   }
186: 
187: 
188:   /**
189:   * Handle Digest HTTP Authentication (no passwords were harmed in this transaction!)
190:   *
191:   * Note that this will not actually work, unless we can either:
192:   *   (A) store the password plain text in the database
193:   *   (B) store an md5( username || realm || password ) in the database
194:   *
195:   * The problem is that potentially means that the administrator can collect the sorts
196:   * of things people use as passwords.  I believe this is quite a bad idea.  In scenario (B)
197:   * while they cannot see the password itself, they can see a hash which only varies when
198:   * the password varies, so can see when two users have the same password, or can use
199:   * some of the reverse lookup sites to attempt to reverse the hash.  I think this is a
200:   * less bad idea, but not ideal.  Probably better than running Basic auth of HTTP though!
201:   */
202:   function DigestAuthSession() {
203:     global $c;
204: 
205:     $realm = $c->system_name;
206:     $opaque = $realm;
207:     if ( isset($_SERVER['HTTP_USER_AGENT']) ) $opaque .= $_SERVER['HTTP_USER_AGENT'];
208:     if ( isset($_SERVER['REMOTE_ADDR']) )     $opaque .= $_SERVER['REMOTE_ADDR'];
209:     $opaque = sha1($opaque);
210: 
211:     if ( ! empty($_SERVER['PHP_AUTH_DIGEST'])) {
212:       // analyze the PHP_AUTH_DIGEST variable
213:       if ( $data = $this->ParseDigestHeader($_SERVER['PHP_AUTH_DIGEST']) ) {
214: 
215:         if ( $data['uri'] != $_SERVER['REQUEST_URI'] ) {
216:           dbg_error_log( "ERROR", " DigestAuth: WTF! URI is '%s' and request URI is '%s'!?!" );
217:           $this->AuthFailedResponse();
218:           // Does not return
219:         }
220: 
221:         // generate the valid response
222:         $test_user = new Principal('username', $data['username']);
223: 
224:         if ( preg_match( '{\*(Digest)?\*(.*)}', $test_user->password, $matches ) ) {
225:           if ( $matches[1] == 'Digest' )
226:             $A1 = $matches[2];
227:           else {
228: //            dbg_error_log( "HTTPAuth", "Constructing A1 from md5(%s:%s:%s)", $data['username'], $realm, $matches[2] );
229:             $A1 = md5($data['username'] . ':' . $realm . ':' . $matches[2]);
230:           }
231:           $A2 = md5($_SERVER['REQUEST_METHOD'].':'.$data['uri']);
232:           $auth_string = $A1.':'.$data['nonce'].':'.$data['nc'].':'.$data['cnonce'].':'.$data['qop'].':'.$A2;
233: //          dbg_error_log( "HTTPAuth", "DigestAuthString: %s", $auth_string);
234:           $valid_response = md5($auth_string);
235: //          dbg_error_log( "HTTPAuth", "DigestResponse: %s", $valid_response);
236: 
237:           if ( $data['response'] == $valid_response ) {
238:             $this->AssignSessionDetails($test_user);
239: //            dbg_error_log( "HTTPAuth", "Success!!!" );
240:             return;
241:           }
242:         }
243:         else {
244:           // Their account is not configured for Digest auth so we need to use Basic.
245:           $this->AuthFailedResponse();
246:           // Does not return
247:         }
248:       }
249:     }
250: 
251:     $nonce = sha1(uniqid('',true));
252:     $authheader = sprintf('WWW-Authenticate: Digest realm="%s", qop="auth", nonce="%s", opaque="%s", algorithm="MD5"',
253:                                      $realm, $nonce, $opaque );
254:     dbg_error_log( "HTTPAuth", $authheader );
255:     $this->AuthFailedResponse( $authheader );
256:     // Does not return
257:   }
258: 
259: 
260:   /**
261:   * Parse the HTTP Digest Auth Header
262:   *  - largely sourced from the PHP documentation
263:   */
264:   function ParseDigestHeader($auth_header) {
265:     // protect against missing data
266:     $needed_parts = array('nonce'=>1, 'nc'=>1, 'cnonce'=>1, 'qop'=>1, 'username'=>1, 'uri'=>1, 'response'=>1);
267:     $data = array();
268: 
269:     preg_match_all('{(\w+)="([^"]+)"}', $auth_header, $matches, PREG_SET_ORDER);
270:     foreach ($matches as $m) {
271: //      dbg_error_log( "HTTPAuth", 'Match: "%s"', $m[0] );
272:       $data[$m[1]] = $m[2];
273:       unset($needed_parts[$m[1]]);
274:       dbg_error_log( "HTTPAuth", 'Received: %s: %s', $m[1], $m[2] );
275:     }
276: 
277:     preg_match_all('{(\w+)=([^" ,]+)}', $auth_header, $matches, PREG_SET_ORDER);
278:     foreach ($matches as $m) {
279: //      dbg_error_log( "HTTPAuth", 'Match: "%s"', $m[0] );
280:       $data[$m[1]] = $m[2];
281:       unset($needed_parts[$m[1]]);
282:       dbg_error_log( "HTTPAuth", 'Received: %s: %s', $m[1], $m[2] );
283:     }
284: 
285: 
286:     @dbg_error_log( "HTTPAuth", 'Received: nonce: %s, nc: %s, cnonce: %s, qop: %s, username: %s, uri: %s, response: %s',
287:         $data['nonce'], $data['nc'], $data['cnonce'], $data['qop'], $data['username'], $data['uri'], $data['response']
288:       );
289:     return $needed_parts ? false : $data;
290:   }
291: 
292: 
293:   /**
294:   * CheckPassword does all of the password checking and
295:   * returns a user record object, or false if it all ends in tears.
296:   */
297:   function CheckPassword( $username, $password ) {
298:     global $c;
299: 
300:     if(isset($c->login_append_domain_if_missing) && $c->login_append_domain_if_missing && !preg_match('/@/',$username))
301:       $username.='@'.$c->domain_name;
302: 
303:     if ( !isset($c->authenticate_hook) || !isset($c->authenticate_hook['call'])
304:                       || !function_exists($c->authenticate_hook['call'])
305:                       || (isset($c->authenticate_hook['optional']) && $c->authenticate_hook['optional']) )
306:     {
307:       if ( $principal = new Principal('username', $username) ) {
308:         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') );
309:         if ( $principal->user_active && session_validate_password( $password, $principal->password ) ) {
310:           return $principal;
311:         }
312:       }
313:     }
314: 
315:     if ( isset($c->authenticate_hook) && isset($c->authenticate_hook['call']) && function_exists($c->authenticate_hook['call']) ) {
316:       /**
317:       * The authenticate hook needs to:
318:       *   - Accept a username / password
319:       *   - Confirm the username / password are correct
320:       *   - Create (or update) a 'usr' record in our database
321:       *   - Return the 'usr' record as an object
322:       *   - Return === false when authentication fails
323:       *
324:       * It can expect that:
325:       *   - Configuration data will be in $c->authenticate_hook['config'], which might be an array, or whatever is needed.
326:       */
327:       $principal = call_user_func( $c->authenticate_hook['call'], $username, $password );
328:       if ( $principal !== false && !($principal instanceof Principal) ) {
329:         $principal = new Principal('username', $username);
330:       }
331:       return $principal;
332:     }
333: 
334:     return false;
335:   }
336: 
337: 
338:   /**
339:   * Checks whether a user is allowed to do something.
340:   *
341:   * The check is performed to see if the user has that role.
342:   *
343:   * @param string $whatever The role we want to know if the user has.
344:   * @return boolean Whether or not the user has the specified role.
345:   */
346:   function AllowedTo ( $whatever ) {
347:     return ( isset($this->logged_in) && $this->logged_in && isset($this->roles[$whatever]) && $this->roles[$whatever] );
348:   }
349: 
350: 
351:   /**
352:   * Internal function used to get the user's roles from the database.
353:   */
354:   function GetRoles () {
355:     $this->roles = array();
356:     $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 ',
357:                                 array( ':user_no' => $this->user_no) );
358:     if ( $qry->Exec('BasicAuth') && $qry->rows() > 0 ) {
359:       while( $role = $qry->Fetch() ) {
360:         $this->roles[$role->role_name] = true;
361:       }
362:     }
363:   }
364: 
365: 
366:   /**
367:   * Internal function used to assign the session details to a user's new session.
368:   * @param object $u The user+session object we (probably) read from the database.
369:   */
370:   function AssignSessionDetails( $principal ) {
371:     if ( is_string($principal) ) $principal = new Principal('username',$principal);
372:     if ( get_class($principal) != 'Principal' ) {
373:       $principal = new Principal('username',$principal->username);
374:     }
375: 
376:     // Assign each field in the selected record to the object
377:     foreach( $principal AS $k => $v ) {
378:       $this->{$k} = $v;
379:     }
380:     if ( !get_class($principal) == 'Principal' ) {
381:       throw new Exception('HTTPAuthSession::AssignSessionDetails could not find a Principal object');
382:     }
383:     $this->username = $principal->username();
384:     $this->user_no  = $principal->user_no();
385:     $this->principal_id = $principal->principal_id();
386:     $this->email = $principal->email();
387:     $this->fullname = $principal->fullname;
388:     $this->dav_name = $principal->dav_name();
389:     $this->principal = $principal;
390: 
391:     $this->GetRoles();
392:     $this->logged_in = true;
393:     if ( function_exists("awl_set_locale") && isset($this->locale) && $this->locale != "" ) {
394:       awl_set_locale($this->locale);
395:     }
396:   }
397: 
398: 
399: }
400: 
401: 
DAViCal API documentation generated by ApiGen 2.8.0