Subversion Repositories oidplus

Rev

Rev 1303 | Rev 1306 | 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. /**
  27.  * Auth content store for JWT tokens (web browser login cookies, Automated AJAX argument, or REST Bearer)
  28.  */
  29. class OIDplusAuthContentStoreJWT {
  30.  
  31.         /**
  32.          * Cookie name for the JWT auth token
  33.          */
  34.         const COOKIE_NAME = 'OIDPLUS_AUTH_JWT';
  35.  
  36.         /**
  37.          * Token generator; must be one of OIDplusAuthContentStoreJWT::JWT_GENERATOR_*
  38.          */
  39.         const CLAIM_GENERATOR = 'urn:oid:1.3.6.1.4.1.37476.2.5.2.7.1';
  40.  
  41.         /**
  42.          * List of logged-in users
  43.          */
  44.         const CLAIM_LOGIN_LIST = 'urn:oid:1.3.6.1.4.1.37476.2.5.2.7.2';
  45.  
  46.         /**
  47.          * SSH = Server Secret Hash
  48.          */
  49.         const CLAIM_SSH = 'urn:oid:1.3.6.1.4.1.37476.2.5.2.7.3';
  50.  
  51.         /**
  52.          * IP-Adress limit
  53.          */
  54.         const CLAIM_LIMIT_IP = 'urn:oid:1.3.6.1.4.1.37476.2.5.2.7.4';
  55.  
  56.         /**
  57.          * "Automated AJAX" plugin
  58.          */
  59.         const JWT_GENERATOR_AJAX   = 10;
  60.         /**
  61.          * "REST API" plugin
  62.          */
  63.         const JWT_GENERATOR_REST   = 20;
  64.         /**
  65.          * Web browser login method
  66.          */
  67.         const JWT_GENERATOR_LOGIN  = 40;
  68.         /**
  69.          * "Manually crafted" JWT tokens
  70.          */
  71.         const JWT_GENERATOR_MANUAL = 80;
  72.  
  73.         /**
  74.          * @param int $gen OIDplusAuthContentStoreJWT::JWT_GENERATOR_...
  75.          * @param string $sub
  76.          * @return string
  77.          */
  78.         private static function jwtGetBlacklistConfigKey(int $gen, string $sub): string {
  79.                 // Note: Needs to be <= 50 characters! If $gen is 2 chars, then the config key is 49 chars long
  80.                 return 'jwt_blacklist_gen('.$gen.')_sub('.trim(base64_encode(md5($sub,true)),'=').')';
  81.         }
  82.  
  83.         /**
  84.          * @param int $gen
  85.          */
  86.         private static function generatorName($gen) {
  87.                 // Note: The strings are not translated, because the name is used in config keys or logs
  88.                 if ($gen === self::JWT_GENERATOR_AJAX)   return 'Automated AJAX calls';
  89.                 if ($gen === self::JWT_GENERATOR_REST)   return 'REST API';
  90.                 if ($gen === self::JWT_GENERATOR_LOGIN)  return 'Browser login';
  91.                 if ($gen === self::JWT_GENERATOR_MANUAL) return 'Manually created';
  92.                 return 'Unknown generator';
  93.         }
  94.  
  95.         /**
  96.          * @param int $gen OIDplusAuthContentStoreJWT::JWT_GENERATOR_...
  97.          * @param string $sub
  98.          * @return void
  99.          * @throws OIDplusException
  100.          */
  101.         public static function jwtBlacklist(int $gen, string $sub) {
  102.                 $cfg = self::jwtGetBlacklistConfigKey($gen, $sub);
  103.                 $bl_time = time()-1;
  104.  
  105.                 $gen_desc = self::generatorName($gen);
  106.  
  107.                 OIDplus::config()->prepareConfigKey($cfg, "Revoke timestamp of all JWT tokens for $sub with generator $gen ($gen_desc)", "$bl_time", OIDplusConfig::PROTECTION_HIDDEN, function($value) {});
  108.                 OIDplus::config()->setValue($cfg, $bl_time);
  109.         }
  110.  
  111.         /**
  112.          * @param int $gen OIDplusAuthContentStoreJWT::JWT_GENERATOR_...
  113.          * @param string $sub
  114.          * @return int
  115.          * @throws OIDplusException
  116.          */
  117.         public static function jwtGetBlacklistTime(int $gen, string $sub): int {
  118.                 $cfg = self::jwtGetBlacklistConfigKey($gen, $sub);
  119.                 return (int)OIDplus::config()->getValue($cfg,0);
  120.         }
  121.  
  122.         /**
  123.          * We include a hash of the server-secret here (ssh = server-secret-hash), so that the JWT can be invalidated by changing the server-secret
  124.          * @return string
  125.          * @throws OIDplusException
  126.          */
  127.         private static function getSsh(): string {
  128.                 $hexadecimal_string = OIDplus::authUtils()->makeSecret(['bb1aebd6-fe6a-11ed-a553-3c4a92df8582']);
  129.                 return base64_encode(pack('H*',$hexadecimal_string));
  130.         }
  131.  
  132.         /**
  133.          * Do various checks if the token is allowed and not blacklisted
  134.          * @param OIDplusAuthContentStoreJWT $contentProvider
  135.          * @param int|null $validGenerators Bitmask which generators to allow (null = allow all)
  136.          * @return void
  137.          * @throws OIDplusException
  138.          */
  139.         private static function jwtSecurityCheck(OIDplusAuthContentStoreJWT $contentProvider, int $validGenerators=null) {
  140.                 // Check if the token is intended for us
  141.                 if ($contentProvider->getValue('aud','') !== OIDplus::getEditionInfo()['jwtaud']) {
  142.                         throw new OIDplusException(_L('Token has wrong audience'));
  143.                 }
  144.  
  145.                 if ($contentProvider->getValue(self::CLAIM_SSH, '') !== self::getSsh()) {
  146.                         throw new OIDplusException(_L('"Server Secret" was changed; therefore the JWT is not valid anymore'));
  147.                 }
  148.  
  149.                 $gen = $contentProvider->getValue(self::CLAIM_GENERATOR, -1);
  150.  
  151.                 $has_admin = $contentProvider->isAdminLoggedIn();
  152.                 $has_ra = $contentProvider->raNumLoggedIn() > 0;
  153.  
  154.                 // Check if the token generator is allowed
  155.                 if ($gen === self::JWT_GENERATOR_AJAX) {
  156.                         if (($has_admin) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_AJAX_ADMIN', true)) {
  157.                                 // Generator: plugins/viathinksoft/adminPages/910_automated_ajax_calls/OIDplusPageAdminAutomatedAJAXCalls.class.php
  158.                                 throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_AJAX_ADMIN'));
  159.                         }
  160.                         if (($has_ra) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_AJAX_USER', true)) {
  161.                                 // Generator: plugins/viathinksoft/raPages/910_automated_ajax_calls/OIDplusPageRaAutomatedAJAXCalls.class.php
  162.                                 throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_AJAX_USER'));
  163.                         }
  164.                 }
  165.                 else if ($gen === self::JWT_GENERATOR_REST) {
  166.                         if (($has_admin) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_REST_ADMIN', true)) {
  167.                                 // Generator: plugins/viathinksoft/adminPages/911_rest_api/OIDplusPageAdminRestApi.class.php
  168.                                 throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_REST_ADMIN'));
  169.                         }
  170.                         if (($has_ra) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_REST_USER', true)) {
  171.                                 // Generator: plugins/viathinksoft/raPages/911_rest_api/OIDplusPageRaRestApi.class.php
  172.                                 throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_REST_USER'));
  173.                         }
  174.                 }
  175.                 else if ($gen === self::JWT_GENERATOR_LOGIN) {
  176.                         // Used for web browser login (use JWT token in a cookie as alternative to PHP session):
  177.                         // - No PHP session will be used
  178.                         // - Session will not be bound to IP address (therefore, you can switch between mobile/WiFi for example)
  179.                         // - No server-side session needed
  180.                         if (($has_admin) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) {
  181.                                 throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_ADMIN'));
  182.                         }
  183.                         if (($has_ra) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) {
  184.                                 throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_USER'));
  185.                         }
  186.                 }
  187.                 else if ($gen === self::JWT_GENERATOR_MANUAL) {
  188.                         // Generator: "hand-crafted" tokens
  189.                         if (($has_admin) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_MANUAL_ADMIN', false)) {
  190.                                 throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_MANUAL_ADMIN'));
  191.                         }
  192.                         if (($has_ra) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_MANUAL_USER', false)) {
  193.                                 throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_MANUAL_USER'));
  194.                         }
  195.                 } else {
  196.                         throw new OIDplusException(_L('Token generator %1 not recognized',$gen));
  197.                 }
  198.  
  199.                 // Make sure that the IAT (issued at time) isn't in a blacklisted timeframe
  200.                 // When an user believes that a token was compromised, then they can blacklist the tokens identified by their "iat" ("Issued at") property
  201.                 // When a user logs out of a web browser session, the JWT token will be blacklisted as well
  202.                 // Small side effect: All web browser login sessions of that user will be revoked then
  203.                 $iat = $contentProvider->getValue('iat',0);
  204.                 if (($iat-120/*leeway 2min*/) > time()) {
  205.                         // Token was created in the future. Something is wrong!
  206.                         throw new OIDplusException(_L('JWT Token cannot be verified because the server time is wrong'));
  207.                 }
  208.                 $sublist = $contentProvider->loggedInRaList();
  209.                 $usernames = array();
  210.                 foreach ($sublist as $sub) {
  211.                         $usernames[] = $sub->raEmail();
  212.                 }
  213.                 if ($has_admin) $usernames[] = 'admin';
  214.                 foreach ($usernames as $username) {
  215.                         $bl_time = self::jwtGetBlacklistTime($gen, $username);
  216.                         if ($iat <= $bl_time) {
  217.                                 // Token is blacklisted (it was created before the last blacklist time)
  218.                                 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)));
  219.                         }
  220.                 }
  221.  
  222.                 // Optional feature: Limit the JWT to a specific IP address
  223.                 // Currently not used in OIDplus
  224.                 $ip = $contentProvider->getValue(self::CLAIM_LIMIT_IP, null);
  225.                 if (!is_null($ip)) {
  226.                         if (isset($_SERVER['REMOTE_ADDR']) && ($ip !== $_SERVER['REMOTE_ADDR'])) {
  227.                                 throw new OIDplusException(_L('Your IP address is not allowed to use this token'));
  228.                         }
  229.                 }
  230.  
  231.                 // Checks if JWT are dependent on the generator
  232.                 if (!is_null($validGenerators)) {
  233.                         if (($gen & $validGenerators) === 0) {
  234.                                 throw new OIDplusException(_L('This kind of JWT token (%1) cannot be used in this request type', self::generatorName($gen)));
  235.                         }
  236.                 }
  237.         }
  238.  
  239.         // Override abstract functions
  240.  
  241.         /**
  242.          * @var array
  243.          */
  244.         protected $content = array();
  245.  
  246.         /**
  247.          * @param string $name
  248.          * @param mixed|null $default
  249.          * @return mixed|null
  250.          */
  251.         public function getValue(string $name, $default = NULL) {
  252.                 return $this->content[$name] ?? $default;
  253.         }
  254.  
  255.         /**
  256.          * @param string $name
  257.          * @param mixed $value
  258.          * @return void
  259.          */
  260.         public function setValue(string $name, $value) {
  261.                 $this->content[$name] = $value;
  262.         }
  263.  
  264.         /**
  265.          * @param string $name
  266.          * @return bool
  267.          */
  268.         public function exists(string $name): bool {
  269.                 return isset($this->content[$name]);
  270.         }
  271.  
  272.         /**
  273.          * @param string $name
  274.          * @return void
  275.          */
  276.         public function delete(string $name) {
  277.                 unset($this->content[$name]);
  278.         }
  279.  
  280.         /**
  281.          * @return void
  282.          */
  283.         public function activate() {
  284.                 // Send cookie at the end of the HTTP request, in case there are multiple activate() calls
  285.                 OIDplus::register_shutdown_function(array($this,'activateNow'));
  286.         }
  287.  
  288.         /**
  289.          * @return void
  290.          * @throws OIDplusException
  291.          */
  292.         public function activateNow() {
  293.                 $token = $this->getJWTToken();
  294.                 $exp = $this->getValue('exp',0);
  295.                 OIDplus::cookieUtils()->setcookie(self::COOKIE_NAME, $token, $exp, false);
  296.         }
  297.  
  298.         /**
  299.          * @return void
  300.          * @throws OIDplusException
  301.          */
  302.         public function destroySession() {
  303.                 OIDplus::cookieUtils()->unsetcookie(self::COOKIE_NAME);
  304.         }
  305.  
  306.         // RA authentication functions (low-level)
  307.  
  308.         /**
  309.          * @param string $email
  310.          * @return void
  311.          */
  312.         public function raLogin(string $email) {
  313.                 if ($email == 'admin') return;
  314.  
  315.                 $list = $this->getValue(self::CLAIM_LOGIN_LIST, null);
  316.                 if (is_null($list)) $list = [];
  317.                 if (!in_array($email, $list)) $list[] = $email;
  318.                 $this->setValue(self::CLAIM_LOGIN_LIST, $list);
  319.         }
  320.  
  321.         /**
  322.          * @return int
  323.          */
  324.         public function raNumLoggedIn(): int {
  325.                 return count($this->loggedInRaList());
  326.         }
  327.  
  328.         /**
  329.          * @return OIDplusRA[]
  330.          */
  331.         public function loggedInRaList(): array {
  332.                 $list = $this->getValue(self::CLAIM_LOGIN_LIST, null);
  333.                 if (is_null($list)) $list = [];
  334.  
  335.                 $res = array();
  336.                 foreach (array_unique($list) as $username) {
  337.                         if ($username == '') continue; // should not happen
  338.                         if ($username == 'admin') continue;
  339.                         $res[] = new OIDplusRA($username);
  340.                 }
  341.                 return $res;
  342.         }
  343.  
  344.         /**
  345.          * @param string $email
  346.          * @return bool
  347.          */
  348.         public function isRaLoggedIn(string $email): bool {
  349.                 foreach ($this->loggedInRaList() as $ra) {
  350.                         if ($email == $ra->raEmail()) return true;
  351.                 }
  352.                 return false;
  353.         }
  354.  
  355.         /**
  356.          * @param string $email
  357.          * @return void
  358.          * @throws OIDplusException
  359.          */
  360.         public function raLogout(string $email) {
  361.                 if ($email == 'admin') return;
  362.  
  363.                 $gen = $this->getValue(self::CLAIM_GENERATOR, -1);
  364.                 if ($gen >= 0) self::jwtBlacklist($gen, $email);
  365.  
  366.                 $list = $this->getValue(self::CLAIM_LOGIN_LIST, null);
  367.                 if (is_null($list)) $list = [];
  368.                 $key = array_search($email, $list);
  369.                 if ($key !== false) unset($list[$key]);
  370.                 $this->setValue(self::CLAIM_LOGIN_LIST, $list);
  371.         }
  372.  
  373.         /**
  374.          * @param string $email
  375.          * @param string $loginfo
  376.          * @return void
  377.          * @throws OIDplusException
  378.          */
  379.         public function raLogoutEx(string $email, string &$loginfo) {
  380.                 $this->raLogout($email);
  381.                 $loginfo = 'from JWT session';
  382.         }
  383.  
  384.         // Admin authentication functions (low-level)
  385.  
  386.         /**
  387.          * @return void
  388.          */
  389.         public function adminLogin() {
  390.                 $list = $this->getValue(self::CLAIM_LOGIN_LIST, null);
  391.                 if (is_null($list)) $list = [];
  392.                 if (!in_array('admin', $list)) $list[] = 'admin';
  393.                 $this->setValue(self::CLAIM_LOGIN_LIST, $list);
  394.         }
  395.  
  396.         /**
  397.          * @return bool
  398.          */
  399.         public function isAdminLoggedIn(): bool {
  400.                 $list = $this->getValue(self::CLAIM_LOGIN_LIST, null);
  401.                 if (is_null($list)) $list = [];
  402.                 return in_array('admin', $list);
  403.         }
  404.  
  405.         /**
  406.          * @return void
  407.          * @throws OIDplusException
  408.          */
  409.         public function adminLogout() {
  410.                 $gen = $this->getValue(self::CLAIM_GENERATOR, -1);
  411.                 if ($gen >= 0) self::jwtBlacklist($gen, 'admin');
  412.  
  413.                 $list = $this->getValue(self::CLAIM_LOGIN_LIST, null);
  414.                 if (is_null($list)) $list = [];
  415.                 $key = array_search('admin', $list);
  416.                 if ($key !== false) unset($list[$key]);
  417.                 $this->setValue(self::CLAIM_LOGIN_LIST, $list);
  418.         }
  419.  
  420.         /**
  421.          * @param string $loginfo
  422.          * @return void
  423.          * @throws OIDplusException
  424.          */
  425.         public function adminLogoutEx(string &$loginfo) {
  426.                 $this->adminLogout();
  427.                 $loginfo = 'from JWT session';
  428.         }
  429.  
  430.         private static $contentProvider = null;
  431.  
  432.         /**
  433.          * @return OIDplusAuthContentStoreJWT|null
  434.          * @throws OIDplusException
  435.          */
  436.         public static function getActiveProvider()/*: ?OIDplusAuthContentStoreJWT*/ {
  437.                 if (!self::$contentProvider) {
  438.  
  439.                         $tmp = null;
  440.                         $silent_error = false;
  441.  
  442.                         try {
  443.  
  444.                                 $rel_url = substr($_SERVER['REQUEST_URI'], strlen(OIDplus::webpath(null, OIDplus::PATH_RELATIVE_TO_ROOT)));
  445.                                 if (str_starts_with($rel_url, 'rest/')) { // <== TODO: Find a way how to move this into the plugin, since REST does not belong to the core.
  446.  
  447.                                         // REST may only use Bearer Authentication
  448.                                         $bearer = getBearerToken();
  449.                                         if (!is_null($bearer)) {
  450.                                                 $silent_error = false;
  451.                                                 $tmp = new OIDplusAuthContentStoreJWT();
  452.                                                 $tmp->loadJWT($bearer);
  453.                                                 self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_REST | self::JWT_GENERATOR_MANUAL);
  454.                                         }
  455.  
  456.                                 } else {
  457.  
  458.                                         // A web-visitor (HTML and AJAX, but not REST) can use a JWT Cookie
  459.                                         if (isset($_COOKIE[self::COOKIE_NAME])) {
  460.                                                 $silent_error = true;
  461.                                                 $tmp = new OIDplusAuthContentStoreJWT();
  462.                                                 $tmp->loadJWT($_COOKIE[self::COOKIE_NAME]);
  463.                                                 self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_LOGIN | self::JWT_GENERATOR_MANUAL);
  464.                                         }
  465.  
  466.                                         // AJAX may additionally use GET/POST automated AJAX (in addition to the normal web browser login Cookie)
  467.                                         if (isset($_SERVER['SCRIPT_FILENAME']) && (strtolower(basename($_SERVER['SCRIPT_FILENAME'])) !== 'ajax.php')) {
  468.                                                 if (isset($_POST[self::COOKIE_NAME])) {
  469.                                                         $silent_error = false;
  470.                                                         $tmp = new OIDplusAuthContentStoreJWT();
  471.                                                         $tmp->loadJWT($_POST[self::COOKIE_NAME]);
  472.                                                         self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_AJAX | self::JWT_GENERATOR_MANUAL);
  473.                                                 }
  474.                                                 if (isset($_GET[self::COOKIE_NAME])) {
  475.                                                         $silent_error = false;
  476.                                                         $tmp = new OIDplusAuthContentStoreJWT();
  477.                                                         $tmp->loadJWT($_GET[self::COOKIE_NAME]);
  478.                                                         self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_AJAX | self::JWT_GENERATOR_MANUAL);
  479.                                                 }
  480.                                         }
  481.  
  482.                                 }
  483.  
  484.                         } catch (\Exception $e) {
  485.                                 if (!$silent_error) {
  486.                                         // Most likely an AJAX request. We can throw an Exception
  487.                                         throw new OIDplusException(_L('The JWT token was rejected: %1',$e->getMessage()));
  488.                                 } else {
  489.                                         // Most likely an expired Cookie/Login session. We must not throw an Exception, otherwise we will break jsTree
  490.                                         OIDplus::cookieUtils()->unsetcookie(self::COOKIE_NAME);
  491.                                         return null;
  492.                                 }
  493.                         }
  494.  
  495.                         self::$contentProvider = $tmp;
  496.                 }
  497.  
  498.                 return self::$contentProvider;
  499.         }
  500.  
  501.         /**
  502.          * @param string $email
  503.          * @param string $loginfo
  504.          * @return void
  505.          * @throws OIDplusException
  506.          */
  507.         public function raLoginEx(string $email, string &$loginfo) {
  508.                 if (is_null(self::getActiveProvider())) {
  509.                         $this->raLogin($email);
  510.                         $loginfo = 'into new JWT session';
  511.                         self::$contentProvider = $this;
  512.                 } else {
  513.                         $gen = $this->getValue(self::CLAIM_GENERATOR,-1);
  514.                         switch ($gen) {
  515.                                 case OIDplusAuthContentStoreJWT::JWT_GENERATOR_AJAX :
  516.                                 case OIDplusAuthContentStoreJWT::JWT_GENERATOR_REST :
  517.                                 case OIDplusAuthContentStoreJWT::JWT_GENERATOR_MANUAL :
  518.                                         throw new OIDplusException(_L('This kind of JWT token cannot be altered. Therefore you cannot do this action.'));
  519.                                 case OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN :
  520.                                         if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) {
  521.                                                 throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_USER'));
  522.                                         }
  523.                                         break;
  524.                                 default:
  525.                                         assert(false); // This cannot happen because jwtSecurityCheck will check for unknown generators
  526.                                         break;
  527.                         }
  528.                         $this->raLogin($email);
  529.                         $loginfo = 'into existing JWT session';
  530.                 }
  531.         }
  532.  
  533.         /**
  534.          * @param string $loginfo
  535.          * @return void
  536.          * @throws OIDplusException
  537.          */
  538.         public function adminLoginEx(string &$loginfo) {
  539.                 if (is_null(self::getActiveProvider())) {
  540.                         $this->adminLogin();
  541.                         $loginfo = 'into new JWT session';
  542.                         self::$contentProvider = $this;
  543.                 } else {
  544.                         $gen = $this->getValue(self::CLAIM_GENERATOR,-1);
  545.                         switch ($gen) {
  546.                                 case OIDplusAuthContentStoreJWT::JWT_GENERATOR_AJAX :
  547.                                 case OIDplusAuthContentStoreJWT::JWT_GENERATOR_REST :
  548.                                 case OIDplusAuthContentStoreJWT::JWT_GENERATOR_MANUAL :
  549.                                         throw new OIDplusException(_L('This kind of JWT token cannot be altered. Therefore you cannot do this action.'));
  550.                                 case OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN :
  551.                                         if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) {
  552.                                                 throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_ADMIN'));
  553.                                         }
  554.                                         break;
  555.                                 default:
  556.                                         assert(false); // This cannot happen because jwtSecurityCheck will check for unknown generators
  557.                                         break;
  558.                         }
  559.                         $this->adminLogin();
  560.                         $loginfo = 'into existing JWT session';
  561.                 }
  562.         }
  563.  
  564.         // Individual functions
  565.  
  566.         /**
  567.          * Decode the JWT. In this step, the signature as well as EXP/NBF times will be checked
  568.          * @param string $jwt
  569.          * @return void
  570.          * @throws OIDplusException
  571.          */
  572.         public function loadJWT(string $jwt) {
  573.                 \Firebase\JWT\JWT::$leeway = 60; // leeway in seconds
  574.                 if (OIDplus::getPkiStatus()) {
  575.                         $pubKey = OIDplus::getSystemPublicKey();
  576.                         $k = new \Firebase\JWT\Key($pubKey, 'RS256'); // RSA+SHA256 is hardcoded in getPkiStatus() generation
  577.                         $this->content = (array) \Firebase\JWT\JWT::decode($jwt, $k);
  578.                 } else {
  579.                         $key = OIDplus::authUtils()->makeSecret(['0be35e52-f4ef-11ed-b67e-3c4a92df8582']);
  580.                         $key = hash_pbkdf2('sha512', $key, '', 10000, 32/*256bit*/, false);
  581.                         $k = new \Firebase\JWT\Key($key, 'HS512'); // HMAC+SHA512 is hardcoded here
  582.                         $this->content = (array) \Firebase\JWT\JWT::decode($jwt, $k);
  583.                 }
  584.         }
  585.  
  586.         /**
  587.          * @return string
  588.          * @throws OIDplusException
  589.          */
  590.         public function getJWTToken(): string {
  591.                 $payload = $this->content;
  592.                 $payload[self::CLAIM_SSH] = self::getSsh(); // SSH = Server Secret Hash
  593.                 $payload["iss"] = OIDplus::getEditionInfo()['jwtaud'];
  594.                 $payload["aud"] = OIDplus::getEditionInfo()['jwtaud'];
  595.                 $payload["jti"] = gen_uuid();
  596.                 $payload["iat"] = time();
  597.  
  598.                 if (OIDplus::getPkiStatus()) {
  599.                         $privKey = OIDplus::getSystemPrivateKey();
  600.                         return \Firebase\JWT\JWT::encode($payload, $privKey, 'RS256'); // RSA+SHA256 is hardcoded in getPkiStatus() generation
  601.                 } else {
  602.                         $key = OIDplus::authUtils()->makeSecret(['0be35e52-f4ef-11ed-b67e-3c4a92df8582']);
  603.                         $key = hash_pbkdf2('sha512', $key, '', 10000, 32/*256bit*/, false);
  604.                         return \Firebase\JWT\JWT::encode($payload, $key, 'HS512'); // HMAC+SHA512 is hardcoded here
  605.                 }
  606.         }
  607.  
  608. }
  609.