Subversion Repositories oidplus

Rev

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