diff --git a/action.php b/action.php new file mode 100644 index 0000000..a6a50ab --- /dev/null +++ b/action.php @@ -0,0 +1,57 @@ +select('accounts', 'password', ['uid' => $_SESSION['uid']])[0]); + if ($oldmatch) { + if ($VARS['newpass'] == $VARS['conpass']) { + $passrank = checkWorst500List($VARS['newpass']); + if ($passrank !== FALSE) { + returnToSender("password_500", $passrank); + } + if (strlen($VARS['newpass']) < MIN_PASSWORD_LENGTH) { + returnToSender("weak_password"); + } + $database->update('accounts', ['password' => encryptPassword($VARS['newpass'])], ['uid' => $_SESSION['uid']]); + returnToSender("password_updated"); + } else { + returnToSender("new_password_mismatch"); + } + } else { + returnToSender("old_password_mismatch"); + } + break; + case "add2fa": + if (is_empty($VARS['secret'])) { + returnToSender("invalid_parameters"); + } + $database->update('accounts', ['authsecret' => $VARS['secret']], ['uid' => $_SESSION['uid']]); + returnToSender("2fa_enabled"); + case "rm2fa": + $database->update('accounts', ['authsecret' => ""], ['uid' => $_SESSION['uid']]); + returnToSender("2fa_removed"); + break; +} \ No newline at end of file diff --git a/apps/2fa_qrcode.php b/apps/2fa_qrcode.php new file mode 100644 index 0000000..209cbb1 --- /dev/null +++ b/apps/2fa_qrcode.php @@ -0,0 +1,17 @@ +' + . lang("remove 2fa", false) . ''; +} else { + $APPS["setup_2fa"]["content"] = '
' . lang("2fa explained", false) . '
' + . ''; +} \ No newline at end of file diff --git a/apps/404_error.php b/apps/404_error.php new file mode 100644 index 0000000..7d88cca --- /dev/null +++ b/apps/404_error.php @@ -0,0 +1,9 @@ +" . lang("page not found", false) . ""; +?> \ No newline at end of file diff --git a/apps/change_password.php b/apps/change_password.php new file mode 100644 index 0000000..b330fad --- /dev/null +++ b/apps/change_password.php @@ -0,0 +1,17 @@ + + + + + + +
+ + +CONTENTEND; diff --git a/apps/sample_app.php b/apps/sample_app.php new file mode 100644 index 0000000..b517ac0 --- /dev/null +++ b/apps/sample_app.php @@ -0,0 +1,20 @@ + +
+ Item 1 +
+
+ Item 2 +
+
+ Item 3 +
+ +CONTENTEND; +?> \ No newline at end of file diff --git a/apps/setup_2fa.php b/apps/setup_2fa.php new file mode 100644 index 0000000..3193e06 --- /dev/null +++ b/apps/setup_2fa.php @@ -0,0 +1,43 @@ + ' . lang("2fa active", false) . '' + . '' + . lang("remove 2fa", false) . ''; +} else if ($_GET['2fa'] == "generate") { + $codeuri = newTOTP($_SESSION['username']); + $qrCode = new QrCode($codeuri); + $qrCode->setSize(200); + $qrCode->setErrorCorrection("H"); + $qrcode = $qrCode->getDataUri(); + $totp = Factory::loadFromProvisioningUri($codeuri); + $codesecret = $totp->getSecret(); + $chunk_secret = trim(chunk_split($codesecret, 8, ' ')); + $APPS["setup_2fa"]["content"] = '
' . lang("scan 2fa qrcode", false) . '
' . << +
$chunk_secret
+
+ + + +
+ + + $_SESSION['realname']]); + ?> + + + + "; + lang(PAGES[$pageid]['title']); + ?> + + + + + + + + $_GET['arg']], false); + } + $alerttype = MESSAGES[$_GET['msg']]['type']; + $alerticon = "square-o"; + switch (MESSAGES[$_GET['msg']]['type']) { + case "danger": + $alerticon = "times"; + break; + case "warning": + $alerticon = "exclamation-triangle"; + break; + case "info": + $alerticon = "info-circle"; + break; + case "success": + $alerticon = "check"; + break; + } + echo << +
+
+ + $alertmsg +
+
+ +END; + } + ?> +
+ + + + + +
+
+

$apptitle

