Subversion Repositories oidplus

Rev

Rev 578 | Rev 583 | 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.                         // Used for feature "stay logged in" (use JWT token in a cookie as alternative to PHP session):
  101.                         // - No PHP session will be used
  102.                         // - Session will not be bound to IP address (therefore, you can switch between mobile/WiFi for example)
  103.                         // - No server-side session needed
  104.                         if (($sub === 'admin') && !OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) {
  105.                                 throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_ADMIN'));
  106.                         }
  107.                         else if (($sub !== 'admin') && !OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) {
  108.                                 throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_USER'));
  109.                         }
  110.                 }
  111.                 else if ($gen === self::JWT_GENERATOR_MANUAL) {
  112.                         // Generator 2 are "hand-crafted" tokens
  113.                         if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_MANUAL', true)) {
  114.                                 throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_MANUAL'));
  115.                         }
  116.                 } else {
  117.                         throw new OIDplusException(_L('Token generator %1 not recognized',$gen));
  118.                 }
  119.  
  120.                 // Make sure that the IAT (issued at time) isn't in a blacklisted timeframe
  121.                 // When an user believes that a token was compromised, then they can blacklist the tokens identified by their "iat" ("Issued at") property
  122.                 $bl_time = $this->jwtGetBlacklistTime($gen, $sub);
  123.                 $iat = $contentProvider->getValue('iat',0);
  124.                 if ($iat <= $bl_time) {
  125.                         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)));
  126.                 }
  127.  
  128.                 // Optional feature: Limit the JWT to a specific IP address
  129.                 // This could become handy if JWTs are used instead of Login sessions,
  130.                 // and you want to avoid session/JWT hijacking
  131.                 $ip = $contentProvider->getValue('ip','');
  132.                 if ($ip !== '') {
  133.                         if (isset($_SERVER['REMOTE_ADDR']) && ($ip !== $_SERVER['REMOTE_ADDR'])) {
  134.                                 throw new OIDplusException(_L('Your IP address is not allowed to use this token'));
  135.                         }
  136.                 }
  137.  
  138.                 // Checks which are dependent on the generator
  139.                 if ($gen === self::JWT_GENERATOR_LOGIN) {
  140.                         if (!isset($_COOKIE['OIDPLUS_AUTH_JWT'])) {
  141.                                 throw new OIDplusException(_L('This kind of JWT token can only be used with the %1 request type','COOKIE'));
  142.                         }
  143.                 }
  144.                 if ($gen === self::JWT_GENERATOR_AJAX) {
  145.                         if (!isset($_GET['OIDPLUS_AUTH_JWT']) && !isset($_POST['OIDPLUS_AUTH_JWT'])) {
  146.                                 throw new OIDplusException(_L('This kind of JWT token can only be used with the %1 request type','GET/POST'));
  147.                         }
  148.                         if (isset($_SERVER['SCRIPT_FILENAME']) && (strtolower(basename($_SERVER['SCRIPT_FILENAME'])) !== 'ajax.php')) {
  149.                                 throw new OIDplusException(_L('This kind of JWT token can only be used in ajax.php'));
  150.                         }
  151.                 }
  152.         }
  153.  
  154.         // Content provider
  155.  
  156.         protected function getAuthContentStore() {
  157.                 static $contentProvider = null;
  158.  
  159.                 if (is_null($contentProvider)) {
  160.                         $jwt = '';
  161.                         if (isset($_COOKIE['OIDPLUS_AUTH_JWT'])) $jwt = $_COOKIE['OIDPLUS_AUTH_JWT'];
  162.                         if (isset($_POST['OIDPLUS_AUTH_JWT']))   $jwt = $_POST['OIDPLUS_AUTH_JWT'];
  163.                         if (isset($_GET['OIDPLUS_AUTH_JWT']))    $jwt = $_GET['OIDPLUS_AUTH_JWT'];
  164.  
  165.                         if (!empty($jwt)) {
  166.                                 $contentProvider = new OIDplusAuthContentStoreJWT();
  167.  
  168.                                 try {
  169.                                         // Decode the JWT. In this step, the signature as well as EXP/NBF times will be checked
  170.                                         $contentProvider->loadJWT($jwt);
  171.  
  172.                                         // Do various checks if the token is allowed and not blacklisted
  173.                                         $this->jwtSecurityCheck($contentProvider);
  174.                                 } catch (Exception $e) {
  175.                                         if (isset($_GET['OIDPLUS_AUTH_JWT']) || isset($_POST['OIDPLUS_AUTH_JWT'])) {
  176.                                                 // Most likely an AJAX request. We can throw an Exception
  177.                                                 $contentProvider = null;
  178.                                                 throw new OIDplusException(_L('The JWT token was rejected: %1',$e->getMessage()));
  179.                                         } else {
  180.                                                 // Most likely an expired Cookie/Login session. We must not throw an Exception, otherwise we will break jsTree
  181.                                                 $contentProvider = new OIDplusAuthContentStoreSession();
  182.                                                 OIDplus::cookieUtils()->unsetcookie('OIDPLUS_AUTH_JWT');
  183.                                         }
  184.                                 }
  185.                         } else {
  186.                                 // Normal login via web-browser
  187.                                 $contentProvider = new OIDplusAuthContentStoreSession();
  188.                         }
  189.                 }
  190.  
  191.                 return $contentProvider;
  192.         }
  193.  
  194.         // RA authentication functions
  195.  
  196.         public function raLogin($email) {
  197.                 return $this->getAuthContentStore()->raLogin($email);
  198.         }
  199.  
  200.         public function raLogout($email) {
  201.                 return $this->getAuthContentStore()->raLogout($email);
  202.         }
  203.  
  204.         public function raCheckPassword($ra_email, $password) {
  205.                 $ra = new OIDplusRA($ra_email);
  206.  
  207.                 $authInfo = $ra->getAuthInfo();
  208.  
  209.                 $plugins = OIDplus::getAuthPlugins();
  210.                 if (count($plugins) == 0) {
  211.                         throw new OIDplusException(_L('No RA authentication plugins found'));
  212.                 }
  213.                 foreach ($plugins as $plugin) {
  214.                         if ($plugin->verify($authInfo, $password)) return true;
  215.                 }
  216.  
  217.                 return false;
  218.         }
  219.  
  220.         public function raNumLoggedIn() {
  221.                 return $this->getAuthContentStore()->raNumLoggedIn();
  222.         }
  223.  
  224.         public function raLogoutAll() {
  225.                 return $this->getAuthContentStore()->raLogoutAll();
  226.         }
  227.  
  228.         public function loggedInRaList() {
  229.                 if (OIDplus::authUtils()->forceAllLoggedOut()) {
  230.                         return array();
  231.                 } else {
  232.                                 return $this->getAuthContentStore()->loggedInRaList();
  233.                 }
  234.         }
  235.  
  236.         public function isRaLoggedIn($email) {
  237.                         return $this->getAuthContentStore()->isRaLoggedIn($email);
  238.         }
  239.  
  240.         // Admin authentication functions
  241.  
  242.         public function adminLogin() {
  243.                 return $this->getAuthContentStore()->adminLogin();
  244.         }
  245.  
  246.         public function adminLogout() {
  247.                 return $this->getAuthContentStore()->adminLogout();
  248.         }
  249.  
  250.         public function adminCheckPassword($password) {
  251.                 $passwordData = OIDplus::baseConfig()->getValue('ADMIN_PASSWORD', '');
  252.                 if (empty($passwordData)) {
  253.                         throw new OIDplusException(_L('No admin password set in %1','userdata/baseconfig/config.inc.php'));
  254.                 }
  255.  
  256.                 if (strpos($passwordData, '$') !== false) {
  257.                         if ($passwordData[0] == '$') {
  258.                                 // Version 3: BCrypt
  259.                                 return password_verify($password, $passwordData);
  260.                         } else {
  261.                                 // Version 2: SHA3-512 with salt
  262.                                 list($s_salt, $hash) = explode('$', $passwordData, 2);
  263.                         }
  264.                 } else {
  265.                         // Version 1: SHA3-512 without salt
  266.                         $s_salt = '';
  267.                         $hash = $passwordData;
  268.                 }
  269.                 return strcmp(sha3_512($s_salt.$password, true), base64_decode($hash)) === 0;
  270.         }
  271.  
  272.         public function isAdminLoggedIn() {
  273.                 if (OIDplus::authUtils()->forceAllLoggedOut()) {
  274.                         return false;
  275.                 } else {
  276.                                 return $this->getAuthContentStore()->isAdminLoggedIn();
  277.                 }
  278.         }
  279.  
  280.         // Authentication keys for validating arguments (e.g. sent by mail)
  281.  
  282.         public static function makeAuthKey($data) {
  283.                 $data = OIDplus::baseConfig()->getValue('SERVER_SECRET') . '/AUTHKEY/' . $data;
  284.                 $calc_authkey = sha3_512($data, false);
  285.                 return $calc_authkey;
  286.         }
  287.  
  288.         public static function validateAuthKey($data, $auth_key) {
  289.                 return strcmp(self::makeAuthKey($data), $auth_key) === 0;
  290.         }
  291.  
  292.         // "Veto" functions to force logout state
  293.  
  294.         public static function forceAllLoggedOut() {
  295.                 if (isset($_SERVER['SCRIPT_FILENAME']) && (basename($_SERVER['SCRIPT_FILENAME']) == 'sitemap.php')) {
  296.                         // The sitemap may not contain any confidential information,
  297.                         // even if the user is logged in, because the admin could
  298.                         // accidentally copy-paste the sitemap to a
  299.                         // search engine control panel while they are logged in
  300.                         return true;
  301.                 } else {
  302.                         return false;
  303.                 }
  304.         }
  305.  
  306.         // CSRF functions
  307.  
  308.         private $enable_csrf = true;
  309.  
  310.         public function enableCSRF() {
  311.                 $this->enable_csrf = true;
  312.         }
  313.  
  314.         public function disableCSRF() {
  315.                 $this->enable_csrf = false;
  316.         }
  317.  
  318.         public function genCSRFToken() {
  319.                 return bin2hex(self::getRandomBytes(64));
  320.         }
  321.  
  322.         public function checkCSRF() {
  323.                 if (!$this->enable_csrf) return;
  324.                 if (!isset($_REQUEST['csrf_token']) || !isset($_COOKIE['csrf_token']) || ($_REQUEST['csrf_token'] !== $_COOKIE['csrf_token'])) {
  325.                         throw new OIDplusException(_L('Wrong CSRF Token'));
  326.                 }
  327.         }
  328.  
  329.         // Generate RA passwords
  330.  
  331.         public static function raGeneratePassword($password): OIDplusRAAuthInfo {
  332.                 $def_method = OIDplus::config()->getValue('default_ra_auth_method');
  333.  
  334.                 $plugins = OIDplus::getAuthPlugins();
  335.                 foreach ($plugins as $plugin) {
  336.                         if (basename($plugin->getPluginDirectory()) === $def_method) {
  337.                                 return $plugin->generate($password);
  338.                         }
  339.                 }
  340.                 throw new OIDplusException(_L('Default RA auth method/plugin "%1" not found',$def_method));
  341.         }
  342.  
  343.         // Generate admin password
  344.  
  345.         /* Nothing here; the admin password will be generated in setup_base.js */
  346.  
  347. }
  348.