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)
Line 
1<?php
2# -- BEGIN LICENSE BLOCK ---------------------------------------
3#
4# This file is part of Dotclear 2.
5#
6# Copyright (c) 2003-2013 Olivier Meunier & Association Dotclear
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/**
15* @ingroup DC_CORE
16* @nosubgrouping
17* @brief Authentication and user credentials management
18*
19* dcAuth is a class used to handle everything related to user authentication
20* and credentials. Object is provided by dcCore $auth property.
21*/
22class dcAuth
23{
24     /** @var dcCore dcCore instance */
25     protected $core;
26     /** @var connection Database connection object */
27     protected $con;
28
29     /** @var string User table name */
30     protected $user_table;
31     /** @var string Perm table name */
32     protected $perm_table;
33
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;
52
53     /** @var array Permission types */
54     protected $perm_types;
55
56     /** @var dcPrefs dcPrefs object */
57     public $user_prefs;
58
59     /**
60     * Class constructor. Takes dcCore object as single argument.
61     *
62     * @param dcCore     $core          dcCore object
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';
71
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     }
83
84     /// @name Credentials and user permissions
85     //@{
86     /**
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.
90     *
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
96     */
97     public function checkUser($user_id, $pwd=null, $user_key=null, $check_blog=true)
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)."' ";
106
107          try {
108               $rs = $this->con->select($strReq);
109          } catch (Exception $e) {
110               $err = $e->getMessage();
111               return false;
112          }
113
114          if ($rs->isEmpty()) {
115               sleep(rand(2,5));
116               return false;
117          }
118
119          $rs->extend('rsExtUser');
120
121          if ($pwd != '')
122          {
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."'");
155               }
156          }
157          elseif ($user_key != '')
158          {
159               if (http::browserUID(DC_MASTER_KEY.$rs->user_id.$this->cryptLegacy($rs->user_id)) != $user_key) {
160                    return false;
161               }
162          }
163
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;
167
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;
180
181          $this->user_info['user_cn'] = dcUtils::getUserCN($rs->user_id, $rs->user_name,
182          $rs->user_firstname, $rs->user_displayname);
183
184          $this->user_options = array_merge($this->core->userDefaults(),$rs->options());
185
186          $this->user_prefs = new dcPrefs($this->core,$this->user_id);
187
188          # Get permissions on blogs
189          if ($check_blog && ($this->findUserBlog() === false)) {
190               return false;
191          }
192          return true;
193     }
194
195     /**
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     {
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     {
214          return crypt::hmac(DC_MASTER_KEY,$pwd,DC_CRYPT_ALGO);
215     }
216
217     /**
218     * This method only check current user password.
219     *
220     * @param string     $pwd           User password
221     * @return boolean
222     */
223     public function checkPassword($pwd)
224     {
225          if (!empty($this->user_info['user_pwd'])) {
226               return password_verify($pwd,$this->user_info['user_pwd']);
227          }
228
229          return false;
230     }
231
232     /**
233     * This method checks if user session cookie exists
234     *
235     * @return boolean
236     */
237     public function sessionExists()
238     {
239          return isset($_COOKIE[DC_SESSION_NAME]);
240     }
241
242     /**
243     * This method checks user session validity.
244     *
245     * @return boolean
246     */
247     public function checkSession($uid=null)
248     {
249          $this->core->session->start();
250
251          # If session does not exist, logout.
252          if (!isset($_SESSION['sess_user_id'])) {
253               $this->core->session->destroy();
254               return false;
255          }
256
257          # Check here for user and IP address
258          $this->checkUser($_SESSION['sess_user_id']);
259          $uid = $uid ?: http::browserUID(DC_MASTER_KEY);
260
261          $user_can_log = $this->userID() !== null && $uid == $_SESSION['sess_browser_uid'];
262
263          if (!$user_can_log) {
264               $this->core->session->destroy();
265               return false;
266          }
267
268          return true;
269     }
270
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     }
280
281     /**
282     * Checks if user is super admin
283     *
284     * @return boolean
285     */
286     public function isSuperAdmin()
287     {
288          return $this->user_admin;
289     }
290
291     /**
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.
295     *
296     * @param string     $permissions   Permissions list
297     * @param string     $blog_id       Blog ID
298     * @return boolean
299     */
300     public function check($permissions,$blog_id)
301     {
302          if ($this->user_admin) {
303               return true;
304          }
305
306          $p = array_map('trim',explode(',',$permissions));
307          $b = $this->getPermissions($blog_id);
308
309          if ($b != false)
310          {
311               if (isset($b['admin'])) {
312                    return true;
313               }
314
315               foreach ($p as $v)
316               {
317                    if (isset($b[$v])) {
318                         return true;
319                    }
320               }
321          }
322
323          return false;
324     }
325
326     /**
327     * Returns true if user is allowed to change its password.
328     *
329     * @return boolean
330     */
331     public function allowPassChange()
332     {
333          return $this->allow_pass_change;
334     }
335     //@}
336
337     /// @name User code handlers
338     //@{
339     public function getUserCode()
340     {
341          $code =
342          pack('a32',$this->userID()).
343          pack('H*',$this->crypt($this->getInfo('user_pwd')));
344          return bin2hex($code);
345     }
346
347     public function checkUserCode($code)
348     {
349          $code = @pack('H*',$code);
350
351          $user_id = trim(@pack('a32',substr($code,0,32)));
352          $pwd = @unpack('H*hex',substr($code,32));
353
354          if ($user_id === false || $pwd === false) {
355               return false;
356          }
357
358          $pwd = $pwd['hex'];
359
360          $strReq = 'SELECT user_id, user_pwd '.
361                    'FROM '.$this->user_table.' '.
362                    "WHERE user_id = '".$this->con->escape($user_id)."' ";
363
364          $rs = $this->con->select($strReq);
365
366          if ($rs->isEmpty()) {
367               return false;
368          }
369
370          if ($this->crypt($rs->user_pwd) != $pwd) {
371               return false;
372          }
373
374          return $rs->user_id;
375     }
376     //@}
377
378
379     /// @name Sudo
380     //@{
381     /**
382     * Calls $f function with super admin rights.
383     * Returns the function result.
384     *
385     * @param callback   $f             Callback function
386     * @return mixed
387     */
388     public function sudo($f)
389     {
390          if (!is_callable($f)) {
391               throw new Exception($f.' function doest not exist');
392          }
393
394          $args = func_get_args();
395          array_shift($args);
396
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          }
409
410          return $res;
411     }
412     //@}
413
414     /// @name User information and options
415     //@{
416     /**
417     * Returns user permissions for a blog as an array which looks like:
418     *
419     *  - [blog_id]
420     *    - [permission] => true
421     *    - ...
422     *
423     * @param string     $blog_id       Blog ID
424     * @return array
425     */
426     public function getPermissions($blog_id)
427     {
428          if (isset($this->blogs[$blog_id])) {
429               return $this->blogs[$blog_id];
430          }
431
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);
437
438               $this->blogs[$blog_id] = $rs->isEmpty() ? false : array('admin' => true);
439
440               return $this->blogs[$blog_id];
441          }
442
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);
449
450          $this->blogs[$blog_id] = $rs->isEmpty() ? false : $this->parsePermissions($rs->permissions);
451
452          return $this->blogs[$blog_id];
453     }
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    }
462
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               {
480                    $strReq = 'SELECT P.blog_id '.
481                              'FROM '.$this->perm_table.' P, '.$this->blog_table.' B '.
482                              "WHERE user_id = '".$this->con->escape($this->user_id)."' ".
483                              "AND P.blog_id = B.blog_id ".
484                              "AND (permissions LIKE '%|usage|%' OR permissions LIKE '%|admin|%' OR permissions LIKE '%|contentadmin|%') ".
485                              "AND blog_status >= 0 ".
486                              'ORDER BY P.blog_id ASC '.
487                              $this->con->limit(1);
488               }
489
490               $rs = $this->con->select($strReq);
491               if (!$rs->isEmpty()) {
492                    return $rs->blog_id;
493               }
494          }
495
496          return false;
497     }
498
499     /**
500     * Returns current user ID
501     *
502     * @return string
503     */
504     public function userID()
505     {
506          return $this->user_id;
507     }
508
509     /**
510     * Returns information about a user .
511     *
512     * @param string     $n             Information name
513     * @return string
514     */
515     public function getInfo($n)
516     {
517          if (isset($this->user_info[$n])) {
518               return $this->user_info[$n];
519          }
520
521          return null;
522     }
523
524     /**
525     * Returns a specific user option
526     *
527     * @param string     $n             Option name
528     * @return string
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     }
537
538     /**
539     * Returns all user options in an associative array.
540     *
541     * @return array
542     */
543     public function getOptions()
544     {
545          return $this->user_options;
546     }
547     //@}
548
549     /// @name Permissions
550     //@{
551     /**
552     * Returns an array with permissions parsed from the string <var>$level</var>
553     *
554     * @param string     $level         Permissions string
555     * @return array
556     */
557     public function parsePermissions($level)
558     {
559          $level = preg_replace('/^\|/','',$level);
560          $level = preg_replace('/\|$/','',$level);
561
562          $res = array();
563          foreach (explode('|',$level) as $v) {
564               $res[$v] = true;
565          }
566          return $res;
567     }
568
569     /**
570     * Returns <var>perm_types</var> property content.
571     *
572     * @return array
573     */
574     public function getPermissionsTypes()
575     {
576          return $this->perm_types;
577     }
578
579     /**
580     * Adds a new permission type.
581     *
582     * @param string     $name          Permission name
583     * @param string     $title         Permission title
584     */
585     public function setPermissionType($name,$title)
586     {
587          $this->perm_types[$name] = $title;
588     }
589     //@}
590
591     /// @name Password recovery
592     //@{
593     /**
594     * Add a recover key to a specific user identified by its email and
595     * password.
596     *
597     * @param string     $user_id       User ID
598     * @param string     $user_email    User Email
599     * @return string
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)."' ";
607
608          $rs = $this->con->select($strReq);
609
610          if ($rs->isEmpty()) {
611               throw new Exception(__('That user does not exist in the database.'));
612          }
613
614          $key = md5(uniqid('',true));
615
616          $cur = $this->con->openCursor($this->user_table);
617          $cur->user_recover_key = $key;
618
619          $cur->update("WHERE user_id = '".$this->con->escape($user_id)."'");
620
621          return $key;
622     }
623
624     /**
625     * Creates a new user password using recovery key. Returns an array:
626     *
627     * - user_email
628     * - user_id
629     * - new_pass
630     *
631     * @param string     $recover_key   Recovery key
632     * @return array
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)."' ";
639
640          $rs = $this->con->select($strReq);
641
642          if ($rs->isEmpty()) {
643               throw new Exception(__('That key does not exist in the database.'));
644          }
645
646          $new_pass = crypt::createPassword();
647
648          $cur = $this->con->openCursor($this->user_table);
649          $cur->user_pwd = $this->crypt($new_pass);
650          $cur->user_recover_key = null;
651          $cur->user_change_pwd = 1;         // User will have to change this temporary password at next login
652
653          $cur->update("WHERE user_recover_key = '".$this->con->escape($recover_key)."'");
654
655          return array('user_email' => $rs->user_email, 'user_id' => $rs->user_id, 'new_pass' => $new_pass);
656     }
657     //@}
658
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     //@{
667
668     /**
669     * Called after core->addUser
670     * @see dcCore::addUser
671     * @param cursor     $cur           User cursor
672     */
673     public function afterAddUser($cur) {}
674
675     /**
676     * Called after core->updUser
677     * @see dcCore::updUser
678     * @param string     $id            User ID
679     * @param cursor     $cur           User cursor
680     */
681     public function afterUpdUser($id,$cur) {}
682
683     /**
684     * Called after core->delUser
685     * @see dcCore::delUser
686     * @param string     $id            User ID
687     */
688     public function afterDelUser($id) {}
689     //@}
690}
Note: See TracBrowser for help on using the repository browser.

Sites map