Subversion Repositories oidplus

Rev

Rev 576 | Rev 578 | 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 {
  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(mt_rand(), true));
  46.                 }
  47.                 $a = substr($a, 0, $len*2);
  48.                 return hex2bin($a);
  49.         }
  50.  
  51.         // JWT handling
  52.  
  53.         const JWT_GENERATOR_AJAX   = 0;
  54.         //const JWT_GENERATOR_LOGIN  = 1;
  55.         const JWT_GENERATOR_MANUAL = 2;
  56.  
  57.         private function jwtGetBlacklistConfigKey($gen, $sub) {
  58.                 // Note: Needs to be <= 50 characters!
  59.                 return 'jwt_blacklist_gen('.$gen.')_sub('.trim(base64_encode(md5($sub,true)),'=').')';
  60.         }
  61.  
  62.         public function jwtBlacklist($gen, $sub) {
  63.                 $cfg = $this->jwtGetBlacklistConfigKey($gen, $sub);
  64.                 $bl_time = time()-1;
  65.  
  66.                 $gen_desc = 'Unknown';
  67.                 if ($gen === self::JWT_GENERATOR_AJAX)   $gen_desc = 'Automated AJAX calls';
  68.                 //if ($gen === self::JWT_GENERATOR_LOGIN)  $gen_desc = 'Login';
  69.                 if ($gen === self::JWT_GENERATOR_MANUAL) $gen_desc = 'Manually created';
  70.  
  71.                 OIDplus::config()->prepareConfigKey($cfg, 'Revoke timestamp of all JWT tokens for $sub with generator $gen ($gen_desc)', $bl_time, OIDplusConfig::PROTECTION_HIDDEN, function($value) {});
  72.                 OIDplus::config()->setValue($cfg, $bl_time);
  73.         }
  74.  
  75.         public function jwtGetBlacklistTime($gen, $sub) {
  76.                 $cfg = $this->jwtGetBlacklistConfigKey($gen, $sub);
  77.                 return OIDplus::config()->getValue($cfg,0);
  78.         }
  79.  
  80.         protected function jwtSecurityCheck($contentProvider) {
  81.                 // Check if the token is intended for us
  82.                 if ($contentProvider->getValue('aud','') !== "http://oidplus.com") {
  83.                         throw new OIDplusException(_L('Token has wrong audience'));
  84.                 }
  85.                 $gen = $contentProvider->getValue('oidplus_generator', -1);
  86.                 $sub = $contentProvider->getValue('sub', '');
  87.  
  88.                 // Check if the token generator is allowed
  89.                 if ($gen === self::JWT_GENERATOR_AJAX) {
  90.                         if (($sub === 'admin') && !OIDplus::baseConfig()->getValue('JWT_ALLOW_AJAX_ADMIN', true)) {
  91.                                 // Generator: plugins/adminPages/910_automated_ajax_calls/OIDplusPageAdminAutomatedAJAXCalls.class.php
  92.                                 throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_AJAX_ADMIN'));
  93.                         }
  94.                         else if (($sub !== 'admin') && !OIDplus::baseConfig()->getValue('JWT_ALLOW_AJAX_USER', true)) {
  95.                                 // Generator: plugins/raPages/910_automated_ajax_calls/OIDplusPageRaAutomatedAJAXCalls.class.php
  96.                                 throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_AJAX_USER'));
  97.                         }
  98.                 }
  99.                 /* else if ($gen === self::JWT_GENERATOR_LOGIN) {
  100.                         // Reserved for future use (use JWT token in a cookie as alternative to PHP session):
  101.                         if (($sub === 'admin') && !OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) {
  102.                                 throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_ADMIN'));
  103.                         }
  104.                         else if (($sub !== 'admin') && !OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) {
  105.                                 throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_USER'));
  106.                         }
  107.                 } */
  108.                 else if ($gen === self::JWT_GENERATOR_MANUAL) {
  109.                         // Generator 2 are "hand-crafted" tokens
  110.                         if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_MANUAL', true)) {
  111.                                 throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_MANUAL'));
  112.                         }
  113.                 } else {
  114.                         throw new OIDplusException(_L('Token generator %1 not recognized',$gen));
  115.                 }
  116.  
  117.                 // Make sure that the IAT (issued at time) isn't in a blacklisted timeframe
  118.                 // When an user believes that a token was compromised, then they can blacklist the tokens identified by their "iat" ("Issued at") property
  119.                 $bl_time = $this->jwtGetBlacklistTime($gen, $sub);
  120.                 $iat = $contentProvider->getValue('iat',0);
  121.                 if ($iat <= $bl_time) {
  122.                         throw new OIDplusException(_L('The JWT token was blacklisted on %1. Please generate a new one',date('d F Y, H:i:s',$bl_time)));
  123.                 }
  124.         }
  125.  
  126.         // Content provider
  127.  
  128.         protected function getAuthContentStore() {
  129.                 static $contentProvider = null;
  130.  
  131.                 if (is_null($contentProvider)) {
  132.                         if (isset($_REQUEST['OIDPLUS_AUTH_JWT'])) {
  133.                                 $contentProvider = new OIDplusAuthContentStoreJWT();
  134.  
  135.                                 try {
  136.                                         // Decode the JWT. In this step, the signature as well as EXP/NBF times will be checked
  137.                                         $contentProvider->loadJWT($_REQUEST['OIDPLUS_AUTH_JWT']);
  138.  
  139.                                         // Do various checks if the token is allowed and not blacklisted
  140.                                         $this->jwtSecurityCheck($contentProvider);
  141.                                 } catch (Exception $e) {
  142.                                         throw new OIDplusException(_L('The JWT token was rejected: %1',$e->getMessage()));
  143.                                 }
  144.                         } else {
  145.                                 // Normal login via web-browser
  146.                                 $contentProvider = new OIDplusAuthContentStoreSession();
  147.                         }
  148.                 }
  149.  
  150.                 return $contentProvider;
  151.         }
  152.  
  153.         // RA authentication functions
  154.  
  155.         public function raLogin($email) {
  156.                 return $this->getAuthContentStore()->raLogin($email);
  157.         }
  158.  
  159.         public function raLogout($email) {
  160.                 return $this->getAuthContentStore()->raLogout($email);
  161.         }
  162.  
  163.         public function raCheckPassword($ra_email, $password) {
  164.                 $ra = new OIDplusRA($ra_email);
  165.  
  166.                 $authInfo = $ra->getAuthInfo();
  167.  
  168.                 $plugins = OIDplus::getAuthPlugins();
  169.                 if (count($plugins) == 0) {
  170.                         throw new OIDplusException(_L('No RA authentication plugins found'));
  171.                 }
  172.                 foreach ($plugins as $plugin) {
  173.                         if ($plugin->verify($authInfo, $password)) return true;
  174.                 }
  175.  
  176.                 return false;
  177.         }
  178.  
  179.         public function raNumLoggedIn() {
  180.                 return $this->getAuthContentStore()->raNumLoggedIn();
  181.         }
  182.  
  183.         public function raLogoutAll() {
  184.                 return $this->getAuthContentStore()->raLogoutAll();
  185.         }
  186.  
  187.         public function loggedInRaList() {
  188.                 if (OIDplus::authUtils()->forceAllLoggedOut()) {
  189.                         return array();
  190.                 } else {
  191.                         return $this->getAuthContentStore()->loggedInRaList();
  192.                 }
  193.         }
  194.  
  195.         public function isRaLoggedIn($email) {
  196.                 return $this->getAuthContentStore()->isRaLoggedIn($email);
  197.         }
  198.  
  199.         // Admin authentication functions
  200.  
  201.         public function adminLogin() {
  202.                 return $this->getAuthContentStore()->adminLogin();
  203.         }
  204.  
  205.         public function adminLogout() {
  206.                 return $this->getAuthContentStore()->adminLogout();
  207.         }
  208.  
  209.         public function adminCheckPassword($password) {
  210.                 $passwordData = OIDplus::baseConfig()->getValue('ADMIN_PASSWORD', '');
  211.                 if (empty($passwordData)) {
  212.                         throw new OIDplusException(_L('No admin password set in %1','userdata/baseconfig/config.inc.php'));
  213.                 }
  214.  
  215.                 if (strpos($passwordData, '$') !== false) {
  216.                         if ($passwordData[0] == '$') {
  217.                                 // Version 3: BCrypt
  218.                                 return password_verify($password, $passwordData);
  219.                         } else {
  220.                                 // Version 2: SHA3-512 with salt
  221.                                 list($s_salt, $hash) = explode('$', $passwordData, 2);
  222.                         }
  223.                 } else {
  224.                         // Version 1: SHA3-512 without salt
  225.                         $s_salt = '';
  226.                         $hash = $passwordData;
  227.                 }
  228.                 return strcmp(sha3_512($s_salt.$password, true), base64_decode($hash)) === 0;
  229.         }
  230.  
  231.         public function isAdminLoggedIn() {
  232.                 if (OIDplus::authUtils()->forceAllLoggedOut()) {
  233.                         return false;
  234.                 } else {
  235.                         return $this->getAuthContentStore()->isAdminLoggedIn();
  236.                 }
  237.         }
  238.  
  239.         // Authentication keys for validating arguments (e.g. sent by mail)
  240.  
  241.         public static function makeAuthKey($data) {
  242.                 $data = OIDplus::baseConfig()->getValue('SERVER_SECRET') . '/AUTHKEY/' . $data;
  243.                 $calc_authkey = sha3_512($data, false);
  244.                 return $calc_authkey;
  245.         }
  246.  
  247.         public static function validateAuthKey($data, $auth_key) {
  248.                 return strcmp(self::makeAuthKey($data), $auth_key) === 0;
  249.         }
  250.  
  251.         // "Veto" functions to force logout state
  252.  
  253.         public static function forceAllLoggedOut() {
  254.                 if (isset($_SERVER['SCRIPT_FILENAME']) && (basename($_SERVER['SCRIPT_FILENAME']) == 'sitemap.php')) {
  255.                         // The sitemap may not contain any confidential information,
  256.                         // even if the user is logged in, because the admin could
  257.                         // accidentally copy-paste the sitemap to a
  258.                         // search engine control panel while they are logged in
  259.                         return true;
  260.                 } else {
  261.                         return false;
  262.                 }
  263.         }
  264.  
  265.         // CSRF functions
  266.  
  267.         private $enable_csrf = true;
  268.  
  269.         public function enableCSRF() {
  270.                 $this->enable_csrf = true;
  271.         }
  272.  
  273.         public function disableCSRF() {
  274.                 $this->enable_csrf = false;
  275.         }
  276.  
  277.         public function genCSRFToken() {
  278.                 return bin2hex(self::getRandomBytes(64));
  279.         }
  280.  
  281.         public function checkCSRF() {
  282.                 if (!$this->enable_csrf) return;
  283.                 if (!isset($_REQUEST['csrf_token']) || !isset($_COOKIE['csrf_token']) || ($_REQUEST['csrf_token'] !== $_COOKIE['csrf_token'])) {
  284.                         throw new OIDplusException(_L('Wrong CSRF Token'));
  285.                 }
  286.         }
  287.  
  288.         // Generate RA passwords
  289.  
  290.         public static function raGeneratePassword($password): OIDplusRAAuthInfo {
  291.                 $def_method = OIDplus::config()->getValue('default_ra_auth_method');
  292.  
  293.                 $plugins = OIDplus::getAuthPlugins();
  294.                 foreach ($plugins as $plugin) {
  295.                         if (basename($plugin->getPluginDirectory()) === $def_method) {
  296.                                 return $plugin->generate($password);
  297.                         }
  298.                 }
  299.                 throw new OIDplusException(_L('Default RA auth method/plugin "%1" not found',$def_method));
  300.         }
  301.  
  302.         // Generate admin password
  303.  
  304.         /* Nothing here; the admin password will be generated in setup_base.js */
  305.  
  306. }
  307.