Subversion Repositories oidplus

Rev

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