chimera-mark2-core-release/core/fox/authToken.php

264 lines
8.5 KiB
PHP

<?php
namespace fox;
/**
*
* Class authToken
*
* @copyright MX STAR LLC 2018-2022
* @version 4.0.0
* @author Pavel Dmitriev
* @license GPLv3
*
* @property-read $id
* @property-read $token
* @property-read $user
*
**/
class authToken extends baseClass
{
protected $id;
protected $sessionId;
protected $token1;
protected $token2;
protected $token2b;
public $userId;
public time $issueStamp;
// Issue stamp of token
public time $renewStamp;
// renew expiration stamp
public time $expireStamp;
// token expiration stamp
protected ?user $__user = null;
protected $renewed=false;
/*
* Token type - defines usage area
* "API" = REST API
* "WEB" = WEB UI
* "APP" = Mobile Application
*
* defaultTTL = days,
* renewTTL = minutes,
* after renewTTL and before defaultTTL token may be renewed. if allowRenew then default TTL restarted at any renew request.
*/
public const tokenTypes = [
"API" => [
"name" => "REST API",
"defaultTTL" => 10,
"allowLogin" => false,
"allowRenew" => true,
"renewTTL" => 30
],
"WEB" => [
"name" => "WWW",
"defaultTTL" => 32,
"allowLogin" => true,
"allowRenew" => true,
"renewTTL" => 30
],
"APP" => [
"name" => "Mobile APP",
"defaultTTL" => 10,
"allowLogin" => true,
"allowRenew" => true,
"renewTTL" => 30
]
];
public string $type = "API";
public static $allowDeleteFromDB = true;
public static $sqlTable = "tblAuthTokens";
public static $sqlColumns = [
"userId" => [
"type" => "INT",
"index" => "INDEX",
"nullable" => false
],
"sessionId"=>[
"type"=>"CHAR(51)",
"index"=>"UNIQUE",
"nullable"=>true,
],
"token1" => [
"type" => "CHAR(64)",
"index" => "UNIQUE",
"nullable" => true
],
"token2" => [
"type" => "CHAR(64)",
"nullable" => false
],
"token2b" => [
"type" => "CHAR(64)",
"nullable" => true
],
"renewed" => [
"type"=>"SKIP",
]
];
public function __xConstruct()
{
$this->issueStamp = new time(time());
$this->expireStamp = new time();
$this->renewStamp = new time();
}
public static function dropExpired()
{
$sql = sql::getConnection();
$sql->quickExec("delete from `" . static::$sqlTable . "` where `expireStamp` is not NULL and `expireStamp` < NOW()");
}
public static function getByToken(string $token)
{
if (strlen($token) != 128) {
return null;
}
$token1 = substr($token, 0, 64);
$token2 = substr($token, 64, 64);
$tx = new static();
$sql = $tx->getSql();
$sql->quickExec("START TRANSACTION");
$row = $sql->quickExec1Line($tx->__sqlSelectTemplate . " where `i`.`token1` = '" . $token1 . "' AND (`expireStamp` is NULL OR `expireStamp` >= NOW()) FOR UPDATE");
$t = new static($row, $sql);
$renewTTL = config::get("TOKEN_RENEW_" . $t->type) === null ? static::tokenTypes[$t->type]["renewTTL"] : config::get("TOKEN_RENEW_" . $t->type);
$rv=null;
if (time() > $t->expireStamp->stamp) {
// expired, delete
// $t->delete();
trigger_error("Token expired");
} else if (time() < $t->renewStamp->stamp && time() < $t->expireStamp->stamp && $token1 == $t->token1 && ($token2 == $t->token2 || $token2 == $t->token2b)) {
// token OK (not expired, match 2A or 2B
$rv= $t;
} else if (time() > $t->renewStamp->stamp && time() < $t->expireStamp->stamp && $t->token1 == $token1 && $t->token2 == $token2) {
// renew expired, 2A matched - renew
trigger_error("Token renew started");
$t->renew();
header("X-Fox-Token-Renew: " . $t->token);
header("X-Fox-Token-Expire: " . ($t->expireStamp->isNull() ? "Never" : $t->expireStamp->stamp));
header("X-Fox-JWT: " . authJwt::issueByAuthToken($t));
trigger_error("Token renew completed");
$rv= $t;
} else if (time() < $t->expireStamp->stamp && time() > ($t->renewStamp->stamp + $renewTTL / 2) && $t->token1 == $token1 && $t->token2 != $token2 && $t->token2b == $token2) {
// RenewExpired, renew+renewTTL/2 - not expired, 2A failed, 2B matched - not update, void conflict, OK
trigger_error("Token2B used!");
$rv= $t;
} else if ($t->token1 == $token1 && $t->token2 != $token2 && $token2 != $t->token2b) {
// token2 failed - token are compromised
trigger_error("Token #".$t->id." are compromised!");
$t->delete();
}
$sql->quickExec("COMMIT");
if ($rv===null) {
trigger_error("Token:: time: ".time()."; Expire: ".$t->expireStamp->stamp."dT(".(time() - $t->expireStamp->stamp)."); Renew: ".$t->renewStamp->stamp."dT(".(time() - $t->renewStamp->stamp).")");
}
return $rv;
}
public static function issue(user $user, $type = null, ?int $expireInDays = null)
{
static::dropExpired();
$t = new authToken();
$t->userId = $user->id;
$t->sessionId=uniqid(more_entropy: true).".".bin2hex(random_bytes(8));
$type = strtoupper($type);
if (array_key_exists($type, static::tokenTypes)) {
$t->type = $type;
} else {
throw new foxException("Invalid token type");
}
if ($expireInDays === null) {
$expireInDays = config::get("TOKEN_TTL_" . $type) === null ? static::tokenTypes[$type]["defaultTTL"] : config::get("TOKEN_TTL_" . $type);
}
$renewTTL = config::get("TOKEN_RENEW_" . $type) === null ? static::tokenTypes[$type]["renewTTL"] : config::get("TOKEN_RENEW_" . $type);
$t->expireStamp = empty($expireInDays) ? (new time()) : (new time(time() + ($expireInDays * 86400)));
$t->renewStamp = empty($renewTTL) ? (new time()) : (new time(time() + ($renewTTL * 60)));
for ($i = 0; $i < 32; $i ++) {
$token = substr(preg_replace("/[-_+=\\/]/", "", base64_encode(random_bytes(64))), 0, 64);
if (static::getByToken($token) === null) {
break;
}
}
if ($i >= 32) {
throw new foxException("Unable to find token in $i iterations");
}
if ($i > 0) {
trigger_error("Token found in $i iterations");
}
$t->token1 = $token;
$t->token2 = substr(preg_replace("/[-_+=\\/]/", "", base64_encode(random_bytes(64))), 0, 64);
$t->save();
return $t;
}
public function renew()
{
$expireAllowRenew = config::get("TOKEN_ALLOW_RENEW_" . $this->type) === null ? static::tokenTypes[$this->type]["allowRenew"] : (config::get("TOKEN_ALLOW_RENEW_" . $this->type) == "true");
$renewTTL = config::get("TOKEN_RENEW_" . $this->type) === null ? static::tokenTypes[$this->type]["renewTTL"] : config::get("TOKEN_RENEW_" . $this->type);
$this->renewStamp = empty($renewTTL) ? (new time()) : (new time(time() + ($renewTTL * 60)));
$this->token2b = $this->token2;
$this->token2 = substr(preg_replace("/[-_+=\\/]/", "", base64_encode(random_bytes(64))), 0, 64);
if ($expireAllowRenew) {
$expireInDays = config::get("TOKEN_TTL_" . $this->type) === null ? static::tokenTypes[$this->type]["defaultTTL"] : config::get("TOKEN_TTL_" . $this->type);
$this->expireStamp = empty($expireInDays) ? (new time()) : (new time(time() + ($expireInDays * 86400)));
} else {
if ($this->expireStamp < $this->renewStamp) {
$this->renewStamp = $this->expireStamp;
}
}
$this->save();
$this->renewed=true;
return $this->token;
}
public function __get($key)
{
switch ($key) {
case "token":
return $this->token1 . $this->token2;
case "user":
if (empty($this->__user) && ! empty($this->userId)) {
$this->__user = new user($this->userId);
}
return $this->__user;
default:
return parent::__get($key);
}
}
}
?>