From 3111185c10a3d47abcf681be1bda157baeaded22 Mon Sep 17 00:00:00 2001 From: Danny Bessems Date: Tue, 10 Dec 2019 15:57:06 +0000 Subject: [PATCH] Added garbage collection for expired and defunct tokens in database. --- include/lucidAuth.functions.php | 67 +++++++++++++++++++++++++++---- include/lucidAuth.template.php | 16 ++++++++ public/lucidAuth.manage.php | 19 +++++++-- public/misc/script.manage.js | 33 +++++++++++++++ public/misc/script.translation.js | 6 ++- public/misc/style.css | 17 ++++++++ 6 files changed, 146 insertions(+), 12 deletions(-) diff --git a/include/lucidAuth.functions.php b/include/lucidAuth.functions.php index df2aab2..675beb7 100644 --- a/include/lucidAuth.functions.php +++ b/include/lucidAuth.functions.php @@ -38,10 +38,11 @@ function authenticateLDAP (string $username, string $password) { $commonName = ldap_get_entries($ds, $ldapSearchResults)[0]['cn'][0]; // Create JWT-payload $jwtPayload = [ - '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) + '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(get_browser(null, True))) // Fingerprint (based on `HTTP_USER_AGENT`) ]; $secureToken = JWT::encode($jwtPayload, base64_decode($settings->JWT['PrivateKey_base64'])); @@ -121,8 +122,8 @@ function validateToken (string $secureToken) { WHERE LOWER(User.Username) = :username '); $pdoQuery->execute([ - ':username' => (string) strtolower($jwtPayload->sub) - ]); + ':username' => (string) strtolower($jwtPayload->sub) + ]); foreach($pdoQuery->fetchAll(PDO::FETCH_ASSOC) as $row) { try { $storedTokens[] = JWT::decode($row['Value'], base64_decode($settings->JWT['PrivateKey_base64']), $settings->JWT['Algorithm']); @@ -136,7 +137,9 @@ function validateToken (string $secureToken) { if (!empty($storedTokens) && sizeof(array_filter($storedTokens, function ($value) use ($jwtPayload) { return $value->iat === $jwtPayload->iat; })) === 1) { - return [ + purgeTokens($currentUserId, $settings->Session['Duration']); + + return [ 'status' => 'Success', 'name' => $jwtPayload->name, 'uid' => $currentUserId @@ -149,4 +152,54 @@ function validateToken (string $secureToken) { } } +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]; + } +} + ?> \ No newline at end of file diff --git a/include/lucidAuth.template.php b/include/lucidAuth.template.php index ebc8c29..197c737 100644 --- a/include/lucidAuth.template.php +++ b/include/lucidAuth.template.php @@ -130,6 +130,22 @@ $contentLayout['manage']['section'] = <<<'MANAGE_SECTION' %1$s +
+ Sessions +
+ + + + + + + + + + + +
Timestamp<origin>DescriptionManage
+
MANAGE_SECTION; diff --git a/public/lucidAuth.manage.php b/public/lucidAuth.manage.php index 0096f3b..7f17ee9 100644 --- a/public/lucidAuth.manage.php +++ b/public/lucidAuth.manage.php @@ -9,23 +9,36 @@ if ($validateTokenResult['status'] === "Success") { if ($_REQUEST['do'] === 'retrievesessions') { + $storedTokens = []; + $pdoQuery = $pdoDB->prepare(' SELECT SecureToken.Id, SecureToken.UserId, SecureToken.Value FROM SecureToken - WHERE SecureToken.Id = :userid + WHERE SecureToken.UserId = :userid '); $pdoQuery->execute([ ':userid' => (int) $_REQUEST['userid'] ]); foreach($pdoQuery->fetchAll(PDO::FETCH_ASSOC) as $row) { - //bla + try { + $JWTPayload = JWT::decode($row['Value'], base64_decode($settings->JWT['PrivateKey_base64']), $settings->JWT['Algorithm']); + $storedTokens[] = [ + 'iat' => $JWTPayload->iat, + 'iss' => $JWTPayload->iss, + 'fp' => $JWTPayload->fp + ]; + } catch (Exception $e) { + // Invalid token + continue; + } } // Return JSON object header('Content-Type: application/json'); echo json_encode([ "Result" => "Success", - "UserSessions" => json_encode( $moo ) + "SessionCount" => sizeof($storedTokens), + "UserSessions" => json_encode($storedTokens) ]); } else { // No action requested, default action diff --git a/public/misc/script.manage.js b/public/misc/script.manage.js index c4d3f15..63cd056 100644 --- a/public/misc/script.manage.js +++ b/public/misc/script.manage.js @@ -2,14 +2,43 @@ $(document).ready(function(){ // Initialize the editable-table functionality $('#usertable').editableTableWidget(); + // Prevent clicking *through* popup-screens + $('#sessions').click(function(event) { + event.stopPropagation(); + }); + // Add eventhandlers to buttons $('#usertable button.session').click(function() { + event.stopPropagation(); + $('#sessions tbody').empty(); + $('#sessions').fadeToggle(); + $.post("lucidAuth.manage.php", { do: "retrievesessions", userid: $(this).closest('tr').find('td:nth-child(1)').data('userid') }) .done(function(data,_status) { if (data.Result === 'Success') { + var Sessions = JSON.parse(data.UserSessions); + for (var i = 0; i < data.SessionCount; i++) { + try { + var Fingerprint = JSON.parse(atob(Sessions[i]['fp'])); + } catch(e) { + // Do nothing + } + $('#sessiontable tbody').append($('') + .append($('', { + text: new Date(Sessions[i]['iat'] * 1000).toLocaleString('en-GB') + })) + .append($('', { + text: Sessions[i]['iss'] + })) + .append($('', { +// text: Sessions[i]['fp'] ? atob(Sessions[i]['fp'])['browser'] + '(' + atob(Sessions[i]['fp'])['platform'] + ')' : '' + text: Fingerprint ? Fingerprint['browser'] + ' (' + Fingerprint['platform'] + ')' : '' + })) + ); + } } else { } }); @@ -107,4 +136,8 @@ console.log({'new': newEntries, 'removed': removedEntries}); } }); +}); + +$(document).click(function () { + $('#sessions').fadeOut(); }); \ No newline at end of file diff --git a/public/misc/script.translation.js b/public/misc/script.translation.js index 9b002cd..b6e10db 100644 --- a/public/misc/script.translation.js +++ b/public/misc/script.translation.js @@ -7,7 +7,8 @@ var locales = { button_delete: "delete", button_login: "login", heading_error: "ERROR!", - label_password: "Password:", + label_password: "Password:", + label_sessions: "Sessions", label_username: "Username:", link_logout: "Logout", span_credentialsavailable: "Login credentials available upon request!", @@ -24,7 +25,8 @@ var locales = { button_delete: "verwijder", button_login: "log in", heading_error: "FOUT!", - label_password: "Wachtwoord:", + label_password: "Wachtwoord:", + label_sessions: "Sessies", label_username: "Gebruikersnaam:", link_logout: "Log uit", span_credentialsavailable: "Inloggegevens verkrijgbaar op aanvraag!", diff --git a/public/misc/style.css b/public/misc/style.css index 5a76a3f..7e59f63 100644 --- a/public/misc/style.css +++ b/public/misc/style.css @@ -129,6 +129,23 @@ body { .main section .buttons button { margin-left: inherit; } + .main section #sessions { + display: none; + position: absolute; + top: calc(50% - 160px); + right: 20%; + height: 320px; + width: 60%; + border: 1px solid rgb(0, 51, 153); + box-shadow: black 0px 0px 20px; + box-sizing: border-box; + padding-top: 5px; + background: white; + font-size: inherit; + font-weight: bold; + z-index: 99; + overflow-y: auto; + } .main section table { width: 100%; }