+
+
+ $appcontent +
+
+
+END; + } + } + ?> + + + + + + + + \ No newline at end of file diff --git a/index.php b/index.php index e962076..236705c 100644 --- a/index.php +++ b/index.php @@ -106,6 +106,10 @@ if ($VARS['progress'] == "1") { + diff --git a/lang/en_us.php b/lang/en_us.php index d1fda28..0d59c14 100644 --- a/lang/en_us.php +++ b/lang/en_us.php @@ -11,5 +11,29 @@ define("STRINGS", [ "login incorrect" => "Login incorrect.", "account locked" => "This account has been disabled. Contact technical support.", "password expired" => "You must change your password before continuing.", - "account terminated" => "Account terminated. Access denied." + "account terminated" => "Account terminated. Access denied.", + "password on 500 list" => "The given password is ranked number {arg} out of the 500 most common passwords. Try a different one.", + "welcome user" => "Welcome, {user}!", + "change password" => "Change password", + "security options" => "Security options", + "account security" => "Account security", + "sign out" => "Sign out", + "settings" => "Settings", + "404 error" => "404 Error", + "page not found" => "Page not found.", + "current password incorrect" => "The current password is incorrect. Try again.", + "new password mismatch" => "The new passwords did not match. Try again.", + "weak password" => "Password does not meet requirements.", + "password updated" => "Password updated successfully.", + "setup 2fa" => "Setup 2-factor authentication", + "2fa removed" => "2-factor authentication disabled.", + "2fa enabled" => "2-factor authentication activated.", + "remove 2fa" => "Disable 2-factor authentication", + "2fa explained" => "2-factor authentication adds more security to your account. You'll need an app such as Google Authenticator on your smartphone. When you have the app installed, you can enable 2-factor authentication by clicking the button below and scanning a QR code with the app. Whenever you sign in in the future, you'll need to input a six-digit code from your phone into the login page when prompted. You can disable 2-factor authentication from this page if you change your mind.", + "2fa active" => "2-factor authentication is active on your account. To remove 2fa, reset your authentication secret, or change to a new security device, click the button below.", + "enable 2fa" => "Enable 2-factor authentication", + "scan 2fa qrcode" => "Scan the QR Code with the authenticator app, or enter the secret key manually.", + "confirm 2fa" => "Finish setup", + "invalid parameters" => "Invalid request parameters.", + "home" => "Home", ]); \ No newline at end of file diff --git a/lang/messages.php b/lang/messages.php new file mode 100644 index 0000000..d597cb6 --- /dev/null +++ b/lang/messages.php @@ -0,0 +1,36 @@ + [ + "string" => "current password incorrect", + "type" => "danger" + ], + "new_password_mismatch" => [ + "string" => "new password mismatch", + "type" => "danger" + ], + "weak_password" => [ + "string" => "weak password", + "type" => "danger" + ], + "password_updated" => [ + "string" => "password updated", + "type" => "success" + ], + "2fa_removed" => [ + "string" => "2fa removed", + "type" => "success" + ], + "2fa_enabled" => [ + "string" => "2fa enabled", + "type" => "success" + ], + "invalid_parameters" => [ + "string" => "invalid parameters", + "type" => "danger" + ], + "password_500" => [ + "string" => "password on 500 list", + "type" => "danger" + ] +]); diff --git a/lib/login.php b/lib/login.php index 13f19a6..b80d245 100644 --- a/lib/login.php +++ b/lib/login.php @@ -43,7 +43,7 @@ function userHasTOTP($username) { } /** - * Generate and store a TOTP secret for the given user. + * Generate a TOTP secret for the given user. * @param string $username * @return string OTP provisioning URI (for generating a QR code) */ @@ -54,10 +54,20 @@ function newTOTP($username) { $userdata = $database->select('accounts', ['email', 'authsecret'], ['username' => $username])[0]; $totp = new TOTP($userdata['email'], $encoded_secret); $totp->setIssuer(SYSTEM_NAME); - $database->update('accounts', ['authsecret' => $encoded_secret], ['username' => $username]); return $totp->getProvisioningUri(); } +/** + * Save a TOTP secret for the user. + * @global $database $database + * @param string $username + * @param string $secret + */ +function saveTOTP($username, $secret) { + global $database; + $database->update('accounts', ['authsecret' => $secret], ['username' => $username]); +} + /** * Verify a TOTP multiauth code * @global $database @@ -72,6 +82,5 @@ function verifyTOTP($username, $code) { return false; } $totp = new TOTP(null, $userdata['authsecret']); - echo $userdata['authsecret'] . ", " . $totp->now() . ", " . $code; return $totp->verify($code); } diff --git a/lib/worst_passwords.php b/lib/worst_passwords.php new file mode 100644 index 0000000..049e42f --- /dev/null +++ b/lib/worst_passwords.php @@ -0,0 +1,522 @@ + [ + "title" => "{DEFAULT}" + ], + "security" => [ + "title" => "security options" + ], + "404" => [ + "title" => "404 error" + ] +]); + +// Which apps to load on a given page +define("APPS", [ + "home" => [ + "sample_app" + ], + "security" => [ + "change_password", + "setup_2fa" + ], + "404" => [ + "404_error" + ] +]); diff --git a/required.php b/required.php index 6b35985..30b3b6d 100644 --- a/required.php +++ b/required.php @@ -13,6 +13,8 @@ require __DIR__ . '/vendor/autoload.php'; // Settings file require __DIR__ . '/settings.php'; +require __DIR__ . '/lang/messages.php'; + require __DIR__ . '/lang/' . LANGUAGE . ".php"; function sendError($error) { @@ -67,13 +69,44 @@ function is_empty($str) { return (is_null($str) || !isset($str) || $str == ''); } +/** + * I18N string getter. If the key doesn't exist, outputs the key itself. + * @param string $key I18N string key + * @param boolean $echo whether to echo the result or return it (default echo) + */ function lang($key, $echo = true) { if (array_key_exists($key, STRINGS)) { $str = STRINGS[$key]; } else { $str = $key; } - + + if ($echo) { + echo $str; + } else { + return $str; + } +} + +/** + * I18N string getter (with builder). If the key doesn't exist, outputs the key itself. + * @param string $key I18N string key + * @param array $replace key-value array of replacements. + * If the string value is "hello {abc}" and you give ["abc" => "123"], the + * result will be "hello 123". + * @param boolean $echo whether to echo the result or return it (default echo) + */ +function lang2($key, $replace, $echo = true) { + if (array_key_exists($key, STRINGS)) { + $str = STRINGS[$key]; + } else { + $str = $key; + } + + foreach ($replace as $find => $repl) { + $str = str_replace("{" . $find . "}", $repl, $str); + } + if ($echo) { echo $str; } else { diff --git a/settings.template.php b/settings.template.php index 0d1acd9..dcfa92e 100644 --- a/settings.template.php +++ b/settings.template.php @@ -21,8 +21,6 @@ define("SITE_TITLE", "Netsyms Business Apps :: Single Sign On"); // Used to identify the system in OTP and other places define("SYSTEM_NAME", "Netsyms SSO Demo"); -define("COPYRIGHT_NAME", "Netsyms Technologies"); - // For supported values, see http://php.net/manual/en/timezones.php define("TIMEZONE", "America/Denver"); @@ -32,5 +30,25 @@ define('URL', 'http://localhost:8000/'); // See lang folder for language options define('LANGUAGE', "en_us"); +// Minimum length for new passwords +// The system checks new passwords against the 500 worst passwords and rejects +// any matches. +// If you want to have additional password requirements, go edit action.php. +// However, all that does is encourage people to use the infamous +// "post-it password manager". See also https://xkcd.com/936/ and +// http://stackoverflow.com/a/34166252/2534036 for reasons why forcing passwords +// like CaPs45$% is not actually a great idea. +// Encourage users to use 2-factor auth whenever possible. +define("MIN_PASSWORD_LENGTH", 8); + // Maximum number of rows to get in a query. -define("QUERY_LIMIT", 1000); \ No newline at end of file +define("QUERY_LIMIT", 1000); + + + +/////////////////////////////////////////////////////////////////////////////////////////////// +// /!\ Warning: Changing these values may violate the terms of your license agreement! /!\ // +/////////////////////////////////////////////////////////////////////////////////////////////// +define("LICENSE_TEXT", "Unlicensed Demo: For Trial Use Only"); +define("COPYRIGHT_NAME", "Netsyms Technologies"); +///////////////////////////////////////////////////////////////////////////////////////////// \ No newline at end of file diff --git a/static/css/app.css b/static/css/app.css index 42c3e0b..1bec671 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -1,3 +1,24 @@ .banner-image { - margin: 2em 0em; + max-height: 100px; + margin: 2em auto; +} + +.navbar-brand { + font-size: 110%; +} + +.footer { + margin-top: 10em; + text-align: center; +} + +.qrcode { + width: 100%; + max-width: 300px; + margin: 0 auto; + image-rendering: -moz-crisp-edges; /* Firefox */ + image-rendering: -o-crisp-edges; /* Opera */ + image-rendering: -webkit-optimize-contrast;/* Webkit (non-standard naming) */ + image-rendering: crisp-edges; + -ms-interpolation-mode: nearest-neighbor; /* IE (non-standard property) */ } \ No newline at end of file diff --git a/static/js/app.js b/static/js/app.js new file mode 100644 index 0000000..6f16ebe --- /dev/null +++ b/static/js/app.js @@ -0,0 +1,7 @@ + +$(document).ready(function () { + /* Fade out alerts */ + $(".alert .close").click(function (e) { + $(this).parent().fadeOut('slow'); + }); +}); \ No newline at end of file