Subversion Repositories oidplus

Rev

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