2022-04-26 19:48:03 +00:00
< ? 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 $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 ;
/*
* 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
],
" token1 " => [
" type " => " CHAR(64) " ,
" index " => " UNIQUE " ,
" nullable " => true
],
" token2 " => [
" type " => " CHAR(64) " ,
" nullable " => false
],
" token2b " => [
" type " => " CHAR(64) " ,
" nullable " => true
]
];
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 );
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! " );
2022-05-12 13:59:48 +00:00
$t -> delete ();
2022-04-26 19:48:03 +00:00
}
$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 ;
$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 ();
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 );
}
}
}
?>