Dotclear

source: inc/core/class.dc.auth.php @ 3627:9bccfc2257ad

Revision 3627:9bccfc2257ad, 16.2 KB checked in by franck <carnet.franck.paul@…>, 8 years ago (diff)

Use PHP 5.5+ new password functions, closes #2182

Warnings:

  • $core->auth->crypt($pwd) doesn't return twice the same result for a single $pwd, so if you need this old behaviour use the $core->auth->cryptLegacy($pwd) instead.
  • $core->auth->checkPassword($pwd) must be used with an uncrypted password string as argument.
  • if you need a unique UID/key, use http::browserUID(DC_MASTER_KEY.$core->auth->userID().$core->auth->cryptLegacy($core->auth->userID())). (may be refined in future)
RevLine 
[0]1<?php
2# -- BEGIN LICENSE BLOCK ---------------------------------------
3#
4# This file is part of Dotclear 2.
5#
[1179]6# Copyright (c) 2003-2013 Olivier Meunier & Association Dotclear
[0]7# Licensed under the GPL version 2.0 license.
8# See LICENSE file or
9# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
10#
11# -- END LICENSE BLOCK -----------------------------------------
12if (!defined('DC_RC_PATH')) { return; }
13
14/**
[333]15* @ingroup DC_CORE
16* @nosubgrouping
17* @brief Authentication and user credentials management
[2566]18*
[333]19* dcAuth is a class used to handle everything related to user authentication
20* and credentials. Object is provided by dcCore $auth property.
[0]21*/
22class dcAuth
23{
[333]24     /** @var dcCore dcCore instance */
25     protected $core;
26     /** @var connection Database connection object */
27     protected $con;
[2566]28
[333]29     /** @var string User table name */
30     protected $user_table;
31     /** @var string Perm table name */
32     protected $perm_table;
[2566]33
[333]34     /** @var string Current user ID */
35     protected $user_id;
36     /** @var array Array with user information */
37     protected $user_info = array();
38     /** @var array Array with user options */
39     protected $user_options = array();
40     /** @var boolean User must change his password after login */
41     protected $user_change_pwd;
42     /** @var boolean User is super admin */
43     protected $user_admin;
44     /** @var array Permissions for each blog */
45     protected $permissions = array();
46     /** @var boolean User can change its password */
47     protected $allow_pass_change = true;
48     /** @var array List of blogs on which the user has permissions */
49     protected $blogs = array();
50     /** @var integer Count of user blogs */
51     public $blog_count = null;
[2566]52
[333]53     /** @var array Permission types */
54     protected $perm_types;
[2566]55
[333]56     /** @var dcPrefs dcPrefs object */
57     public $user_prefs;
[2566]58
[0]59     /**
[333]60     * Class constructor. Takes dcCore object as single argument.
[2566]61     *
[333]62     * @param dcCore     $core          dcCore object
[0]63     */
64     public function __construct($core)
65     {
66          $this->core =& $core;
67          $this->con =& $core->con;
68          $this->blog_table = $core->prefix.'blog';
69          $this->user_table = $core->prefix.'user';
70          $this->perm_table = $core->prefix.'permissions';
[2566]71
[0]72          $this->perm_types = array(
73               'admin' => __('administrator'),
74               'usage' => __('manage their own entries and comments'),
75               'publish' => __('publish entries and comments'),
76               'delete' => __('delete entries and comments'),
77               'contentadmin' => __('manage all entries and comments'),
78               'categories' => __('manage categories'),
79               'media' => __('manage their own media items'),
80               'media_admin' => __('manage all media items')
81          );
82     }
[2566]83
[0]84     /// @name Credentials and user permissions
85     //@{
86     /**
[333]87     * Checks if user exists and can log in. <var>$pwd</var> argument is optionnal
88     * while you may need to check user without password. This method will create
89     * credentials and populate all needed object properties.
[2566]90     *
[333]91     * @param string     $user_id       User ID
92     * @param string     $pwd           User password
93     * @param string     $user_key      User key check
94     * @param boolean    $check_blog    checks if user is associated to a blog or not.
95     * @return boolean
[0]96     */
[312]97     public function checkUser($user_id, $pwd=null, $user_key=null, $check_blog=true)
[0]98     {
99          # Check user and password
100          $strReq = 'SELECT user_id, user_super, user_pwd, user_change_pwd, '.
101                    'user_name, user_firstname, user_displayname, user_email, '.
102                    'user_url, user_default_blog, user_options, '.
103                    'user_lang, user_tz, user_post_status, user_creadt, user_upddt '.
104                    'FROM '.$this->con->escapeSystem($this->user_table).' '.
105                    "WHERE user_id = '".$this->con->escape($user_id)."' ";
[2566]106
[0]107          try {
108               $rs = $this->con->select($strReq);
109          } catch (Exception $e) {
110               $err = $e->getMessage();
111               return false;
[2566]112          }
113
[0]114          if ($rs->isEmpty()) {
[2311]115               sleep(rand(2,5));
[0]116               return false;
117          }
[2566]118
[0]119          $rs->extend('rsExtUser');
[2566]120
[0]121          if ($pwd != '')
122          {
[3627]123               $rehash = false;
124               if (password_verify($pwd,$rs->user_pwd)) {
125                    // User password ok
126                    if (password_needs_rehash($rs->user_pwd,PASSWORD_DEFAULT)) {
127                         $rs->user_pwd = $this->crypt($pwd);
128                         $rehash = true;
129                    }
130               } else {
131                    // Check if pwd still stored in old fashion way
132                    $ret = password_get_info($rs->user_pwd);
133                    if (is_array($ret) && isset($ret['algo']) && $ret['algo'] == 0) {
134                         // hash not done with password_hash() function, check by old fashion way
135                         if (crypt::hmac(DC_MASTER_KEY,$pwd,DC_CRYPT_ALGO) == $rs->user_pwd) {
136                              // Password Ok, need to store it in new fashion way
137                              $rs->user_pwd = $this->crypt($pwd);
138                              $rehash = true;
139                         } else {
140                              // Password KO
141                              sleep(rand(2,5));
142                              return false;
143                         }
144                    } else {
145                         // Password KO
146                         sleep(rand(2,5));
147                         return false;
148                    }
149               }
150               if ($rehash) {
151                    // Store new hash in DB
152                    $cur = $this->con->openCursor($this->user_table);
153                    $cur->user_pwd = (string) $rs->user_pwd;
154                    $cur->update("WHERE user_id = '".$rs->user_id."'");
[0]155               }
156          }
157          elseif ($user_key != '')
158          {
[3627]159               if (http::browserUID(DC_MASTER_KEY.$rs->user_id.$this->cryptLegacy($rs->user_id)) != $user_key) {
[0]160                    return false;
161               }
162          }
[2566]163
[0]164          $this->user_id = $rs->user_id;
165          $this->user_change_pwd = (boolean) $rs->user_change_pwd;
166          $this->user_admin = (boolean) $rs->user_super;
[2566]167
[0]168          $this->user_info['user_pwd'] = $rs->user_pwd;
169          $this->user_info['user_name'] = $rs->user_name;
170          $this->user_info['user_firstname'] = $rs->user_firstname;
171          $this->user_info['user_displayname'] = $rs->user_displayname;
172          $this->user_info['user_email'] = $rs->user_email;
173          $this->user_info['user_url'] = $rs->user_url;
174          $this->user_info['user_default_blog'] = $rs->user_default_blog;
175          $this->user_info['user_lang'] = $rs->user_lang;
176          $this->user_info['user_tz'] = $rs->user_tz;
177          $this->user_info['user_post_status'] = $rs->user_post_status;
178          $this->user_info['user_creadt'] = $rs->user_creadt;
179          $this->user_info['user_upddt'] = $rs->user_upddt;
[2566]180
[0]181          $this->user_info['user_cn'] = dcUtils::getUserCN($rs->user_id, $rs->user_name,
182          $rs->user_firstname, $rs->user_displayname);
[2566]183
[0]184          $this->user_options = array_merge($this->core->userDefaults(),$rs->options());
[2566]185
[3]186          $this->user_prefs = new dcPrefs($this->core,$this->user_id);
[2566]187
[0]188          # Get permissions on blogs
[312]189          if ($check_blog && ($this->findUserBlog() === false)) {
[0]190               return false;
191          }
192          return true;
193     }
[2566]194
[0]195     /**
[3036]196      * This method crypt given string (password, session_id, …).
197      *
198      * @param string $pwd string to be crypted
199      * @return string crypted value
200      */
201     public function crypt($pwd)
202     {
[3627]203          return password_hash($pwd,PASSWORD_DEFAULT);
204     }
205
206     /**
207      * This method crypt given string (password, session_id, …).
208      *
209      * @param string $pwd string to be crypted
210      * @return string crypted value
211      */
212     public function cryptLegacy($pwd)
213     {
[3231]214          return crypt::hmac(DC_MASTER_KEY,$pwd,DC_CRYPT_ALGO);
[3036]215     }
216
217     /**
[333]218     * This method only check current user password.
[2566]219     *
[333]220     * @param string     $pwd           User password
221     * @return boolean
[0]222     */
223     public function checkPassword($pwd)
224     {
225          if (!empty($this->user_info['user_pwd'])) {
[3627]226               return password_verify($pwd,$this->user_info['user_pwd']);
[0]227          }
[2566]228
[0]229          return false;
230     }
[2566]231
[0]232     /**
[333]233     * This method checks if user session cookie exists
[2566]234     *
[333]235     * @return boolean
[0]236     */
237     public function sessionExists()
238     {
239          return isset($_COOKIE[DC_SESSION_NAME]);
240     }
[2566]241
[0]242     /**
[333]243     * This method checks user session validity.
[2566]244     *
[333]245     * @return boolean
[0]246     */
247     public function checkSession($uid=null)
248     {
249          $this->core->session->start();
[2566]250
[0]251          # If session does not exist, logout.
252          if (!isset($_SESSION['sess_user_id'])) {
253               $this->core->session->destroy();
254               return false;
255          }
[2566]256
[0]257          # Check here for user and IP address
258          $this->checkUser($_SESSION['sess_user_id']);
[3340]259          $uid = $uid ?: http::browserUID(DC_MASTER_KEY);
[2566]260
[0]261          $user_can_log = $this->userID() !== null && $uid == $_SESSION['sess_browser_uid'];
[2566]262
[0]263          if (!$user_can_log) {
264               $this->core->session->destroy();
265               return false;
266          }
[2566]267
[0]268          return true;
269     }
[2566]270
[0]271     /**
272     * Checks if user must change his password in order to login.
273     *
274     * @return boolean
275     */
276     public function mustChangePassword()
277     {
278          return $this->user_change_pwd;
279     }
[2566]280
[0]281     /**
[333]282     * Checks if user is super admin
[2566]283     *
[333]284     * @return boolean
[0]285     */
286     public function isSuperAdmin()
287     {
288          return $this->user_admin;
289     }
[2566]290
[0]291     /**
[333]292     * Checks if user has permissions given in <var>$permissions</var> for blog
293     * <var>$blog_id</var>. <var>$permissions</var> is a coma separated list of
294     * permissions.
[2566]295     *
[333]296     * @param string     $permissions   Permissions list
297     * @param string     $blog_id       Blog ID
298     * @return boolean
[0]299     */
300     public function check($permissions,$blog_id)
301     {
302          if ($this->user_admin) {
303               return true;
304          }
[2566]305
[3298]306          $p = array_map('trim',explode(',',$permissions));
[0]307          $b = $this->getPermissions($blog_id);
[2566]308
[0]309          if ($b != false)
310          {
311               if (isset($b['admin'])) {
312                    return true;
313               }
[2566]314
[0]315               foreach ($p as $v)
316               {
317                    if (isset($b[$v])) {
318                         return true;
319                    }
320               }
321          }
[2566]322
[0]323          return false;
324     }
[2566]325
[0]326     /**
[333]327     * Returns true if user is allowed to change its password.
[2566]328     *
[333]329     * @return boolean
[0]330     */
331     public function allowPassChange()
332     {
333          return $this->allow_pass_change;
334     }
335     //@}
[2566]336
[0]337     /// @name User code handlers
338     //@{
339     public function getUserCode()
340     {
341          $code =
342          pack('a32',$this->userID()).
[3036]343          pack('H*',$this->crypt($this->getInfo('user_pwd')));
[0]344          return bin2hex($code);
345     }
[2566]346
[0]347     public function checkUserCode($code)
348     {
349          $code = @pack('H*',$code);
[2566]350
[0]351          $user_id = trim(@pack('a32',substr($code,0,32)));
[3231]352          $pwd = @unpack('H*hex',substr($code,32));
[2566]353
[0]354          if ($user_id === false || $pwd === false) {
355               return false;
356          }
[2566]357
[0]358          $pwd = $pwd['hex'];
[2566]359
[0]360          $strReq = 'SELECT user_id, user_pwd '.
361                    'FROM '.$this->user_table.' '.
362                    "WHERE user_id = '".$this->con->escape($user_id)."' ";
[2566]363
[0]364          $rs = $this->con->select($strReq);
[2566]365
[0]366          if ($rs->isEmpty()) {
367               return false;
368          }
[2566]369
[3036]370          if ($this->crypt($rs->user_pwd) != $pwd) {
[0]371               return false;
372          }
[2566]373
[0]374          return $rs->user_id;
375     }
376     //@}
[2566]377
378
[0]379     /// @name Sudo
380     //@{
381     /**
[333]382     * Calls $f function with super admin rights.
383     * Returns the function result.
[2566]384     *
[333]385     * @param callback   $f             Callback function
386     * @return mixed
[0]387     */
388     public function sudo($f)
389     {
390          if (!is_callable($f)) {
391               throw new Exception($f.' function doest not exist');
392          }
[2566]393
[0]394          $args = func_get_args();
395          array_shift($args);
[2566]396
[0]397          if ($this->user_admin) {
398               $res = call_user_func_array($f,$args);
399          } else {
400               $this->user_admin = true;
401               try {
402                    $res = call_user_func_array($f,$args);
403                    $this->user_admin = false;
404               } catch (Exception $e) {
405                    $this->user_admin = false;
406                    throw $e;
407               }
408          }
[2566]409
[0]410          return $res;
411     }
412     //@}
[2566]413
[0]414     /// @name User information and options
415     //@{
416     /**
[333]417     * Returns user permissions for a blog as an array which looks like:
[2566]418     *
[333]419     *  - [blog_id]
420     *    - [permission] => true
421     *    - ...
[2566]422     *
[333]423     * @param string     $blog_id       Blog ID
424     * @return array
[0]425     */
426     public function getPermissions($blog_id)
427     {
428          if (isset($this->blogs[$blog_id])) {
429               return $this->blogs[$blog_id];
430          }
[2159]431
[0]432          if ($this->user_admin) {
433               $strReq = 'SELECT blog_id '.
434                    'from '.$this->blog_table.' '.
435                    "WHERE blog_id = '".$this->con->escape($blog_id)."' ";
436               $rs = $this->con->select($strReq);
[2566]437
[0]438               $this->blogs[$blog_id] = $rs->isEmpty() ? false : array('admin' => true);
[2566]439
[0]440               return $this->blogs[$blog_id];
441          }
[2566]442
[0]443          $strReq = 'SELECT permissions '.
444                    'FROM '.$this->perm_table.' '.
445                    "WHERE user_id = '".$this->con->escape($this->user_id)."' ".
446                    "AND blog_id = '".$this->con->escape($blog_id)."' ".
447                    "AND (permissions LIKE '%|usage|%' OR permissions LIKE '%|admin|%' OR permissions LIKE '%|contentadmin|%') ";
448          $rs = $this->con->select($strReq);
[2566]449
[0]450          $this->blogs[$blog_id] = $rs->isEmpty() ? false : $this->parsePermissions($rs->permissions);
[2566]451
[0]452          return $this->blogs[$blog_id];
453     }
[2159]454
455    public function getBlogCount() {
456          if ($this->blog_count === null) {
457               $this->blog_count = $this->core->getBlogs(array(),true)->f(0);
458          }
459
460        return $this->blog_count;
461    }
[2566]462
[0]463     public function findUserBlog($blog_id=null)
464     {
465          if ($blog_id && $this->getPermissions($blog_id) !== false)
466          {
467               return $blog_id;
468          }
469          else
470          {
471               if ($this->user_admin)
472               {
473                    $strReq = 'SELECT blog_id '.
474                              'FROM '.$this->blog_table.' '.
475                              'ORDER BY blog_id ASC '.
476                              $this->con->limit(1);
477               }
478               else
479               {
[3399]480                    $strReq = 'SELECT P.blog_id '.
481                              'FROM '.$this->perm_table.' P, '.$this->blog_table.' B '.
[0]482                              "WHERE user_id = '".$this->con->escape($this->user_id)."' ".
[3399]483                              "AND P.blog_id = B.blog_id ".
[0]484                              "AND (permissions LIKE '%|usage|%' OR permissions LIKE '%|admin|%' OR permissions LIKE '%|contentadmin|%') ".
[3399]485                              "AND blog_status >= 0 ".
486                              'ORDER BY P.blog_id ASC '.
[0]487                              $this->con->limit(1);
488               }
[2566]489
[0]490               $rs = $this->con->select($strReq);
491               if (!$rs->isEmpty()) {
492                    return $rs->blog_id;
493               }
494          }
[2566]495
[0]496          return false;
497     }
[2566]498
[0]499     /**
[333]500     * Returns current user ID
[2566]501     *
[333]502     * @return string
[0]503     */
504     public function userID()
505     {
506          return $this->user_id;
507     }
[2566]508
[0]509     /**
[333]510     * Returns information about a user .
[2566]511     *
[333]512     * @param string     $n             Information name
513     * @return string
[0]514     */
515     public function getInfo($n)
516     {
517          if (isset($this->user_info[$n])) {
518               return $this->user_info[$n];
519          }
[2566]520
[0]521          return null;
522     }
[2566]523
[0]524     /**
[333]525     * Returns a specific user option
[2566]526     *
[333]527     * @param string     $n             Option name
528     * @return string
[0]529     */
530     public function getOption($n)
531     {
532          if (isset($this->user_options[$n])) {
533               return $this->user_options[$n];
534          }
535          return null;
536     }
[2566]537
[0]538     /**
[333]539     * Returns all user options in an associative array.
[2566]540     *
[333]541     * @return array
[0]542     */
543     public function getOptions()
544     {
545          return $this->user_options;
546     }
547     //@}
[2566]548
[0]549     /// @name Permissions
550     //@{
551     /**
[333]552     * Returns an array with permissions parsed from the string <var>$level</var>
[2566]553     *
[333]554     * @param string     $level         Permissions string
555     * @return array
[0]556     */
557     public function parsePermissions($level)
558     {
559          $level = preg_replace('/^\|/','',$level);
560          $level = preg_replace('/\|$/','',$level);
[2566]561
[0]562          $res = array();
563          foreach (explode('|',$level) as $v) {
564               $res[$v] = true;
565          }
566          return $res;
567     }
[2566]568
[0]569     /**
[333]570     * Returns <var>perm_types</var> property content.
[2566]571     *
[333]572     * @return array
[0]573     */
574     public function getPermissionsTypes()
575     {
576          return $this->perm_types;
577     }
[2566]578
[0]579     /**
[333]580     * Adds a new permission type.
[2566]581     *
[333]582     * @param string     $name          Permission name
583     * @param string     $title         Permission title
[0]584     */
585     public function setPermissionType($name,$title)
586     {
587          $this->perm_types[$name] = $title;
588     }
589     //@}
[2566]590
[0]591     /// @name Password recovery
592     //@{
593     /**
[333]594     * Add a recover key to a specific user identified by its email and
595     * password.
[2566]596     *
[333]597     * @param string     $user_id       User ID
598     * @param string     $user_email    User Email
599     * @return string
[0]600     */
601     public function setRecoverKey($user_id,$user_email)
602     {
603          $strReq = 'SELECT user_id '.
604                    'FROM '.$this->user_table.' '.
605                    "WHERE user_id = '".$this->con->escape($user_id)."' ".
606                    "AND user_email = '".$this->con->escape($user_email)."' ";
[2566]607
[0]608          $rs = $this->con->select($strReq);
[2566]609
[0]610          if ($rs->isEmpty()) {
611               throw new Exception(__('That user does not exist in the database.'));
612          }
[2566]613
[3512]614          $key = md5(uniqid('',true));
[2566]615
[0]616          $cur = $this->con->openCursor($this->user_table);
617          $cur->user_recover_key = $key;
[2566]618
[0]619          $cur->update("WHERE user_id = '".$this->con->escape($user_id)."'");
[2566]620
[0]621          return $key;
622     }
[2566]623
[0]624     /**
[333]625     * Creates a new user password using recovery key. Returns an array:
[2566]626     *
[333]627     * - user_email
628     * - user_id
629     * - new_pass
[2566]630     *
[333]631     * @param string     $recover_key   Recovery key
632     * @return array
[0]633     */
634     public function recoverUserPassword($recover_key)
635     {
636          $strReq = 'SELECT user_id, user_email '.
637                    'FROM '.$this->user_table.' '.
638                    "WHERE user_recover_key = '".$this->con->escape($recover_key)."' ";
[2566]639
[0]640          $rs = $this->con->select($strReq);
[2566]641
[0]642          if ($rs->isEmpty()) {
643               throw new Exception(__('That key does not exist in the database.'));
644          }
[2566]645
[0]646          $new_pass = crypt::createPassword();
[2566]647
[0]648          $cur = $this->con->openCursor($this->user_table);
[3036]649          $cur->user_pwd = $this->crypt($new_pass);
[0]650          $cur->user_recover_key = null;
[3535]651          $cur->user_change_pwd = 1;         // User will have to change this temporary password at next login
[2566]652
[0]653          $cur->update("WHERE user_recover_key = '".$this->con->escape($recover_key)."'");
[2566]654
[0]655          return array('user_email' => $rs->user_email, 'user_id' => $rs->user_id, 'new_pass' => $new_pass);
656     }
657     //@}
[2566]658
[0]659     /** @name User management callbacks
660     This 3 functions only matter if you extend this class and use
661     DC_AUTH_CLASS constant.
662     These are called after core user management functions.
663     Could be useful if you need to add/update/remove stuff in your
664     LDAP directory or other third party authentication database.
665     */
666     //@{
[2566]667
[0]668     /**
[333]669     * Called after core->addUser
670     * @see dcCore::addUser
671     * @param cursor     $cur           User cursor
[0]672     */
673     public function afterAddUser($cur) {}
[2566]674
[0]675     /**
[333]676     * Called after core->updUser
677     * @see dcCore::updUser
678     * @param string     $id            User ID
679     * @param cursor     $cur           User cursor
[0]680     */
681     public function afterUpdUser($id,$cur) {}
[2566]682
[0]683     /**
[333]684     * Called after core->delUser
685     * @see dcCore::delUser
686     * @param string     $id            User ID
[0]687     */
688     public function afterDelUser($id) {}
689     //@}
690}
Note: See TracBrowser for help on using the repository browser.

Sites map