xref: /dokuwiki/inc/auth.php (revision f5cb575df722c05fc0a6ba960bd2a79d5ed5621c)
1<?php
2/**
3 * Authentication library
4 *
5 * Including this file will automatically try to login
6 * a user by calling auth_login()
7 *
8 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
9 * @author     Andreas Gohr <andi@splitbrain.org>
10 */
11
12  if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../').'/');
13  require_once(DOKU_INC.'inc/common.php');
14  require_once(DOKU_INC.'inc/io.php');
15  require_once(DOKU_INC.'inc/blowfish.php');
16  require_once(DOKU_INC.'inc/mail.php');
17
18  // load the the backend auth functions and instantiate the auth object
19  if (@file_exists(DOKU_INC.'inc/auth/'.$conf['authtype'].'.class.php')) {
20      require_once(DOKU_INC.'inc/auth/basic.class.php');
21      require_once(DOKU_INC.'inc/auth/'.$conf['authtype'].'.class.php');
22
23      $auth_class = "auth_".$conf['authtype'];
24      if (!class_exists($auth_class)) $auth_class = "auth_basic";
25      $auth = new $auth_class();
26      if ($auth->success == false) {
27          msg($lang['authmodfailed'],-1);
28          unset($auth);
29      }
30
31      // interface between current dokuwiki/old auth system and new style auth object
32      function auth_canDo($fn) {
33        global $auth;
34        return method_exists($auth, $fn);
35      }
36
37      // mandatory functions - these should exist
38      function auth_checkPass($user,$pass) {
39        global $auth;
40        return method_exists($auth,'checkPass') ? $auth->checkPass($user, $pass) : false;
41      }
42
43      function auth_getUserData($user) {
44        global $auth;
45        return method_exists($auth, 'getUserData') ? $auth->getUserData($user) : false;
46      }
47
48      // optional functions, behave gracefully if these don't exist;
49      // potential calling code should query whether these exist in advance
50      function auth_createUser($user,$pass,$name,$mail) {
51        global $auth;
52        return method_exists($auth, 'createUser') ? $auth->createUser($user,$pass,$name,$mail) : null;
53      }
54
55      function auth_modifyUser($user, $changes) {
56        global $auth;
57        return method_exists($auth, 'modifyUser') ? $auth->modifyUser($user,$changes) : false;
58      }
59
60      function auth_deleteUsers($users) {
61        global $auth;
62        return method_exists($auth, 'deleteUsers') ? $auth->deleteUsers($users) : 0;
63      }
64
65      // other functions, will only be accessed by new code
66      //- these must query auth_canDo() or test method existence themselves.
67
68  } else {
69    // old style auth functions
70    require_once(DOKU_INC.'inc/auth/'.$conf['authtype'].'.php');
71    $auth = null;
72
73    // new function, allows other parts of dokuwiki to know what they can and can't do
74    function auth_canDo($fn) { return function_exists("auth_$fn"); }
75  }
76
77  if (!defined('DOKU_COOKIE')) define('DOKU_COOKIE', 'DW'.md5($conf['title']));
78
79  // some ACL level defines
80  define('AUTH_NONE',0);
81  define('AUTH_READ',1);
82  define('AUTH_EDIT',2);
83  define('AUTH_CREATE',4);
84  define('AUTH_UPLOAD',8);
85  define('AUTH_DELETE',16);
86  define('AUTH_ADMIN',255);
87
88  // do the login either by cookie or provided credentials
89  if($conf['useacl']){
90    // external trust mechanism in place?
91    if(auth_canDo('trustExternal') && !is_null($auth)){
92      $auth->trustExternal($_REQUEST['u'],$_REQUEST['p'],$_REQUEST['r']);
93    }else{
94      auth_login($_REQUEST['u'],$_REQUEST['p'],$_REQUEST['r']);
95    }
96
97    //load ACL into a global array
98    if(is_readable(DOKU_CONF.'acl.auth.php')){
99      $AUTH_ACL = file(DOKU_CONF.'acl.auth.php');
100    }else{
101      $AUTH_ACL = array();
102    }
103  }
104
105/**
106 * This tries to login the user based on the sent auth credentials
107 *
108 * The authentication works like this: if a username was given
109 * a new login is assumed and user/password are checked. If they
110 * are correct the password is encrypted with blowfish and stored
111 * together with the username in a cookie - the same info is stored
112 * in the session, too. Additonally a browserID is stored in the
113 * session.
114 *
115 * If no username was given the cookie is checked: if the username,
116 * crypted password and browserID match between session and cookie
117 * no further testing is done and the user is accepted
118 *
119 * If a cookie was found but no session info was availabe the
120 * blowfish encrypted password from the cookie is decrypted and
121 * together with username rechecked by calling this function again.
122 *
123 * On a successful login $_SERVER[REMOTE_USER] and $USERINFO
124 * are set.
125 *
126 * @author  Andreas Gohr <andi@splitbrain.org>
127 *
128 * @param   string  $user    Username
129 * @param   string  $pass    Cleartext Password
130 * @param   bool    $sticky  Cookie should not expire
131 * @return  bool             true on successful auth
132*/
133function auth_login($user,$pass,$sticky=false){
134  global $USERINFO;
135  global $conf;
136  global $lang;
137  $sticky ? $sticky = true : $sticky = false; //sanity check
138
139  if(isset($user)){
140    //usual login
141    if (auth_checkPass($user,$pass)){
142      // make logininfo globally available
143      $_SERVER['REMOTE_USER'] = $user;
144      $USERINFO = auth_getUserData($user); //FIXME move all references to session
145
146      // set cookie
147      $pass   = PMA_blowfish_encrypt($pass,auth_cookiesalt());
148      $cookie = base64_encode("$user|$sticky|$pass");
149      if($sticky) $time = time()+60*60*24*365; //one year
150      setcookie(DOKU_COOKIE,$cookie,$time,'/');
151
152      // set session
153      $_SESSION[$conf['title']]['auth']['user'] = $user;
154      $_SESSION[$conf['title']]['auth']['pass'] = $pass;
155      $_SESSION[$conf['title']]['auth']['buid'] = auth_browseruid();
156      $_SESSION[$conf['title']]['auth']['info'] = $USERINFO;
157      return true;
158    }else{
159      //invalid credentials - log off
160      msg($lang['badlogin'],-1);
161      auth_logoff();
162      return false;
163    }
164  }else{
165    // read cookie information
166    $cookie = base64_decode($_COOKIE[DOKU_COOKIE]);
167    list($user,$sticky,$pass) = split('\|',$cookie,3);
168    // get session info
169    $session = $_SESSION[$conf['title']]['auth'];
170
171    if($user && $pass){
172      // we got a cookie - see if we can trust it
173      if(isset($session) &&
174        ($session['user'] == $user) &&
175        ($session['pass'] == $pass) &&  //still crypted
176        ($session['buid'] == auth_browseruid()) ){
177        // he has session, cookie and browser right - let him in
178        $_SERVER['REMOTE_USER'] = $user;
179        $USERINFO = $session['info']; //FIXME move all references to session
180        return true;
181      }
182      // no we don't trust it yet - recheck pass
183      $pass = PMA_blowfish_decrypt($pass,auth_cookiesalt());
184      return auth_login($user,$pass,$sticky);
185    }
186  }
187  //just to be sure
188  auth_logoff();
189  return false;
190}
191
192/**
193 * Builds a pseudo UID from browser and IP data
194 *
195 * This is neither unique nor unfakable - still it adds some
196 * security. Using the first part of the IP makes sure
197 * proxy farms like AOLs are stil okay.
198 *
199 * @author  Andreas Gohr <andi@splitbrain.org>
200 *
201 * @return  string  a MD5 sum of various browser headers
202 */
203function auth_browseruid(){
204  $uid  = '';
205  $uid .= $_SERVER['HTTP_USER_AGENT'];
206  $uid .= $_SERVER['HTTP_ACCEPT_ENCODING'];
207  $uid .= $_SERVER['HTTP_ACCEPT_LANGUAGE'];
208  $uid .= $_SERVER['HTTP_ACCEPT_CHARSET'];
209  $uid .= substr($_SERVER['REMOTE_ADDR'],0,strpos($_SERVER['REMOTE_ADDR'],'.'));
210  return md5($uid);
211}
212
213/**
214 * Creates a random key to encrypt the password in cookies
215 *
216 * This function tries to read the password for encrypting
217 * cookies from $conf['metadir'].'/_htcookiesalt'
218 * if no such file is found a random key is created and
219 * and stored in this file.
220 *
221 * @author  Andreas Gohr <andi@splitbrain.org>
222 *
223 * @return  string
224 */
225function auth_cookiesalt(){
226  global $conf;
227  $file = $conf['metadir'].'/_htcookiesalt';
228  $salt = io_readFile($file);
229  if(empty($salt)){
230    $salt = uniqid(rand(),true);
231    io_saveFile($file,$salt);
232  }
233  return $salt;
234}
235
236/**
237 * This clears all authenticationdata and thus log the user
238 * off
239 *
240 * @author  Andreas Gohr <andi@splitbrain.org>
241 */
242function auth_logoff(){
243  global $conf;
244  global $USERINFO;
245  global $INFO, $ID;
246
247  if(isset($_SESSION[$conf['title']]['auth']['user']))
248    unset($_SESSION[$conf['title']]['auth']['user']);
249  if(isset($_SESSION[$conf['title']]['auth']['pass']))
250    unset($_SESSION[$conf['title']]['auth']['pass']);
251  if(isset($_SESSION[$conf['title']]['auth']['info']))
252    unset($_SESSION[$conf['title']]['auth']['info']);
253  if(isset($_SERVER['REMOTE_USER']))
254    unset($_SERVER['REMOTE_USER']);
255  $USERINFO=null; //FIXME
256  setcookie(DOKU_COOKIE,'',time()-600000,'/');
257}
258
259/**
260 * Convinience function for auth_aclcheck()
261 *
262 * This checks the permissions for the current user
263 *
264 * @author  Andreas Gohr <andi@splitbrain.org>
265 *
266 * @param  string  $id  page ID
267 * @return int          permission level
268 */
269function auth_quickaclcheck($id){
270  global $conf;
271  global $USERINFO;
272  # if no ACL is used always return upload rights
273  if(!$conf['useacl']) return AUTH_UPLOAD;
274  return auth_aclcheck($id,$_SERVER['REMOTE_USER'],$USERINFO['grps']);
275}
276
277/**
278 * Returns the maximum rights a user has for
279 * the given ID or its namespace
280 *
281 * @author  Andreas Gohr <andi@splitbrain.org>
282 *
283 * @param  string  $id     page ID
284 * @param  string  $user   Username
285 * @param  array   $groups Array of groups the user is in
286 * @return int             permission level
287 */
288function auth_aclcheck($id,$user,$groups){
289  global $conf;
290  global $AUTH_ACL;
291
292  # if no ACL is used always return upload rights
293  if(!$conf['useacl']) return AUTH_UPLOAD;
294
295  //if user is superuser return 255 (acl_admin)
296  if($conf['superuser'] == $user) { return AUTH_ADMIN; }
297
298  //make sure groups is an array
299  if(!is_array($groups)) $groups = array();
300
301  //prepend groups with @
302  $cnt = count($groups);
303  for($i=0; $i<$cnt; $i++){
304    $groups[$i] = '@'.$groups[$i];
305  }
306  //if user is in superuser group return 255 (acl_admin)
307  if(in_array($conf['superuser'], $groups)) { return AUTH_ADMIN; }
308
309  $ns    = getNS($id);
310  $perm  = -1;
311
312  if($user){
313    //add ALL group
314    $groups[] = '@ALL';
315    //add User
316    $groups[] = $user;
317    //build regexp
318    $regexp   = join('|',$groups);
319  }else{
320    $regexp = '@ALL';
321  }
322
323  //check exact match first
324  $matches = preg_grep('/^'.preg_quote($id,'/').'\s+('.$regexp.')\s+/',$AUTH_ACL);
325  if(count($matches)){
326    foreach($matches as $match){
327      $match = preg_replace('/#.*$/','',$match); //ignore comments
328      $acl   = preg_split('/\s+/',$match);
329      if($acl[2] > AUTH_DELETE) $acl[2] = AUTH_DELETE; //no admins in the ACL!
330      if($acl[2] > $perm){
331        $perm = $acl[2];
332      }
333    }
334    if($perm > -1){
335      //we had a match - return it
336      return $perm;
337    }
338  }
339
340  //still here? do the namespace checks
341  if($ns){
342    $path = $ns.':\*';
343  }else{
344    $path = '\*'; //root document
345  }
346
347  do{
348    $matches = preg_grep('/^'.$path.'\s+('.$regexp.')\s+/',$AUTH_ACL);
349    if(count($matches)){
350      foreach($matches as $match){
351        $match = preg_replace('/#.*$/','',$match); //ignore comments
352        $acl   = preg_split('/\s+/',$match);
353        if($acl[2] > AUTH_DELETE) $acl[2] = AUTH_DELETE; //no admins in the ACL!
354        if($acl[2] > $perm){
355          $perm = $acl[2];
356        }
357      }
358      //we had a match - return it
359      return $perm;
360    }
361
362    //get next higher namespace
363    $ns   = getNS($ns);
364
365    if($path != '\*'){
366      $path = $ns.':\*';
367      if($path == ':\*') $path = '\*';
368    }else{
369      //we did this already
370      //looks like there is something wrong with the ACL
371      //break here
372      msg('No ACL setup yet! Denying access to everyone.');
373      return AUTH_NONE;
374    }
375  }while(1); //this should never loop endless
376
377  //still here? return no permissions
378  return AUTH_NONE;
379}
380
381/**
382 * Create a pronouncable password
383 *
384 * @author  Andreas Gohr <andi@splitbrain.org>
385 * @link    http://www.phpbuilder.com/annotate/message.php3?id=1014451
386 *
387 * @return string  pronouncable password
388 */
389function auth_pwgen(){
390  $pw = '';
391  $c  = 'bcdfghjklmnprstvwz'; //consonants except hard to speak ones
392  $v  = 'aeiou';              //vowels
393  $a  = $c.$v;                //both
394
395  //use two syllables...
396  for($i=0;$i < 2; $i++){
397    $pw .= $c[rand(0, strlen($c)-1)];
398    $pw .= $v[rand(0, strlen($v)-1)];
399    $pw .= $a[rand(0, strlen($a)-1)];
400  }
401  //... and add a nice number
402  $pw .= rand(10,99);
403
404  return $pw;
405}
406
407/**
408 * Sends a password to the given user
409 *
410 * @author  Andreas Gohr <andi@splitbrain.org>
411 *
412 * @return bool  true on success
413 */
414function auth_sendPassword($user,$password){
415  global $conf;
416  global $lang;
417  $hdrs  = '';
418  $userinfo = auth_getUserData($user);
419
420  if(!$userinfo['mail']) return false;
421
422  $text = rawLocale('password');
423  $text = str_replace('@DOKUWIKIURL@',DOKU_URL,$text);
424  $text = str_replace('@FULLNAME@',$userinfo['name'],$text);
425  $text = str_replace('@LOGIN@',$user,$text);
426  $text = str_replace('@PASSWORD@',$password,$text);
427  $text = str_replace('@TITLE@',$conf['title'],$text);
428
429  return mail_send($userinfo['name'].' <'.$userinfo['mail'].'>',
430                   $lang['regpwmail'],
431                   $text,
432                   $conf['mailfrom']);
433}
434
435/**
436 * Register a new user
437 *
438 * This registers a new user - Data is read directly from $_POST
439 *
440 * @author  Andreas Gohr <andi@splitbrain.org>
441 *
442 * @return bool  true on success, false on any error
443 */
444function register(){
445  global $lang;
446  global $conf;
447
448  if(!$_POST['save']) return false;
449
450  //clean username
451  $_POST['login'] = preg_replace('/.*:/','',$_POST['login']);
452  $_POST['login'] = cleanID($_POST['login']);
453  //clean fullname and email
454  $_POST['fullname'] = trim(str_replace(':','',$_POST['fullname']));
455  $_POST['email']    = trim(str_replace(':','',$_POST['email']));
456
457  if( empty($_POST['login']) ||
458      empty($_POST['fullname']) ||
459      empty($_POST['email']) ){
460    msg($lang['regmissing'],-1);
461    return false;
462  }
463
464  if ($conf['autopasswd']) {
465    $pass = auth_pwgen();                // automatically generate password
466  } elseif (empty($_POST['pass']) ||
467            empty($_POST['passchk'])) {
468    msg($lang['regmissing'], -1);        // complain about missing passwords
469    return false;
470  } elseif ($_POST['pass'] != $_POST['passchk']) {
471    msg($lang['regbadpass'], -1);      // complain about misspelled passwords
472    return false;
473  } else {
474    $pass = $_POST['pass'];              // accept checked and valid password
475  }
476
477  //check mail
478  if(!mail_isvalid($_POST['email'])){
479    msg($lang['regbadmail'],-1);
480    return false;
481  }
482
483  //okay try to create the user
484  $pass = auth_createUser($_POST['login'],$pass,$_POST['fullname'],$_POST['email']);
485  if(empty($pass)){
486    msg($lang['reguexists'],-1);
487    return false;
488  }
489
490  if (!$conf['autopasswd']) {
491    msg($lang['regsuccess2'],1);
492    return true;
493  }
494
495  // autogenerated password? then send him the password
496  if (auth_sendPassword($_POST['login'],$pass)){
497    msg($lang['regsuccess'],1);
498    return true;
499  }else{
500    msg($lang['regmailfail'],-1);
501    return false;
502  }
503}
504
505/**
506 * Update user profile
507 *
508 * @author    Christopher Smith <chris@jalakai.co.uk>
509 */
510function updateprofile() {
511  global $conf;
512  global $INFO;
513  global $lang;
514
515  if(!$_POST['save']) return false;
516
517  // should not be able to get here without modifyUser being possible...
518  if(!auth_canDo('modifyUser')) {
519    msg($lang['profna'],-1);
520    return false;
521  }
522
523  if ($_POST['newpass'] != $_POST['passchk']) {
524    msg($lang['regbadpass'], -1);      // complain about misspelled passwords
525    return false;
526  }
527
528  //clean fullname and email
529  $_POST['fullname'] = trim(str_replace(':','',$_POST['fullname']));
530  $_POST['email']    = trim(str_replace(':','',$_POST['email']));
531
532  if (empty($_POST['fullname']) || empty($_POST['email'])) {
533    msg($lang['profnoempty'],-1);
534    return false;
535  }
536
537  if (!mail_isvalid($_POST['email'])){
538    msg($lang['regbadmail'],-1);
539    return false;
540  }
541
542  if ($_POST['fullname'] != $INFO['userinfo']['name']) $changes['name'] = $_POST['fullname'];
543  if ($_POST['email']    != $INFO['userinfo']['mail']) $changes['mail'] = $_POST['email'];
544  if (!empty($_POST['newpass']))  $changes['pass'] = $_POST['newpass'];
545
546  if (!count($changes)) {
547    msg($lang['profnochange'], -1);
548    return false;
549  }
550
551  if ($conf['profileconfirm']) {
552      if (!auth_verifyPassword($_POST['oldpass'],$INFO['userinfo']['pass'])) {
553      msg($lang['badlogin'],-1);
554      return false;
555    }
556  }
557
558  return auth_modifyUser($_SERVER['REMOTE_USER'], $changes);
559}
560
561/**
562 * Send a  new password
563 *
564 * @author Benoit Chesneau <benoit@bchesneau.info>
565 * @author Chris Smith <chris@jalakai.co.uk>
566 *
567 * @return bool true on success, false on any error
568*/
569function act_resendpwd(){
570    global $lang;
571    global $conf;
572
573    if(!$_POST['save']) return false;
574
575    // should not be able to get here without modifyUser being possible...
576	if(!auth_canDo('modifyUser')) {
577      msg($lang['resendna'],-1);
578      return false;
579	}
580
581    if (empty($_POST['login'])) {
582      msg($lang['resendpwdmissing'], -1);
583      return false;
584    } else {
585      $user = $_POST['login'];
586    }
587
588    $userinfo = auth_getUserData($user);
589    if(!$userinfo['mail']) {
590      msg($lang['resendpwdnouser'], -1);
591      return false;
592    }
593
594    $pass = auth_pwgen();
595    if (!auth_modifyUser($user,array('pass' => $pass))) {
596      msg('error modifying user data',-1);
597      return false;
598    }
599
600    if (auth_sendPassword($user,$pass)) {
601      msg($lang['resendpwdsuccess'],1);
602    } else {
603      msg($lang['regmailfail'],-1);
604    }
605    return true;
606}
607
608/**
609 * Uses a regular expresion to check if a given mail address is valid
610 *
611 * May not be completly RFC conform!
612 *
613 * @link    http://www.webmasterworld.com/forum88/135.htm
614 *
615 * @param   string $email the address to check
616 * @return  bool          true if address is valid
617 */
618function isvalidemail($email){
619  return eregi("^[0-9a-z]([-_.]?[0-9a-z])*@[0-9a-z]([-.]?[0-9a-z])*\\.[a-z]{2,4}$", $email);
620}
621
622/**
623 * Encrypts a password using the given method and salt
624 *
625 * If the selected method needs a salt and none was given, a random one
626 * is chosen.
627 *
628 * The following methods are understood:
629 *
630 *   smd5  - Salted MD5 hashing
631 *   md5   - Simple MD5 hashing
632 *   sha1  - SHA1 hashing
633 *   ssha  - Salted SHA1 hashing
634 *   crypt - Unix crypt
635 *   mysql - MySQL password (old method)
636 *   my411 - MySQL 4.1.1 password
637 *
638 * @author  Andreas Gohr <andi@splitbrain.org>
639 * @return  string  The crypted password
640 */
641function auth_cryptPassword($clear,$method='',$salt=''){
642  global $conf;
643  if(empty($method)) $method = $conf['passcrypt'];
644
645  //prepare a salt
646  if(empty($salt)) $salt = md5(uniqid(rand(), true));
647
648  switch(strtolower($method)){
649    case 'smd5':
650        return crypt($clear,'$1$'.substr($salt,0,8).'$');
651    case 'md5':
652      return md5($clear);
653    case 'sha1':
654      return sha1($clear);
655    case 'ssha':
656      $salt=substr($salt,0,4);
657      return '{SSHA}'.base64_encode(pack("H*", sha1($clear.$salt)).$salt);
658    case 'crypt':
659      return crypt($clear,substr($salt,0,2));
660    case 'mysql':
661      //from http://www.php.net/mysql comment by <soren at byu dot edu>
662      $nr=0x50305735;
663      $nr2=0x12345671;
664      $add=7;
665      $charArr = preg_split("//", $clear);
666      foreach ($charArr as $char) {
667        if (($char == '') || ($char == ' ') || ($char == '\t')) continue;
668        $charVal = ord($char);
669        $nr ^= ((($nr & 63) + $add) * $charVal) + ($nr << 8);
670        $nr2 += ($nr2 << 8) ^ $nr;
671        $add += $charVal;
672      }
673      return sprintf("%08x%08x", ($nr & 0x7fffffff), ($nr2 & 0x7fffffff));
674    case 'my411':
675      return '*'.sha1(pack("H*", sha1($clear)));
676    default:
677      msg("Unsupported crypt method $method",-1);
678  }
679}
680
681/**
682 * Verifies a cleartext password against a crypted hash
683 *
684 * The method and salt used for the crypted hash is determined automatically
685 * then the clear text password is crypted using the same method. If both hashs
686 * match true is is returned else false
687 *
688 * @author  Andreas Gohr <andi@splitbrain.org>
689 * @return  bool
690 */
691function auth_verifyPassword($clear,$crypt){
692  $method='';
693  $salt='';
694
695  //determine the used method and salt
696  $len = strlen($crypt);
697  if(substr($crypt,0,3) == '$1$'){
698    $method = 'smd5';
699    $salt   = substr($crypt,3,8);
700  }elseif(substr($crypt,0,6) == '{SSHA}'){
701    $method = 'ssha';
702    $salt   = substr(base64_decode(substr($crypt, 6)),20);
703  }elseif($len == 32){
704    $method = 'md5';
705  }elseif($len == 40){
706    $method = 'sha1';
707  }elseif($len == 16){
708    $method = 'mysql';
709  }elseif($len == 41 && $crypt[0] == '*'){
710    $method = 'my411';
711  }else{
712    $method = 'crypt';
713    $salt   = substr($crypt,0,2);
714  }
715
716  //crypt and compare
717  if(auth_cryptPassword($clear,$method,$salt) === $crypt){
718    return true;
719  }
720  return false;
721}
722
723//Setup VIM: ex: et ts=2 enc=utf-8 :
724