From 47281ed7fc6a8f975ce6a1cca41f38b67b12d0c5 Mon Sep 17 00:00:00 2001 From: djpbessems Date: Mon, 28 Jan 2019 14:42:49 +0100 Subject: [PATCH 1/3] Comments added, along with tiny fixes --- public/lucidAuth.login.php | 2 ++ public/lucidAuth.validateRequest.php | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/public/lucidAuth.login.php b/public/lucidAuth.login.php index f51d157..cf7dccd 100644 --- a/public/lucidAuth.login.php +++ b/public/lucidAuth.login.php @@ -20,6 +20,8 @@ // Save authentication token in cookie $httpHost = $_SERVER['HTTP_HOST']; $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]; setcookie('JWT', $result['token'], (time() + $settings->Session['Duration']), '/', '.' . $cookieDomain); diff --git a/public/lucidAuth.validateRequest.php b/public/lucidAuth.validateRequest.php index 41ac22e..ee300df 100644 --- a/public/lucidAuth.validateRequest.php +++ b/public/lucidAuth.validateRequest.php @@ -20,7 +20,7 @@ file_put_contents('../requestHeaders.log', (new DateTime())->format('Y-m-d\TH:i:s.u') . ' --- ' . (json_encode($proxyHeaders, JSON_FORCE_OBJECT)) . PHP_EOL, FILE_APPEND); } - if (sizeof($proxyHeaders) == 0) { + if (sizeof($proxyHeaders) === 0) { // Non-proxied request; this is senseless, go fetch! header("HTTP/1.1 403 Forbidden"); exit; From 2776d1b421e12a0796a318f8da8f492cf1fdafa9 Mon Sep 17 00:00:00 2001 From: djpbessems Date: Mon, 28 Jan 2019 21:37:52 +0100 Subject: [PATCH 2/3] Tidied up code\nRemoved placeholders --- include/lucidAuth.functions.php | 40 +++++++++++++++++++++++++-------- public/lucidAuth.login.php | 29 +++++++----------------- 2 files changed, 39 insertions(+), 30 deletions(-) diff --git a/include/lucidAuth.functions.php b/include/lucidAuth.functions.php index e9d9b3a..ba80a30 100644 --- a/include/lucidAuth.functions.php +++ b/include/lucidAuth.functions.php @@ -57,19 +57,41 @@ function authenticateLDAP (string $username, string $password) { } } -function storeToken (string $username, string $password, object $cookie) { - global $settings; +function storeToken (string $secureToken, string $qualifiedUsername, string $httpHost) { + global $settings, $pdoDB; -} - -function retrieveTokenFromDB (string $username, string $foo) { - global $settings; + // Save authentication token in database serverside + try { + $pdoQuery = $pdoDB->prepare(' + INSERT INTO SecureToken (UserId, Value) + SELECT User.Id, :securetoken + FROM User + WHERE User.Username = :qualifiedusername + '); + $pdoQuery->execute([ + ':securetoken' => $secureToken, + ':qualifiedusername' => $qualifiedUsername + ]); + } + catch (Exception $e) { + return ['status' => 'Fail', 'reason' => $e]; + } + // 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 (setcookie('JWT', $secureToken, (time() + $settings->Session['Duration']), '/', '.' . $cookieDomain)) { + return ['status' => 'Success']; + } } 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) { @@ -82,6 +104,7 @@ function validateToken (string $secureToken) { return ['status' => 'Fail', 'reason' => '3']; } + // Retrieve all authentication tokens from database matching username $pdoQuery = $pdoDB->prepare(' SELECT SecureToken.Value FROM SecureToken @@ -100,13 +123,12 @@ function validateToken (string $secureToken) { } } + // 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) { - // At least one of the database-stored tokens match - return ['status' => 'Success', 'token' => $jwtPayload]; + return ['status' => 'Success']; } else { - // No matching token in database return ['status' => 'Fail', 'reason' => '2']; } } diff --git a/public/lucidAuth.login.php b/public/lucidAuth.login.php index cf7dccd..c663f74 100644 --- a/public/lucidAuth.login.php +++ b/public/lucidAuth.login.php @@ -6,25 +6,12 @@ if ($_POST['do'] === 'login') { $result = authenticateLDAP($_POST['username'], $_POST['password']); if ($result['status'] === 'Success') { - // Save authentication token in database - $pdoQuery = $pdoDB->prepare(' - INSERT INTO SecureToken (UserId, Value) - SELECT User.Id, :securetoken - FROM User - WHERE User.Username = :qualifiedusername - '); - $pdoQuery->execute([ - ':securetoken' => $result['token'], - ':qualifiedusername' => $settings->LDAP['Domain'] . '\\' . $_POST['username'] - ]); - // Save authentication token in cookie - $httpHost = $_SERVER['HTTP_HOST']; - $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]; - setcookie('JWT', $result['token'], (time() + $settings->Session['Duration']), '/', '.' . $cookieDomain); + // Store authentication token; in database serverside & in cookie clientside + if (storeToken($result['token'], $settings->LDAP['Domain'] . '\\' . $_POST['username'], $_SERVER['HTTP_HOST'])['status'] !== 'Success') { + // Since this action is only ever called through an AJAX-request; return JSON object + echo '{"Result":"Fail","Reason":"Failed storing authentication token in database and/or cookie"}' . PHP_EOL; + exit; + } // Convert base64 encoded string back from JSON; // forcing it into an associative array (instead of javascript's default StdClass object) @@ -32,13 +19,13 @@ $proxyHeaders = json_decode(base64_decode($_POST['ref']), JSON_OBJECT_AS_ARRAY); } catch (Exception $e) { - // Since this request is only ever called through an AJAX-request; return JSON object + // Since this action is only ever called through an AJAX-request; return JSON object echo '{"Result":"Fail","Reason":"Original request URI lost in transition"}' . PHP_EOL; exit; } $originalUri = !empty($proxyHeaders) ? $proxyHeaders['XForwardedProto'] . '://' . $proxyHeaders['XForwardedHost'] . $proxyHeaders['XForwardedUri'] : 'lucidAuth.manage.php'; - // Since this request is only ever called through an AJAX-request; return JSON object + // Since this action is only ever called through an AJAX-request; return JSON object echo '{"Result":"Success","Location":"' . $originalUri . '"}' . PHP_EOL; } else { switch ($result['reason']) { From d9e53fce495193b70cde27bd77577365b48c7472 Mon Sep 17 00:00:00 2001 From: djpbessems Date: Fri, 22 Feb 2019 11:28:42 +0100 Subject: [PATCH 3/3] Added nonfunctional workflow for crossdomain cookies --- include/lucidAuth.functions.php | 4 +++- lucidAuth.config.php.example | 15 +++++++-------- public/lucidAuth.login.php | 2 +- public/lucidAuth.setXDomainCookie.php | 18 ++++++++++++++++++ public/misc/script.index.js | 12 ++++++++---- 5 files changed, 37 insertions(+), 14 deletions(-) create mode 100644 public/lucidAuth.setXDomainCookie.php diff --git a/include/lucidAuth.functions.php b/include/lucidAuth.functions.php index ba80a30..fcc7e36 100644 --- a/include/lucidAuth.functions.php +++ b/include/lucidAuth.functions.php @@ -83,8 +83,10 @@ function storeToken (string $secureToken, string $qualifiedUsername, string $htt // 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 (setcookie('JWT', $secureToken, (time() + $settings->Session['Duration']), '/', '.' . $cookieDomain)) { + if ($cookieDomain && setcookie('JWT', $secureToken, (time() + $settings->Session['Duration']), '/', '.' . $cookieDomain)) { return ['status' => 'Success']; + } else { + return ['status' => 'Fail', 'reason' => 'Unable to store cookie(s)']; } } diff --git a/lucidAuth.config.php.example b/lucidAuth.config.php.example index 51a7671..6a2abb5 100644 --- a/lucidAuth.config.php.example +++ b/lucidAuth.config.php.example @@ -22,10 +22,7 @@ return (object) array( 'Path' => '../data/lucidAuth.sqlite.db' // Relative path to the location where the database should be stored ], - - 'ApiKeyFile' => 'externalResource.api.key', - // File containing your token - + 'JWT' => [ 'PrivateKey_base64' => '', // A base64-encoded random (preferably long) string (see https://www.base64encode.org/) @@ -35,20 +32,22 @@ return (object) array( ], 'Session' => [ - 'Duration' => 2592000, + 'Duration' => 2592000, // In seconds (2592000 is equivalent to 30 days) + 'CrossDomainLogin' => False, + // Set this to True if SingleSignOn (albeit rudementary) is desired + // (cookies are inheritently unaware of each other; clearing cookies for one domain does not affect other domains) 'CookieDomains' => [ 'domain1.tld' #, 'domain2.tld', 'subdomain.domain3.tld' ] // Domain(s) that will be used to set cookie-domains to // (multiple domains are allowed; remove the '#' above) ], - + 'Debug' => [ - 'Verbose' => False, + 'Verbose' => False, 'LogToFile' => False ] - ); ?> \ No newline at end of file diff --git a/public/lucidAuth.login.php b/public/lucidAuth.login.php index c663f74..4e7445c 100644 --- a/public/lucidAuth.login.php +++ b/public/lucidAuth.login.php @@ -26,7 +26,7 @@ $originalUri = !empty($proxyHeaders) ? $proxyHeaders['XForwardedProto'] . '://' . $proxyHeaders['XForwardedHost'] . $proxyHeaders['XForwardedUri'] : 'lucidAuth.manage.php'; // Since this action is only ever called through an AJAX-request; return JSON object - echo '{"Result":"Success","Location":"' . $originalUri . '"}' . PHP_EOL; + echo sprintf('{"Result":"Success","Location":"%1$s","CrossDomainLogin":%2$s}', $originalUri, $settings->Session['CrossDomainLogin'] ? 'True' : 'False') . PHP_EOL; } else { switch ($result['reason']) { case '1': diff --git a/public/lucidAuth.setXDomainCookie.php b/public/lucidAuth.setXDomainCookie.php new file mode 100644 index 0000000..c951e24 --- /dev/null +++ b/public/lucidAuth.setXDomainCookie.php @@ -0,0 +1,18 @@ +Session['CookieDomains'] and iterate through the remaining domains, serving them in one page (which contains iframes already) + // this might be slower because it means one additional roundtrip between client and server + + // approach 2: + // let the client setup multiple iframes for all domains other than origin domains + // this requires passing an array of domains to the client in asynchronous reply; which feels insecure + +?> \ No newline at end of file diff --git a/public/misc/script.index.js b/public/misc/script.index.js index e0c925e..d62d6d3 100644 --- a/public/misc/script.index.js +++ b/public/misc/script.index.js @@ -1,7 +1,7 @@ $(document).ready(function(){ // Allow user to press enter to submit credentials $('#username, #password').keypress(function(event) { - if (event.which == 13) { + if (event.which === 13) { $('#btnlogin').trigger('click'); } }); @@ -26,16 +26,20 @@ $(document).ready(function(){ catch (e) { console.log(data); } - if (ajaxData.Result == 'Success') { + if (ajaxData.Result === 'Success') { $('#btnlogin').css({ 'background': 'green url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAaklEQVQ4jeXOMQ5AQBBG4T2BC4i76EWich7ncAKbqCRuodTqnMNTkFgJs3ZU4tXz/Rlj/hUQv8EpMAClFk9sjUAiHVcCnoFMwhZYgPYG575Xe46aIOyMdJx7ji9GwrEzUgOFCu8DkRp/qxU2BKCUyZR6ygAAAABJRU5ErkJggg==) no-repeat center', 'transform': 'rotateX(0deg)' }); setTimeout(function() { - $('#btnsync').prop('disabled', false).css({ + $('#btnlogin').prop('disabled', false).css({ 'background': '#B50000 linear-gradient(0deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0) 50%, rgba(255,255,255,0.25) 51%) no-repeat center', 'color': '#FFF' }); + if (ajaxData.CrossDomainLogin) { + // Create iframes for other domains +console.log('CrossDomainLogin initiated'); + } window.location.replace(ajaxData.Location); }, 2250); } else { @@ -44,7 +48,7 @@ $(document).ready(function(){ 'transform': 'rotateX(0deg)' }); setTimeout(function() { - $('#btnsync').prop('disabled', false).css({ + $('#btnlogin').prop('disabled', false).css({ 'background': '#B50000 linear-gradient(0deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0) 50%, rgba(255,255,255,0.25) 51%) no-repeat center', 'color': '#FFF' });