2019-02-27 20:39:31 +00:00
< ? php
$configurationFile = '../lucidAuth.config.php' ;
if ( ! file_exists ( $configurationFile )) {
throw new Exception ( sprintf ( 'Missing config file. Please rename \'%1$s.example\' to \'%1$s\' and edit it to reflect your setup.' , explode ( '../' , $configurationFile )[ 1 ]));
}
$settings = include_once ( $configurationFile );
try {
# switch ($settings->Database['Driver']) {
# case 'sqlite':
# $database = new PDO('sqlite:' . $settings->Database['Path']);
if ( is_writable ( $settings -> Sqlite [ 'Path' ])) {
$pdoDB = new PDO ( 'sqlite:' . $settings -> Sqlite [ 'Path' ]);
} else {
throw new Exception ( sprintf ( 'Database file \'%1$s\' is not writable' , $settings -> Sqlite [ 'Path' ]));
}
# }
}
catch ( Exception $e ) {
throw new Exception ( sprintf ( 'Unable to connect to database \'%1$s\'' , $settings -> Sqlite [ 'Path' ]));
}
function authenticateLDAP ( string $username , string $password ) {
global $settings ;
if ( ! empty ( $username ) && ! empty ( $password )) {
// Handle login requests
$ds = ldap_connect ( $settings -> LDAP [ 'Server' ], $settings -> LDAP [ 'Port' ]);
// Strict namingconvention: only allow alphabetic characters
$sanitizedUsername = preg_replace ( '([^a-zA-Z]*)' , '' , $_POST [ 'username' ]);
$qualifiedUsername = $settings -> LDAP [ 'Domain' ] . '\\' . $sanitizedUsername ;
if ( @ ldap_bind ( $ds , $qualifiedUsername , utf8_encode ( $_POST [ 'password' ]))) {
// Successful authentication; get additional userdetails from authenticationsource
$ldapSearchResults = ldap_search ( $ds , $settings -> LDAP [ 'BaseDN' ], " sAMAccountName= $sanitizedUsername " );
2019-12-24 12:58:01 +00:00
$commonName = ldap_get_entries ( $ds , $ldapSearchResults )[ 0 ][ 'cn' ][ 0 ];
$browserDetails = get_browser ( null , True );
$geoLocation = json_decode ( file_get_contents ( " http://ip-api.com/json/ { $_SERVER [ 'HTTP_X_REAL_IP' ] } " ));
if ( $geoLocation -> status === 'fail' ) {
switch ( $geoLocation -> message ) {
case 'private range' :
case 'reserved range' :
$geoLocation = json_decode ( file_get_contents ( " http://ip-api.com/json/ " . trim ( file_get_contents ( 'https://api.ipify.org' )) ));
break ;
case 'invalid query' :
default :
$geoLocation -> city = null ;
$geoLocation -> countryCode = null ;
break ;
}
}
// Create JWT-payload
2019-02-27 20:39:31 +00:00
$jwtPayload = [
2019-12-24 12:58:01 +00:00
'iat' => time (), // Issued at: time when the token was generated
'iss' => $_SERVER [ 'SERVER_NAME' ], // Issuer
'sub' => $qualifiedUsername , // Subject (ie. username)
'name' => $commonName , // Common name (as retrieved from AD)
'fp' => base64_encode ( json_encode (( object ) [ // Fingerprint
'browser' => $browserDetails [ 'browser' ],
'platform' => $browserDetails [ 'platform' ],
'city' => $geoLocation -> city ,
'countrycode' => $geoLocation -> countryCode
]))
2019-02-27 20:39:31 +00:00
];
$secureToken = JWT :: encode ( $jwtPayload , base64_decode ( $settings -> JWT [ 'PrivateKey_base64' ]));
return [ 'status' => 'Success' , 'token' => $secureToken ];
} else {
// LDAP authentication failed!
return [ 'status' => 'Fail' , 'reason' => '1' ];
}
} else {
// Empty username or passwords not allowed!
return [ 'status' => 'Fail' , 'reason' => '1' ];
}
}
function storeToken ( string $secureToken , string $qualifiedUsername , string $httpHost ) {
global $settings , $pdoDB ;
// Save authentication token in database serverside
try {
$pdoQuery = $pdoDB -> prepare ( '
INSERT INTO SecureToken ( UserId , Value )
SELECT User . Id , : securetoken
FROM User
2019-03-07 19:50:04 +00:00
WHERE LOWER ( User . Username ) = : qualifiedusername
2019-02-27 20:39:31 +00:00
' );
$pdoQuery -> execute ([
':securetoken' => $secureToken ,
2019-03-07 19:50:04 +00:00
':qualifiedusername' => strtolower ( $qualifiedUsername )
2019-02-27 20:39:31 +00:00
]);
}
catch ( Exception $e ) {
return [ 'status' => 'Fail' , 'reason' => $e ];
}
2019-03-06 13:21:47 +00:00
2019-02-27 20:39:31 +00:00
// Save authentication token in cookie clientside
$cookieDomain = array_values ( array_filter ( $settings -> Session [ 'CookieDomains' ], function ( $value ) use ( $httpHost ) {
// Check if $_SERVER['HTTP_HOST'] matches any of the configured domains (either explicitly or as a subdomain)
// This might seem backwards, but relying on $_SERVER directly allows spoofed values with potential security risks
return ( strlen ( $value ) > strlen ( $httpHost )) ? false : ( 0 === substr_compare ( $httpHost , $value , - strlen ( $value )));
}))[ 0 ];
if ( $cookieDomain && setcookie ( 'JWT' , $secureToken , ( time () + $settings -> Session [ 'Duration' ]), '/' , '.' . $cookieDomain )) {
return [ 'status' => 'Success' ];
} else {
return [ 'status' => 'Fail' , 'reason' => 'Unable to store cookie(s)' ];
}
}
function validateToken ( string $secureToken ) {
global $settings , $pdoDB ;
// Decode provided authentication token
try {
$jwtPayload = JWT :: decode ( $secureToken , base64_decode ( $settings -> JWT [ 'PrivateKey_base64' ]), $settings -> JWT [ 'Algorithm' ]);
} catch ( Exception $e ) {
// Invalid token
if ( $settings -> Debug [ 'LogToFile' ]) {
file_put_contents ( '../validateToken.log' , ( new DateTime ()) -> format ( 'Y-m-d\TH:i:s.u' ) . ' --- Provided token could not be decoded' . PHP_EOL , FILE_APPEND );
}
return [ 'status' => 'Fail' , 'reason' => '1' ];
}
if (( int ) $jwtPayload -> iat < ( time () - ( int ) $settings -> Session [ 'Duration' ])) {
// Expired token
if ( $settings -> Debug [ 'LogToFile' ]) {
file_put_contents ( '../validateToken.log' , ( new DateTime ()) -> format ( 'Y-m-d\TH:i:s.u' ) . ' --- Provided token has expired' . PHP_EOL , FILE_APPEND );
}
return [ 'status' => 'Fail' , 'reason' => '3' ];
}
// Retrieve all authentication tokens from database matching username
$pdoQuery = $pdoDB -> prepare ( '
2019-03-06 13:21:47 +00:00
SELECT User . Id , SecureToken . Value
2019-02-27 20:39:31 +00:00
FROM SecureToken
2019-03-06 13:21:47 +00:00
LEFT JOIN User
2019-02-27 20:39:31 +00:00
ON ( User . Id = SecureToken . UserId )
2019-03-07 19:50:04 +00:00
WHERE LOWER ( User . Username ) = : username
2019-02-27 20:39:31 +00:00
' );
$pdoQuery -> execute ([
2019-12-10 15:57:06 +00:00
':username' => ( string ) strtolower ( $jwtPayload -> sub )
]);
2019-02-27 20:39:31 +00:00
foreach ( $pdoQuery -> fetchAll ( PDO :: FETCH_ASSOC ) as $row ) {
try {
$storedTokens [] = JWT :: decode ( $row [ 'Value' ], base64_decode ( $settings -> JWT [ 'PrivateKey_base64' ]), $settings -> JWT [ 'Algorithm' ]);
2019-03-06 13:21:47 +00:00
$currentUserId = $row [ 'Id' ];
2019-02-27 20:39:31 +00:00
} catch ( Exception $e ) {
continue ;
}
}
// Compare provided authentication token to all stored tokens in database
if ( ! empty ( $storedTokens ) && sizeof ( array_filter ( $storedTokens , function ( $value ) use ( $jwtPayload ) {
return $value -> iat === $jwtPayload -> iat ;
})) === 1 ) {
2019-12-10 15:57:06 +00:00
purgeTokens ( $currentUserId , $settings -> Session [ 'Duration' ]);
return [
2019-02-27 20:39:31 +00:00
'status' => 'Success' ,
2019-03-06 13:21:47 +00:00
'name' => $jwtPayload -> name ,
'uid' => $currentUserId
2019-02-27 20:39:31 +00:00
];
} else {
if ( $settings -> Debug [ 'LogToFile' ]) {
file_put_contents ( '../validateToken.log' , ( new DateTime ()) -> format ( 'Y-m-d\TH:i:s.u' ) . ' --- Either no matching token or multiple matching tokens found in database' . PHP_EOL , FILE_APPEND );
}
return [ 'status' => 'Fail' , 'reason' => '2' ];
}
}
2019-12-10 15:57:06 +00:00
function purgeTokens ( int $userID , int $maximumTokenAge ) {
global $settings , $pdoDB ;
$defunctTokens = []; $expiredTokens = [];
$pdoQuery = $pdoDB -> prepare ( '
SELECT SecureToken . Id , SecureToken . Value
FROM SecureToken
WHERE SecureToken . UserId = : userid
' );
$pdoQuery -> execute ([
':userid' => ( int ) $userID
]);
foreach ( $pdoQuery -> fetchAll ( PDO :: FETCH_ASSOC ) as $row ) {
try {
$token = JWT :: decode ( $row [ 'Value' ], base64_decode ( $settings -> JWT [ 'PrivateKey_base64' ]), $settings -> JWT [ 'Algorithm' ]);
if ( $token -> iat < ( time () - $maximumTokenAge )) {
$expiredTokens [] = $row [ 'Id' ];
}
} catch ( Exception $e ) {
$defunctTokens [] = $row [ 'Id' ];
}
}
try {
// Sadly, PDO does not support named parameters in constructions like 'IN ( :array )'
// instead, the supported syntax is unnamed placeholders like 'IN (?, ?, ?, ...)'
$pdoQuery = $pdoDB -> prepare ( '
DELETE FROM SecureToken
WHERE SecureToken . Id IN ( ' . implode( ' , ', array_fill(0, count(array_merge($defunctTokens, $expiredTokens)), ' ? ')) . ' )
' );
$pdoQuery -> execute ( array_merge ( $defunctTokens , $expiredTokens ));
if ( $settings -> Debug [ 'LogToFile' ]) {
file_put_contents ( '../purgeToken.log' , ( new DateTime ()) -> format ( 'Y-m-d\TH:i:s.u' ) . ' --- Garbage collection succeeded (' . $userID . ' => ' . $pdoQuery -> rowCount () . ')' . PHP_EOL , FILE_APPEND );
}
return [
'status' => 'Success' ,
'amount' => $pdoQuery -> rowCount ()
];
} catch ( Exception $e ) {
if ( $settings -> Debug [ 'LogToFile' ]) {
file_put_contents ( '../purgeToken.log' , ( new DateTime ()) -> format ( 'Y-m-d\TH:i:s.u' ) . ' --- Garbage collection failed (' . $userID . ' => ' . $e . ')' . PHP_EOL , FILE_APPEND );
}
return [ 'status' => 'Fail' , 'reason' => $e ];
}
}
2019-01-16 10:37:35 +00:00
?>