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
+
+END;
+} else {
+ $APPS["setup_2fa"]["content"] = ' ' . lang("2fa explained", false) . '
'
+ . ''
+ . lang("enable 2fa", false) . '';
+}
\ No newline at end of file
diff --git a/home.php b/home.php
index af2df6a..c013c59 100644
--- a/home.php
+++ b/home.php
@@ -1,6 +1,176 @@
-Home
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $_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 <<
+
+
+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