diff --git a/include/lucidAuth.functions.php b/include/lucidAuth.functions.php index f187dae..e9d9b3a 100644 --- a/include/lucidAuth.functions.php +++ b/include/lucidAuth.functions.php @@ -1,10 +1,24 @@ 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'])); } -$settings = include_once('../lucidAuth.config.php'); function authenticateLDAP (string $username, string $password) { global $settings; @@ -15,22 +29,23 @@ function authenticateLDAP (string $username, string $password) { $ds = ldap_connect($settings->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'])); + return ['status' => 'Success', 'token' => $secureToken]; } else { // LDAP authentication failed! @@ -45,20 +60,54 @@ 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'] - global $settings; +function validateToken (string $secureToken) { + global $settings, $pdoDB; - If ($expiration > time()) { - #moo + try { + $jwtPayload = JWT::decode($secureToken, base64_decode($settings->JWT['PrivateKey_base64']), $settings->JWT['Algorithm']); + } catch (Exception $e) { + // Invalid token + 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 + LEFT JOIN User + ON (User.Id=SecureToken.UserId) + WHERE User.Username = :username + '); + $pdoQuery->execute([ + ':username' => (string)$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']); + } catch (Exception $e) { + continue; + } + } + + 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']; } } diff --git a/include/lucidAuth.template.php b/include/lucidAuth.template.php index d972ec6..95bddc7 100644 --- a/include/lucidAuth.template.php +++ b/include/lucidAuth.template.php @@ -69,7 +69,8 @@ $contentLayout['login'] = <<
  • - + +
  •   @@ -79,7 +80,7 @@ $contentLayout['login'] = << - PVR [Secure] + Secure! LOGIN; $contentLayout['manage'] = << ['*.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 ], @@ -29,16 +27,21 @@ return (object) array( // File containing your token 'JWT' => [ - 'PrivateKey_base64' => 'result of base64_encode()', + 'PrivateKey_base64' => '', + // A base64-encoded random (preferably long) string (see https://www.base64encode.org/) 'Algorithm' => [ 'HS256', ] ], - 'Cookie' => [ - 'Duration' => 2592000, + 'Session' => [ + 'Duration' => 2592000, // In seconds (2592000 is equivalent to 30 days) -# 'Prefix' => '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/images/bg_header.gif b/public/images/bg_header.gif index bec2069..912cd4c 100644 Binary files a/public/images/bg_header.gif and b/public/images/bg_header.gif differ diff --git a/public/images/tag_lock.png b/public/images/tag_lock.png new file mode 100644 index 0000000..354e583 Binary files /dev/null and b/public/images/tag_lock.png differ diff --git a/public/images/tag_pvr.png b/public/images/tag_pvr.png deleted file mode 100644 index 1a58550..0000000 Binary files a/public/images/tag_pvr.png and /dev/null differ diff --git a/public/lucidAuth.login.php b/public/lucidAuth.login.php index 3dc7af2..f51d157 100644 --- a/public/lucidAuth.login.php +++ b/public/lucidAuth.login.php @@ -3,13 +3,41 @@ include_once('../include/lucidAuth.functions.php'); - echo $settings->Debug['Verbose']; - - if ($_POST['do'] == 'login') { + if ($_POST['do'] === 'login') { $result = authenticateLDAP($_POST['username'], $_POST['password']); - if ($result['status'] == 'Success') { + 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) + 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 = !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":""}' . 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..41ac22e 100644 --- a/public/lucidAuth.validateRequest.php +++ b/public/lucidAuth.validateRequest.php @@ -16,25 +16,27 @@ }, 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); + } -# 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; } -# 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['JWT']) && validateToken($_COOKIE['JWT'])['status'] == "Success") { // Valid authentication token found header("HTTP/1.1 202 Accepted"); exit; + } else { + // 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))); } ?> \ 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 { 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;