Subversion Repositories oidplus

Rev

Rev 712 | Rev 849 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed

  1. <?php
  2.  
  3. /*
  4.  * OIDplus 2.0
  5.  * Copyright 2019 - 2021 Daniel Marschall, ViaThinkSoft
  6.  *
  7.  * Licensed under the Apache License, Version 2.0 (the "License");
  8.  * you may not use this file except in compliance with the License.
  9.  * You may obtain a copy of the License at
  10.  *
  11.  *     http://www.apache.org/licenses/LICENSE-2.0
  12.  *
  13.  * Unless required by applicable law or agreed to in writing, software
  14.  * distributed under the License is distributed on an "AS IS" BASIS,
  15.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16.  * See the License for the specific language governing permissions and
  17.  * limitations under the License.
  18.  */
  19.  
  20. if (!defined('INSIDE_OIDPLUS')) die();
  21.  
  22. class OIDplusAuthUtils extends OIDplusBaseClass {
  23.  
  24.         // Useful functions
  25.  
  26.         public static function getRandomBytes($len) {
  27.                 if (function_exists('openssl_random_pseudo_bytes')) {
  28.                         $a = openssl_random_pseudo_bytes($len);
  29.                         if ($a) return $a;
  30.                 }
  31.  
  32.                 if (function_exists('mcrypt_create_iv')) {
  33.                         $a = bin2hex(mcrypt_create_iv($len, MCRYPT_DEV_URANDOM));
  34.                         if ($a) return $a;
  35.                 }
  36.  
  37.                 if (function_exists('random_bytes')) {
  38.                         $a = random_bytes($len);
  39.                         if ($a) return $a;
  40.                 }
  41.  
  42.                 // Fallback to non-secure RNG
  43.                 $a = '';
  44.                 while (strlen($a) < $len*2) {
  45.                         $a .= sha1(uniqid((string)mt_rand(), true));
  46.                 }
  47.                 $a = substr($a, 0, $len*2);
  48.                 return hex2bin($a);
  49.         }
  50.  
  51.         private static function raPepperProcessing(string $password): string {
  52.                 // Additional feature: Pepper
  53.                 // The pepper is stored inside the base configuration file
  54.                 // It prevents that an attacker with SQL write rights can
  55.                 // create accounts.
  56.                 // ATTENTION!!! If a pepper is used, then the
  57.                 // hashes are bound to that pepper. If you change the pepper,
  58.                 // then ALL passwords of RAs become INVALID!
  59.                 $pepper = OIDplus::baseConfig()->getValue('RA_PASSWORD_PEPPER','');
  60.                 if ($pepper !== '') {
  61.                         $algo = OIDplus::baseConfig()->getValue('RA_PASSWORD_PEPPER_ALGO','sha512'); // sha512 works with PHP 7.0
  62.                         if (strtolower($algo) === 'sha3-512') {
  63.                                 $hmac = sha3_512_hmac($password, $pepper);
  64.                         } else {
  65.                                 $hmac = hash_hmac($algo, $password, $pepper);
  66.                         }
  67.                         if ($hmac === false) throw new OIDplusException(_L('HMAC failed'));
  68.                         return $hmac;
  69.                 } else {
  70.                         return $password;
  71.                 }
  72.         }
  73.  
  74.         // Content provider
  75.  
  76.         public function getAuthMethod() {
  77.                 $acs = $this->getAuthContentStore();
  78.                 if (is_null($acs)) return 'null';
  79.                 return get_class($acs);
  80.         }
  81.  
  82.         protected function getAuthContentStore() {
  83.                 // Logged in via JWT
  84.                 $tmp = OIDplusAuthContentStoreJWT::getActiveProvider();
  85.                 if ($tmp) return $tmp;
  86.  
  87.                 // Normal login via web-browser
  88.                 // Cookie will only be created once content is stored
  89.                 $tmp = OIDplusAuthContentStoreSession::getActiveProvider();
  90.                 if ($tmp) return $tmp;
  91.  
  92.                 // No active session and no JWT token available. User is not logged in.
  93.                 return null;
  94.         }
  95.  
  96.         public function getExtendedAttribute($name, $default=NULL) {
  97.                 $acs = $this->getAuthContentStore();
  98.                 if (is_null($acs)) return $default;
  99.                 return $acs->getValue($name, $default);
  100.         }
  101.  
  102.         // RA authentication functions
  103.  
  104.         public function raLogin($email) {
  105.                 $acs = $this->getAuthContentStore();
  106.                 if (is_null($acs)) return;
  107.                 return $acs->raLogin($email);
  108.         }
  109.  
  110.         public function raLogout($email) {
  111.                 $acs = $this->getAuthContentStore();
  112.                 if (is_null($acs)) return;
  113.                 return $acs->raLogout($email);
  114.         }
  115.  
  116.         public function raCheckPassword($ra_email, $password) {
  117.                 $ra = new OIDplusRA($ra_email);
  118.  
  119.                 // Get RA info from RA
  120.                 $authInfo = $ra->getAuthInfo();
  121.                 if (!$authInfo) return false; // user not found
  122.  
  123.                 // Ask plugins if they can verify this hash
  124.                 $plugins = OIDplus::getAuthPlugins();
  125.                 if (count($plugins) == 0) {
  126.                         throw new OIDplusException(_L('No RA authentication plugins found'));
  127.                 }
  128.                 foreach ($plugins as $plugin) {
  129.                         if ($plugin->verify($authInfo, self::raPepperProcessing($password))) return true;
  130.                 }
  131.  
  132.                 return false;
  133.         }
  134.  
  135.         public function raNumLoggedIn() {
  136.                 $acs = $this->getAuthContentStore();
  137.                 if (is_null($acs)) return 0;
  138.                 return $acs->raNumLoggedIn();
  139.         }
  140.  
  141.         public function loggedInRaList() {
  142.                 if ($this->forceAllLoggedOut()) {
  143.                         return array();
  144.                 } else {
  145.                         $acs = $this->getAuthContentStore();
  146.                         if (is_null($acs)) return array();
  147.                         return $acs->loggedInRaList();
  148.                 }
  149.         }
  150.  
  151.         public function isRaLoggedIn($email) {
  152.                 $acs = $this->getAuthContentStore();
  153.                 if (is_null($acs)) return false;
  154.                 return $acs->isRaLoggedIn($email);
  155.         }
  156.  
  157.         // "High level" function including logging and checking for valid JWT alternations
  158.         public function raLoginEx($email, $remember_me, $origin='') {
  159.                 $loginfo = '';
  160.                 $acs = $this->getAuthContentStore();
  161.                 if (!is_null($acs)) {
  162.                         $acs->raLoginEx($email, $loginfo);
  163.                         $acs->activate();
  164.                 } else {
  165.                         if ($remember_me) {
  166.                                 if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) {
  167.                                         throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_USER'));
  168.                                 }
  169.                                 $ttl = OIDplus::baseConfig()->getValue('JWT_TTL_LOGIN_USER', 10*365*24*60*60);
  170.                                 $authSimulation = new OIDplusAuthContentStoreJWT();
  171.                                 $authSimulation->raLoginEx($email, $loginfo);
  172.                                 $authSimulation->setValue('oidplus_generator', OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN);
  173.                                 $authSimulation->setValue('exp', time()+$ttl); // JWT "exp" attribute
  174.                                 $authSimulation->activate();
  175.                         } else {
  176.                                 $authSimulation = new OIDplusAuthContentStoreSession();
  177.                                 $authSimulation->raLoginEx($email, $loginfo);
  178.                                 $authSimulation->activate();
  179.                         }
  180.                 }
  181.                 $logmsg = "RA '$email' logged in";
  182.                 if ($origin != '') $logmsg .= " via $origin";
  183.                 if ($loginfo != '') $logmsg .= " ($loginfo)";
  184.                 OIDplus::logger()->log("[OK]RA($email)!", $logmsg);
  185.         }
  186.  
  187.         public function raLogoutEx($email) {
  188.                 $loginfo = '';
  189.  
  190.                 $acs = $this->getAuthContentStore();
  191.                 if (is_null($acs)) return;
  192.                 $res = $acs->raLogoutEx($email, $loginfo);
  193.  
  194.                 OIDplus::logger()->log("[OK]RA($email)!", "RA '$email' logged out ($loginfo)");
  195.  
  196.                 if (($this->raNumLoggedIn() == 0) && (!$this->isAdminLoggedIn())) {
  197.                         // Nobody logged in anymore. Destroy session cookie to make GDPR people happy
  198.                         $acs->destroySession();
  199.                 } else {
  200.                         // Get a new token for the remaining users
  201.                         $acs->activate();
  202.                 }
  203.  
  204.                 return $res;
  205.         }
  206.  
  207.         // Admin authentication functions
  208.  
  209.         public function adminLogin() {
  210.                 $acs = $this->getAuthContentStore();
  211.                 if (is_null($acs)) return;
  212.                 return $acs->adminLogin();
  213.         }
  214.  
  215.         public function adminLogout() {
  216.                 $acs = $this->getAuthContentStore();
  217.                 if (is_null($acs)) return;
  218.                 return $acs->adminLogout();
  219.         }
  220.  
  221.         public function adminCheckPassword($password) {
  222.                 $cfgData = OIDplus::baseConfig()->getValue('ADMIN_PASSWORD', '');
  223.                 if (empty($cfgData)) {
  224.                         throw new OIDplusException(_L('No admin password set in %1','userdata/baseconfig/config.inc.php'));
  225.                 }
  226.  
  227.                 if (!is_array($cfgData)) {
  228.                         $passwordDataArray = array($cfgData);
  229.                 } else {
  230.                         $passwordDataArray = $cfgData;
  231.                 }
  232.  
  233.                 foreach ($passwordDataArray as $passwordData) {
  234.                         if (strpos($passwordData, '$') !== false) {
  235.                                 if ($passwordData[0] == '$') {
  236.                                         // Version 3: BCrypt
  237.                                         return password_verify($password, $passwordData);
  238.                                 } else {
  239.                                         // Version 2: SHA3-512 with salt
  240.                                         list($s_salt, $hash) = explode('$', $passwordData, 2);
  241.                                 }
  242.                         } else {
  243.                                 // Version 1: SHA3-512 without salt
  244.                                 $s_salt = '';
  245.                                 $hash = $passwordData;
  246.                         }
  247.  
  248.                         if (hash_equals(sha3_512($s_salt.$password, true), base64_decode($hash))) return true;
  249.                 }
  250.  
  251.                 return false;
  252.         }
  253.  
  254.         public function isAdminLoggedIn() {
  255.                 if ($this->forceAllLoggedOut()) {
  256.                         return false;
  257.                 } else {
  258.                         $acs = $this->getAuthContentStore();
  259.                         if (is_null($acs)) return false;
  260.                         return $acs->isAdminLoggedIn();
  261.                 }
  262.         }
  263.  
  264.         // "High level" function including logging and checking for valid JWT alternations
  265.         public function adminLoginEx($remember_me, $origin='') {
  266.                 $loginfo = '';
  267.                 $acs = $this->getAuthContentStore();
  268.                 if (!is_null($acs)) {
  269.                         $acs->adminLoginEx($loginfo);
  270.                         $acs->activate();
  271.                 } else {
  272.                         if ($remember_me) {
  273.                                 if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) {
  274.                                         throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_ADMIN'));
  275.                                 }
  276.                                 $ttl = OIDplus::baseConfig()->getValue('JWT_TTL_LOGIN_ADMIN', 10*365*24*60*60);
  277.                                 $authSimulation = new OIDplusAuthContentStoreJWT();
  278.                                 $authSimulation->adminLoginEx($loginfo);
  279.                                 $authSimulation->setValue('oidplus_generator', OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN);
  280.                                 $authSimulation->setValue('exp', time()+$ttl); // JWT "exp" attribute
  281.                                 $authSimulation->activate();
  282.                         } else {
  283.                                 $authSimulation = new OIDplusAuthContentStoreSession();
  284.                                 $authSimulation->adminLoginEx($loginfo);
  285.                                 $authSimulation->activate();
  286.                         }
  287.                 }
  288.                 $logmsg = "Admin logged in";
  289.                 if ($origin != '') $logmsg .= " via $origin";
  290.                 if ($loginfo != '') $logmsg .= " ($loginfo)";
  291.                 OIDplus::logger()->log("[OK]A!", $logmsg);
  292.         }
  293.  
  294.         public function adminLogoutEx() {
  295.                 $loginfo = '';
  296.  
  297.                 $acs = $this->getAuthContentStore();
  298.                 if (is_null($acs)) return;
  299.                 $res = $acs->adminLogoutEx($loginfo);
  300.  
  301.                 if ($this->raNumLoggedIn() == 0) {
  302.                         // Nobody here anymore. Destroy the cookie to make GDPR people happy
  303.                         $acs->destroySession();
  304.                 } else {
  305.                         // Get a new token for the remaining users
  306.                         $acs->activate();
  307.                 }
  308.  
  309.                 OIDplus::logger()->log("[OK]A!", "Admin logged out ($loginfo)");
  310.                 return $res;
  311.         }
  312.  
  313.         // Authentication keys for validating arguments (e.g. sent by mail)
  314.  
  315.         public static function makeAuthKey($data) {
  316.                 return sha3_512_hmac($data, 'authkey:'.OIDplus::baseConfig()->getValue('SERVER_SECRET'), false);
  317.         }
  318.  
  319.         public static function validateAuthKey($data, $auth_key) {
  320.                 return hash_equals(self::makeAuthKey($data), $auth_key);
  321.         }
  322.  
  323.         // "Veto" functions to force logout state
  324.  
  325.         protected function forceAllLoggedOut() {
  326.                 if (isset($_SERVER['SCRIPT_FILENAME']) && (basename($_SERVER['SCRIPT_FILENAME']) == 'sitemap.php')) {
  327.                         // The sitemap may not contain any confidential information,
  328.                         // even if the user is logged in, because the admin could
  329.                         // accidentally copy-paste the sitemap to a
  330.                         // search engine control panel while they are logged in
  331.                         return true;
  332.                 } else {
  333.                         return false;
  334.                 }
  335.         }
  336.  
  337.         // CSRF functions
  338.  
  339.         private $enable_csrf = true;
  340.  
  341.         public function enableCSRF() {
  342.                 $this->enable_csrf = true;
  343.         }
  344.  
  345.         public function disableCSRF() {
  346.                 $this->enable_csrf = false;
  347.         }
  348.  
  349.         public function genCSRFToken() {
  350.                 return bin2hex(self::getRandomBytes(64));
  351.         }
  352.  
  353.         public function checkCSRF() {
  354.                 if (!$this->enable_csrf) return;
  355.                 if (!isset($_REQUEST['csrf_token']) || !isset($_COOKIE['csrf_token']) || ($_REQUEST['csrf_token'] !== $_COOKIE['csrf_token'])) {
  356.                         throw new OIDplusException(_L('Wrong CSRF Token'));
  357.                 }
  358.         }
  359.  
  360.         // Generate RA passwords
  361.  
  362.         public static function raGeneratePassword($password): OIDplusRAAuthInfo {
  363.                 $def_method = OIDplus::config()->getValue('default_ra_auth_method');
  364.  
  365.                 $plugins = OIDplus::getAuthPlugins();
  366.                 foreach ($plugins as $plugin) {
  367.                         if (basename($plugin->getPluginDirectory()) === $def_method) {
  368.                                 return $plugin->generate(self::raPepperProcessing($password));
  369.                         }
  370.                 }
  371.                 throw new OIDplusException(_L('Default RA auth method/plugin "%1" not found',$def_method));
  372.         }
  373.  
  374.         // Generate admin password
  375.  
  376.         /* Nothing here; the admin password will be generated in setup_base.js , purely in the web-browser */
  377.  
  378. }
  379.