From afd134659ba1b21e8c075cd175d0eb142775146f Mon Sep 17 00:00:00 2001 From: djpbessems Date: Thu, 17 Jan 2019 15:06:16 +0100 Subject: [PATCH 1/9] Corrected static exception message to dynamic message --- include/lucidAuth.functions.php | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/include/lucidAuth.functions.php b/include/lucidAuth.functions.php index f187dae..0b3f5ec 100644 --- a/include/lucidAuth.functions.php +++ b/include/lucidAuth.functions.php @@ -1,10 +1,10 @@ LDAP['Server'], $settings->LDAP['Port']); // Strict namingconvention: only allow alphabetic characters - $strGivenname = preg_replace('([^a-zA-Z]*)', '', $_POST['username']); - $strUsername = $settings->LDAP['Domain'] . '\\' . $strGivenname; + $sanitizedUsername = preg_replace('([^a-zA-Z]*)', '', $_POST['username']); + $qualifiedUsername = $settings->LDAP['Domain'] . '\\' . $sanitizedUsername; - if (@ldap_bind($ds, $strUsername, utf8_encode($_POST['password']))) { - // Successful auth; get additional userdetails from Active Directory - $ldapSearchResults = ldap_search($ds, $settings->LDAP['BaseDN'], "sAMAccountName=$strGivenname"); - $strFullname = ldap_get_entries($ds, $ldapSearchResults)[0]['cn'][0]; + 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"); + $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' => $strGivenname, // Subject (ie. username) - 'name' => $strFullname // Full 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) ]; $secureToken = JWT::encode($jwtPayload, base64_decode($settings->JWT['PrivateKey_base64'])); From b04b2fb48027acbee441eebd0a5ee01b9e9e7b0e Mon Sep 17 00:00:00 2001 From: djpbessems Date: Wed, 23 Jan 2019 22:08:30 +0100 Subject: [PATCH 2/9] Rudimentary implementation of authentication processflow --- include/lucidAuth.functions.php | 46 ++++++++++++++++++++++++---- include/lucidAuth.template.php | 3 +- lucidAuth.config.php.example | 2 +- public/lucidAuth.login.php | 16 ++++++++-- public/lucidAuth.validateRequest.php | 21 ++++++++----- public/misc/script.index.js | 3 +- 6 files changed, 71 insertions(+), 20 deletions(-) diff --git a/include/lucidAuth.functions.php b/include/lucidAuth.functions.php index 0b3f5ec..805268b 100644 --- a/include/lucidAuth.functions.php +++ b/include/lucidAuth.functions.php @@ -5,6 +5,16 @@ 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']); + $pdoDB = new PDO('sqlite:' . $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; @@ -45,21 +55,45 @@ function authenticateLDAP (string $username, string $password) { function storeToken (string $username, string $password, object $cookie) { global $settings; - } -function retrieveToken (string $username, string $foo) { +function retrieveTokenFromDB (string $username, string $foo) { global $settings; } -function validateCookie (int $expiration, string $username, string $securetoken) { -# $_COOKIE['Exp'], $_COOKIE['Sub'], $_COOKIE['JWT'] +function validateToken (array $cookieData) { global $settings; - If ($expiration > time()) { - #moo + try { + $jwtPayload = JWT::decode($cookieData['token'], base64_decode($settings->JWT['PrivateKey_base64']), $settings->JWT['Algorithm']); + } catch (Exception $e) { + // Invalid token, inform client (client should handle discarding invalid token) + return ['status' => 'Fail', 'reason' => '3']; } + + $pdoQuery = $pdoDB->prepare(' + SELECT SecureToken.Payload + FROM SecureToken + LEFT JOIN User + ON (User.Id=SecureToken.UserId) + WHERE User.Username = :username + '); + $pdoQuery->execute([ + 'username' => ($_COOKIE['Sub'] ?? "Danny") + ]); + foreach($pdoQuery->fetchAll(PDO::FETCH_ASSOC) as $row) { + $tokens[] = $row['Payload']; + } + print_r($tokens); +# if ($pdoResult['Username']) + + + If ($cookieData['Exp'] < time()) { + // Expired cookie (shouldn't the browser disregard it?) + return ['status' => 'Fail', 'reason' => '3']; + } + } ?> \ No newline at end of file diff --git a/include/lucidAuth.template.php b/include/lucidAuth.template.php index d972ec6..e6db275 100644 --- a/include/lucidAuth.template.php +++ b/include/lucidAuth.template.php @@ -69,7 +69,8 @@ $contentLayout['login'] = <<
  • - + +
  •   diff --git a/lucidAuth.config.php.example b/lucidAuth.config.php.example index 9165393..46768b6 100644 --- a/lucidAuth.config.php.example +++ b/lucidAuth.config.php.example @@ -21,7 +21,7 @@ return (object) array( 'DomainNames' => ['*.subdomain.domain.{(tld1|tld2)}'], 'Sqlite' => [ - 'Path' => '../config/lucidAuth.sqlite.db' + 'Path' => '../data/lucidAuth.sqlite.db' // Relative path to the location where the database should be stored ], diff --git a/public/lucidAuth.login.php b/public/lucidAuth.login.php index 3dc7af2..73f36fb 100644 --- a/public/lucidAuth.login.php +++ b/public/lucidAuth.login.php @@ -3,13 +3,23 @@ include_once('../include/lucidAuth.functions.php'); - echo $settings->Debug['Verbose']; - if ($_POST['do'] == 'login') { $result = authenticateLDAP($_POST['username'], $_POST['password']); if ($result['status'] == 'Success') { + // Convert base64 encoded string back from JSON; + // forcing it into an associative array (instead of javascript's default StdClass object) + try { + $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 + echo '{"Result":"Fail","Reason":"Original request URI lost in transition"}' . PHP_EOL; + exit; + } + $originalUri = $proxyHeaders['XForwardedProto'] . '://' . $proxyHeaders['XForwardedHost'] . $proxyHeaders['XForwardedUri']; + // Since this request is only ever called through an AJAX-request; return JSON object - echo '{"Result":"Success","Location":""}' . PHP_EOL; + echo '{"Result":"Success","Location":"' . $originalUri . '"}' . PHP_EOL; } else { switch ($result['reason']) { case '1': diff --git a/public/lucidAuth.validateRequest.php b/public/lucidAuth.validateRequest.php index c0d88a1..336d594 100644 --- a/public/lucidAuth.validateRequest.php +++ b/public/lucidAuth.validateRequest.php @@ -16,7 +16,10 @@ }, ARRAY_FILTER_USE_KEY); // For debugging purposes - enable it in ../lucidAuth.config.php - if ($settings->Debug['LogToFile']) 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 ($settings->Debug['LogToFile']) { + 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); + file_put_contents('../requestHeaders.log', (new DateTime())->format('Y-m-d\TH:i:s.u') . ' --+ ' . (base64_encode(json_encode($proxyHeaders))) . PHP_EOL, FILE_APPEND); + } # if (sizeof($proxyHeaders) == 0) { if (False) { @@ -25,16 +28,18 @@ exit; } -# if (validateToken($_COOKIE['Exp'], $_COOKIE['Sub'], $_COOKIE['JWT']) != True) { - if (False) { - // No or invalid authentication token found, redirecting to loginpage - header("HTTP/1.1 401 Unauthorized"); -#remember to include cookies/headers/something - header("Location: lucidAuth.login.php"); - } else { + if ((!empty($_COOKIE['Exp']) && !empty($_COOKIE['Sub']) && !empty($_COOKIE['JWT'])) && validateToken([ + 'Exp' => $_COOKIE['Exp'], + 'Sub' => $_COOKIE['Sub'], + 'JWT' => $_COOKIE['JWT'] + ])['status'] == "Success") { // Valid authentication token found header("HTTP/1.1 202 Accepted"); exit; + } else { + // No cookie containing valid authentication token found, redirecting to loginpage + header("HTTP/1.1 401 Unauthorized"); + header("Location: lucidAuth.login.php?ref=" . base64_encode(json_encode($proxyHeaders))); } ?> \ No newline at end of file diff --git a/public/misc/script.index.js b/public/misc/script.index.js index e1f4254..e0c925e 100644 --- a/public/misc/script.index.js +++ b/public/misc/script.index.js @@ -16,7 +16,8 @@ $(document).ready(function(){ $.post("lucidAuth.login.php", { do: "login", username: $('#username').val(), - password: $('#password').val() + password: $('#password').val(), + ref: $('#ref').val() }) .done(function(data,status) { try { From 203536cfd5c5f305231d2d5edf6fe5fb752be195 Mon Sep 17 00:00:00 2001 From: djpbessems Date: Wed, 23 Jan 2019 22:29:26 +0100 Subject: [PATCH 3/9] Fixed non-proxied loginrequest logic --- public/lucidAuth.login.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/lucidAuth.login.php b/public/lucidAuth.login.php index 73f36fb..0024bdf 100644 --- a/public/lucidAuth.login.php +++ b/public/lucidAuth.login.php @@ -16,7 +16,7 @@ echo '{"Result":"Fail","Reason":"Original request URI lost in transition"}' . PHP_EOL; exit; } - $originalUri = $proxyHeaders['XForwardedProto'] . '://' . $proxyHeaders['XForwardedHost'] . $proxyHeaders['XForwardedUri']; + $originalUri = !empty($proxyHeaders) ? $proxyHeaders['XForwardedProto'] . '://' . $proxyHeaders['XForwardedHost'] . $proxyHeaders['XForwardedUri'] : '#'; // Since this request is only ever called through an AJAX-request; return JSON object echo '{"Result":"Success","Location":"' . $originalUri . '"}' . PHP_EOL; From 579403c1277c2b57140032b883ea41a6a76e0a78 Mon Sep 17 00:00:00 2001 From: djpbessems Date: Thu, 24 Jan 2019 16:31:16 +0100 Subject: [PATCH 4/9] Changed style/theming --- include/lucidAuth.template.php | 2 +- public/images/bg_header.gif | Bin 299 -> 318 bytes public/images/tag_lock.png | Bin 0 -> 29503 bytes public/images/tag_pvr.png | Bin 7862 -> 0 bytes public/misc/style.button.css | 2 +- public/misc/style.css | 10 +++++----- 6 files changed, 7 insertions(+), 7 deletions(-) create mode 100644 public/images/tag_lock.png delete mode 100644 public/images/tag_pvr.png diff --git a/include/lucidAuth.template.php b/include/lucidAuth.template.php index e6db275..95bddc7 100644 --- a/include/lucidAuth.template.php +++ b/include/lucidAuth.template.php @@ -80,7 +80,7 @@ $contentLayout['login'] = << - PVR [Secure] + Secure! LOGIN; $contentLayout['manage'] = <<qxqY2cdJM!uPG=$u->9=HkcL!n?WEDn!D1%XH; xG71_B8w?B$4IK_14j&H>5D*{{ArT@H5)%_66eOG#6&4mH7o#Q^7#yiV06PpUR^I>s literal 299 zcmZ?wbhEHbWM#QWG6nH6`H@l?5mM=X;?d>;4hDP{ z(b8EVl1Vze#-3uK+Pp^fs#BZQXVt1s&XliCldDLQEsmDX4v|cD5edkUuS$_CO_VK) zmB|g0O0^gEvKDeR5wO+eGi9I+&;c0>@(Tl7fJ0+}gAP~!iB6+KIcCfO4JB4nBi7$| zQ|k3HYv0rlZ4NhbxMrVbOWass(d)G6gTW2nndg=>@A{$7#iPi>&ECeY#KG6a!67Fn OFkym(1phQg25SKQ) diff --git a/public/images/tag_lock.png b/public/images/tag_lock.png new file mode 100644 index 0000000000000000000000000000000000000000..354e5831c68b732a94536bf3fcde744ef9c342ac GIT binary patch literal 29503 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 literal 0 HcmV?d00001 diff --git a/public/images/tag_pvr.png b/public/images/tag_pvr.png deleted file mode 100644 index 1a58550de800dfc10b9c34bde870097943ddb04e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7862 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 Uk|8NJQ2+n{07*qoM6N<$f|elIx&QzG diff --git a/public/misc/style.button.css b/public/misc/style.button.css index 9a22a78..89ea54a 100644 --- a/public/misc/style.button.css +++ b/public/misc/style.button.css @@ -115,7 +115,7 @@ background: rgba(255,255,255,0.4); } .bttn-simple.bttn-primary { - background: #550055; + background: #003399; background-image: linear-gradient(0deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0) 50%, rgba(255,255,255,0.25) 51%); } .bttn-simple.bttn-warning { diff --git a/public/misc/style.css b/public/misc/style.css index c932699..5de8498 100644 --- a/public/misc/style.css +++ b/public/misc/style.css @@ -121,7 +121,7 @@ body { content: ' '; } .main fieldset input { - border: 1px solid #550055; + border: 1px solid #003399; padding: 2px; width: 100px; } @@ -135,7 +135,7 @@ body { text-transform: uppercase; } .main fieldset select { - border: 1px solid #550055; + border: 1px solid #003399; padding: 2px; width: 375px; } @@ -172,7 +172,7 @@ body { margin-left: 10px; } .main a:link, .main a:visited { - color: #550055; + color: #003399; text-decoration: none; } .main a:hover, .main a:active { @@ -188,7 +188,7 @@ body { font-size: 12px; } .main span#user a:link, .main span#user a:visited { - color: #CC1111; + color: #001177; text-decoration: none; } .main span#user a:hover, .main span#user a:active { @@ -218,7 +218,7 @@ body { right: 10px; height: 112px; width: 250px; - border: 1px solid rgb(181, 0, 0); + border: 1px solid rgb(0, 51, 153); box-shadow: black 0px 0px 20px; box-sizing: border-box; padding-top: 5px; From 118e45db9ccb816673771af5b135ed65f5d18f0f Mon Sep 17 00:00:00 2001 From: djpbessems Date: Thu, 24 Jan 2019 17:17:53 +0100 Subject: [PATCH 5/9] First iteration of using cookies to store session/securetoken --- include/lucidAuth.functions.php | 18 ++++++++++-------- lucidAuth.config.php.example | 9 +++++---- public/lucidAuth.login.php | 5 ++++- public/lucidAuth.validateRequest.php | 4 +--- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/include/lucidAuth.functions.php b/include/lucidAuth.functions.php index 805268b..5c1ba8c 100644 --- a/include/lucidAuth.functions.php +++ b/include/lucidAuth.functions.php @@ -62,11 +62,11 @@ function retrieveTokenFromDB (string $username, string $foo) { } -function validateToken (array $cookieData) { +function validateToken (string $secureToken) { global $settings; try { - $jwtPayload = JWT::decode($cookieData['token'], base64_decode($settings->JWT['PrivateKey_base64']), $settings->JWT['Algorithm']); + $jwtPayload = JWT::decode($secureToken, base64_decode($settings->JWT['PrivateKey_base64']), $settings->JWT['Algorithm']); } catch (Exception $e) { // Invalid token, inform client (client should handle discarding invalid token) return ['status' => 'Fail', 'reason' => '3']; @@ -80,17 +80,19 @@ function validateToken (array $cookieData) { WHERE User.Username = :username '); $pdoQuery->execute([ - 'username' => ($_COOKIE['Sub'] ?? "Danny") + 'username' => $jwtPayload['sub'] ]); foreach($pdoQuery->fetchAll(PDO::FETCH_ASSOC) as $row) { - $tokens[] = $row['Payload']; + $storedTokens[] = $row['Payload']; } - print_r($tokens); -# if ($pdoResult['Username']) + + print_r($storedTokens); +# if (!empty($storedTokens) && ) { +# } - If ($cookieData['Exp'] < time()) { - // Expired cookie (shouldn't the browser disregard it?) + If ($secureToken['iat'] < (time() - $settings->Session['Duration'])) { + // Expired token (shouldn't the browser disregard it?) return ['status' => 'Fail', 'reason' => '3']; } diff --git a/lucidAuth.config.php.example b/lucidAuth.config.php.example index 46768b6..a4b42fd 100644 --- a/lucidAuth.config.php.example +++ b/lucidAuth.config.php.example @@ -29,16 +29,17 @@ return (object) array( // File containing your token 'JWT' => [ - 'PrivateKey_base64' => 'result of base64_encode()', + 'PrivateKey_base64' => '', + // A base64-encoded string of a random string (see https://www.base64encode.org/) 'Algorithm' => [ 'HS256', ] ], - 'Cookie' => [ - 'Duration' => 2592000, + 'Session' => [ + 'Duration' => 2592000, // In seconds (2592000 is equivalent to 30 days) -# 'Prefix' => 'lucidAuth_' +# 'CookiePrefix' => 'lucidAuth_' ], 'Debug' => [ diff --git a/public/lucidAuth.login.php b/public/lucidAuth.login.php index 0024bdf..b8285d2 100644 --- a/public/lucidAuth.login.php +++ b/public/lucidAuth.login.php @@ -6,6 +6,9 @@ if ($_POST['do'] == 'login') { $result = authenticateLDAP($_POST['username'], $_POST['password']); if ($result['status'] == 'Success') { + // Save secure token in cookie + setcookie('JWT', $result['token'], (time() + $settings->Session['Duration'])); + // Convert base64 encoded string back from JSON; // forcing it into an associative array (instead of javascript's default StdClass object) try { @@ -16,7 +19,7 @@ echo '{"Result":"Fail","Reason":"Original request URI lost in transition"}' . PHP_EOL; exit; } - $originalUri = !empty($proxyHeaders) ? $proxyHeaders['XForwardedProto'] . '://' . $proxyHeaders['XForwardedHost'] . $proxyHeaders['XForwardedUri'] : '#'; + $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 echo '{"Result":"Success","Location":"' . $originalUri . '"}' . PHP_EOL; diff --git a/public/lucidAuth.validateRequest.php b/public/lucidAuth.validateRequest.php index 336d594..572aba5 100644 --- a/public/lucidAuth.validateRequest.php +++ b/public/lucidAuth.validateRequest.php @@ -28,9 +28,7 @@ exit; } - if ((!empty($_COOKIE['Exp']) && !empty($_COOKIE['Sub']) && !empty($_COOKIE['JWT'])) && validateToken([ - 'Exp' => $_COOKIE['Exp'], - 'Sub' => $_COOKIE['Sub'], + if (!empty($_COOKIE['JWT']) && validateToken([ 'JWT' => $_COOKIE['JWT'] ])['status'] == "Success") { // Valid authentication token found From ef4c97a78486b7daacbe2097a74349e49250034a Mon Sep 17 00:00:00 2001 From: djpbessems Date: Thu, 24 Jan 2019 19:48:29 +0100 Subject: [PATCH 6/9] Added database queries during login flow --- include/lucidAuth.functions.php | 27 +++++++++++++++++++++------ public/lucidAuth.login.php | 2 +- public/lucidAuth.validateRequest.php | 5 ++++- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/include/lucidAuth.functions.php b/include/lucidAuth.functions.php index 5c1ba8c..b589875 100644 --- a/include/lucidAuth.functions.php +++ b/include/lucidAuth.functions.php @@ -41,6 +41,18 @@ function authenticateLDAP (string $username, string $password) { ]; $secureToken = JWT::encode($jwtPayload, base64_decode($settings->JWT['PrivateKey_base64'])); + // Store 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' => $secureToken, + 'qualifiedusername' => $qualifiedUsername + ]); + return ['status' => 'Success', 'token' => $secureToken]; } else { // LDAP authentication failed! @@ -68,12 +80,12 @@ function validateToken (string $secureToken) { try { $jwtPayload = JWT::decode($secureToken, base64_decode($settings->JWT['PrivateKey_base64']), $settings->JWT['Algorithm']); } catch (Exception $e) { - // Invalid token, inform client (client should handle discarding invalid token) - return ['status' => 'Fail', 'reason' => '3']; + // Invalid token + return ['status' => 'Fail', 'reason' => '1']; } $pdoQuery = $pdoDB->prepare(' - SELECT SecureToken.Payload + SELECT SecureToken.Value FROM SecureToken LEFT JOIN User ON (User.Id=SecureToken.UserId) @@ -83,16 +95,19 @@ function validateToken (string $secureToken) { 'username' => $jwtPayload['sub'] ]); foreach($pdoQuery->fetchAll(PDO::FETCH_ASSOC) as $row) { - $storedTokens[] = $row['Payload']; + $storedTokens[] = $row['Value']; } - print_r($storedTokens); +print_r($storedTokens); # if (!empty($storedTokens) && ) { +# } else { + // No matching token in database +# return ['status' => 'Fail', 'reason' => '2']; # } If ($secureToken['iat'] < (time() - $settings->Session['Duration'])) { - // Expired token (shouldn't the browser disregard it?) + // Expired token return ['status' => 'Fail', 'reason' => '3']; } diff --git a/public/lucidAuth.login.php b/public/lucidAuth.login.php index b8285d2..a2ce736 100644 --- a/public/lucidAuth.login.php +++ b/public/lucidAuth.login.php @@ -7,7 +7,7 @@ $result = authenticateLDAP($_POST['username'], $_POST['password']); if ($result['status'] == 'Success') { // Save secure token in cookie - setcookie('JWT', $result['token'], (time() + $settings->Session['Duration'])); + setcookie('JWT', $result['token'], (time() + $settings->Session['Duration'])); // Convert base64 encoded string back from JSON; // forcing it into an associative array (instead of javascript's default StdClass object) diff --git a/public/lucidAuth.validateRequest.php b/public/lucidAuth.validateRequest.php index 572aba5..3b289f1 100644 --- a/public/lucidAuth.validateRequest.php +++ b/public/lucidAuth.validateRequest.php @@ -35,7 +35,10 @@ header("HTTP/1.1 202 Accepted"); exit; } else { - // No cookie containing valid authentication token found, redirecting to loginpage + // No cookie containing valid authentication token found; + // explicitly deleting any remaining cookie, then redirecting to loginpage + setcookie('JWT', FALSE); + header("HTTP/1.1 401 Unauthorized"); header("Location: lucidAuth.login.php?ref=" . base64_encode(json_encode($proxyHeaders))); } From 0c8b672b412091cd8a5e1560db4fb820cbf46a1c Mon Sep 17 00:00:00 2001 From: djpbessems Date: Mon, 28 Jan 2019 11:48:05 +0100 Subject: [PATCH 7/9] Implemented storage of authentication token in database and cookies (latter are isolated per domain) --- include/lucidAuth.functions.php | 52 +++++++++++++--------------- lucidAuth.config.php.example | 10 +++--- public/lucidAuth.login.php | 23 +++++++++--- public/lucidAuth.validateRequest.php | 5 +-- 4 files changed, 51 insertions(+), 39 deletions(-) diff --git a/include/lucidAuth.functions.php b/include/lucidAuth.functions.php index b589875..7297513 100644 --- a/include/lucidAuth.functions.php +++ b/include/lucidAuth.functions.php @@ -9,7 +9,11 @@ try { # switch ($settings->Database['Driver']) { # case 'sqlite': # $database = new PDO('sqlite:' . $settings->Database['Path']); - $pdoDB = new PDO('sqlite:' . $settings->Sqlite['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) { @@ -41,18 +45,7 @@ function authenticateLDAP (string $username, string $password) { ]; $secureToken = JWT::encode($jwtPayload, base64_decode($settings->JWT['PrivateKey_base64'])); - // Store 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' => $secureToken, - 'qualifiedusername' => $qualifiedUsername - ]); - + return ['status' => 'Success', 'token' => $secureToken]; } else { // LDAP authentication failed! @@ -75,7 +68,7 @@ function retrieveTokenFromDB (string $username, string $foo) { } function validateToken (string $secureToken) { - global $settings; + global $settings, $pdoDB; try { $jwtPayload = JWT::decode($secureToken, base64_decode($settings->JWT['PrivateKey_base64']), $settings->JWT['Algorithm']); @@ -84,6 +77,11 @@ function validateToken (string $secureToken) { return ['status' => 'Fail', 'reason' => '1']; } + if ((int)$jwtPayload->iat < (time() - (int)$settings->Session['Duration'])) { + // Expired token + return ['status' => 'Fail', 'reason' => '3']; + } + $pdoQuery = $pdoDB->prepare(' SELECT SecureToken.Value FROM SecureToken @@ -92,25 +90,25 @@ function validateToken (string $secureToken) { WHERE User.Username = :username '); $pdoQuery->execute([ - 'username' => $jwtPayload['sub'] + ':username' => (string)$jwtPayload->sub ]); foreach($pdoQuery->fetchAll(PDO::FETCH_ASSOC) as $row) { - $storedTokens[] = $row['Value']; + try { + $storedTokens[] = JWT::decode($row['Value'], base64_decode($settings->JWT['PrivateKey_base64']), $settings->JWT['Algorithm']); + } catch (Exception $e) { + continue; + } } - -print_r($storedTokens); -# if (!empty($storedTokens) && ) { -# } else { + 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]; + } else { // No matching token in database -# return ['status' => 'Fail', 'reason' => '2']; -# } - - If ($secureToken['iat'] < (time() - $settings->Session['Duration'])) { - // Expired token - return ['status' => 'Fail', 'reason' => '3']; + return ['status' => 'Fail', 'reason' => '2']; } - } ?> \ No newline at end of file diff --git a/lucidAuth.config.php.example b/lucidAuth.config.php.example index a4b42fd..51a7671 100644 --- a/lucidAuth.config.php.example +++ b/lucidAuth.config.php.example @@ -18,8 +18,6 @@ return (object) array( // Specify the NetBios name of the domain; to allow users to log on with just their usernames. ], - 'DomainNames' => ['*.subdomain.domain.{(tld1|tld2)}'], - 'Sqlite' => [ 'Path' => '../data/lucidAuth.sqlite.db' // Relative path to the location where the database should be stored @@ -30,7 +28,7 @@ return (object) array( 'JWT' => [ 'PrivateKey_base64' => '', - // A base64-encoded string of a random string (see https://www.base64encode.org/) + // A base64-encoded random (preferably long) string (see https://www.base64encode.org/) 'Algorithm' => [ 'HS256', ] @@ -39,7 +37,11 @@ return (object) array( 'Session' => [ 'Duration' => 2592000, // In seconds (2592000 is equivalent to 30 days) -# 'CookiePrefix' => 'lucidAuth_' + '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' => [ diff --git a/public/lucidAuth.login.php b/public/lucidAuth.login.php index a2ce736..f51d157 100644 --- a/public/lucidAuth.login.php +++ b/public/lucidAuth.login.php @@ -3,11 +3,26 @@ include_once('../include/lucidAuth.functions.php'); - if ($_POST['do'] == 'login') { + if ($_POST['do'] === 'login') { $result = authenticateLDAP($_POST['username'], $_POST['password']); - if ($result['status'] == 'Success') { - // Save secure token in cookie - setcookie('JWT', $result['token'], (time() + $settings->Session['Duration'])); + 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) { + return (strlen($value) > strlen($httpHost)) ? false : (0 === substr_compare($httpHost, $value, -strlen($value))); + }))[0]; + setcookie('JWT', $result['token'], (time() + $settings->Session['Duration']), '/', '.' . $cookieDomain); // Convert base64 encoded string back from JSON; // forcing it into an associative array (instead of javascript's default StdClass object) diff --git a/public/lucidAuth.validateRequest.php b/public/lucidAuth.validateRequest.php index 3b289f1..e3ddb66 100644 --- a/public/lucidAuth.validateRequest.php +++ b/public/lucidAuth.validateRequest.php @@ -18,7 +18,6 @@ // For debugging purposes - enable it in ../lucidAuth.config.php if ($settings->Debug['LogToFile']) { 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); - file_put_contents('../requestHeaders.log', (new DateTime())->format('Y-m-d\TH:i:s.u') . ' --+ ' . (base64_encode(json_encode($proxyHeaders))) . PHP_EOL, FILE_APPEND); } # if (sizeof($proxyHeaders) == 0) { @@ -28,9 +27,7 @@ exit; } - if (!empty($_COOKIE['JWT']) && validateToken([ - 'JWT' => $_COOKIE['JWT'] - ])['status'] == "Success") { + if (!empty($_COOKIE['JWT']) && validateToken($_COOKIE['JWT'])['status'] == "Success") { // Valid authentication token found header("HTTP/1.1 202 Accepted"); exit; From ac5f509d4e980630ae5c896da7bd63340790a50a Mon Sep 17 00:00:00 2001 From: djpbessems Date: Mon, 28 Jan 2019 13:47:37 +0100 Subject: [PATCH 8/9] Fixed incorrect syntax (trying to use an object as an associative array) --- include/lucidAuth.functions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/lucidAuth.functions.php b/include/lucidAuth.functions.php index 7297513..e9d9b3a 100644 --- a/include/lucidAuth.functions.php +++ b/include/lucidAuth.functions.php @@ -101,7 +101,7 @@ function validateToken (string $secureToken) { } if (!empty($storedTokens) && sizeof(array_filter($storedTokens, function ($value) use ($jwtPayload) { - return $value['iat'] === $jwtPayload['iat']; + return $value->iat === $jwtPayload->iat; })) === 1) { // At least one of the database-stored tokens match return ['status' => 'Success', 'token' => $jwtPayload]; From 01bb3f33daf19f97cf68850966ed751a9efd6706 Mon Sep 17 00:00:00 2001 From: djpbessems Date: Mon, 28 Jan 2019 13:50:00 +0100 Subject: [PATCH 9/9] Re-enabled '403 Forbidden' result for non-proxied requests --- public/lucidAuth.validateRequest.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/lucidAuth.validateRequest.php b/public/lucidAuth.validateRequest.php index e3ddb66..41ac22e 100644 --- a/public/lucidAuth.validateRequest.php +++ b/public/lucidAuth.validateRequest.php @@ -20,8 +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 (False) { + if (sizeof($proxyHeaders) == 0) { // Non-proxied request; this is senseless, go fetch! header("HTTP/1.1 403 Forbidden"); exit;