From 74971a4592385ae1b53d6ab1320d06b024a4f512 Mon Sep 17 00:00:00 2001 From: Skylar Ittner Date: Sat, 22 Dec 2018 16:56:25 -0700 Subject: [PATCH] Add new more flexible login flow --- api/actions/checkloginkey.php | 15 ++ api/actions/getloginkey.php | 17 ++ api/apisettings.php | 12 ++ database.mwb | Bin 22644 -> 22596 bytes database_upgrade/2.1_3.0.sql | 28 ++++ index.php | 288 ++++++++------------------------ langs/en/login.json | 12 ++ langs/en/titles.json | 3 +- lib/LoginKeys.lib.php | 33 ++++ login/index.php | 150 +++++++++++++++++ login/parts/change_password.php | 51 ++++++ login/parts/footer.php | 15 ++ login/parts/header.php | 44 +++++ login/parts/password.php | 29 ++++ login/parts/totp.php | 28 ++++ login/parts/username.php | 28 ++++ static/css/login.css | 11 ++ 17 files changed, 542 insertions(+), 222 deletions(-) create mode 100644 api/actions/checkloginkey.php create mode 100644 api/actions/getloginkey.php create mode 100644 database_upgrade/2.1_3.0.sql create mode 100644 langs/en/login.json create mode 100644 lib/LoginKeys.lib.php create mode 100644 login/index.php create mode 100644 login/parts/change_password.php create mode 100644 login/parts/footer.php create mode 100644 login/parts/header.php create mode 100644 login/parts/password.php create mode 100644 login/parts/totp.php create mode 100644 login/parts/username.php create mode 100644 static/css/login.css diff --git a/api/actions/checkloginkey.php b/api/actions/checkloginkey.php new file mode 100644 index 0000000..8f70c47 --- /dev/null +++ b/api/actions/checkloginkey.php @@ -0,0 +1,15 @@ + "OK", "uid" => $uid]); +} catch (Exception $ex) { + sendJsonResp("", "ERROR"); +} diff --git a/api/actions/getloginkey.php b/api/actions/getloginkey.php new file mode 100644 index 0000000..166b488 --- /dev/null +++ b/api/actions/getloginkey.php @@ -0,0 +1,17 @@ + "OK", "code" => $code, "loginurl" => $url]); diff --git a/api/apisettings.php b/api/apisettings.php index b8b8222..043b339 100644 --- a/api/apisettings.php +++ b/api/apisettings.php @@ -212,4 +212,16 @@ $APIS = [ "id" => "numeric" ] ], + "getloginkey" => [ + "load" => "getloginkey.php", + "vars" => [ + "appname" => "string" + ] + ], + "checkloginkey" => [ + "load" => "checkloginkey.php", + "vars" => [ + "code" => "string" + ] + ] ]; diff --git a/database.mwb b/database.mwb index 4afbe0bf94b7437f72c9c22955ca7fa0cc870d0f..9479ae42bac1a54a9e948a502638eda7fce15ce4 100644 GIT binary patch delta 21348 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 delta 21455 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 diff --git a/database_upgrade/2.1_3.0.sql b/database_upgrade/2.1_3.0.sql new file mode 100644 index 0000000..0992672 --- /dev/null +++ b/database_upgrade/2.1_3.0.sql @@ -0,0 +1,28 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +DROP TABLE IF EXISTS `available_apps`; +DROP TABLE IF EXISTS `apps`; + +CREATE TABLE IF NOT EXISTS `userloginkeys` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `key` VARCHAR(255) NOT NULL, + `expires` DATETIME NULL DEFAULT NULL, + `uid` INT(11) NULL DEFAULT NULL, + PRIMARY KEY (`id`, `key`), + UNIQUE INDEX `id_UNIQUE` (`id` ASC), + UNIQUE INDEX `key_UNIQUE` (`key` ASC), + INDEX `fk_userloginkeys_accounts1_idx` (`uid` ASC), + CONSTRAINT `fk_userloginkeys_accounts1` + FOREIGN KEY (`uid`) + REFERENCES `accounts` (`uid`) + ON DELETE NO ACTION + ON UPDATE NO ACTION) +ENGINE = InnoDB +DEFAULT CHARACTER SET = utf8; + +ALTER TABLE `userloginkeys` +ADD COLUMN `appname` VARCHAR(255) NOT NULL AFTER `uid`; \ No newline at end of file diff --git a/index.php b/index.php index 414accd..a922a89 100644 --- a/index.php +++ b/index.php @@ -1,238 +1,84 @@ exists()) { - $status = $user->getStatus()->getString(); - switch ($status) { - case "LOCKED_OR_DISABLED": - $alert = $Strings->get("account locked", false); - break; - case "TERMINATED": - $alert = $Strings->get("account terminated", false); - break; - case "CHANGE_PASSWORD": - $alert = $Strings->get("password expired", false); - $alerttype = "info"; - $_SESSION['username'] = $user->getUsername(); - $_SESSION['uid'] = $user->getUID(); - $change_password = true; - break; - case "NORMAL": - $username_ok = true; - break; - case "ALERT_ON_ACCESS": - $mail_resp = $user->sendAlertEmail(); - if ($SETTINGS['debug']) { - var_dump($mail_resp); - } - $username_ok = true; - break; - default: - if (!empty($error)) { - $alert = $error; - } else { - $alert = $Strings->get("login error", false); - } - break; - } - if ($username_ok) { - if ($user->checkPassword($VARS['password'])) { - $_SESSION['passok'] = true; // stop logins using only username and authcode - if ($user->has2fa()) { - $multiauth = true; - } else { - Session::start($user); - Log::insert(LogType::LOGIN_OK, $user->getUID()); - header('Location: app.php'); - die("Logged in, go to app.php"); - } - } else { - $alert = $Strings->get("login incorrect", false); - Log::insert(LogType::LOGIN_FAILED, null, "Username: " . $VARS['username']); - } - } - } else { // User does not exist anywhere - $alert = $Strings->get("login incorrect", false); - Log::insert(LogType::LOGIN_FAILED, null, "Username: " . $VARS['username']); - } - } else { - $alert = $Strings->get("captcha error", false); - Log::insert(LogType::BAD_CAPTCHA, null, "Username: " . $VARS['username']); - } -} else if ($VARS['progress'] == "2") { - engageRateLimit(); - $user = User::byUsername($VARS['username']); - if ($_SESSION['passok'] !== true) { - // stop logins using only username and authcode - sendError("Password integrity check failed!"); - } - if ($user->check2fa($VARS['authcode'])) { - Session::start($user); - Log::insert(LogType::LOGIN_OK, $user->getUID()); - header('Location: app.php'); - die("Logged in, go to app.php"); - } else { - $alert = $Strings->get("2fa incorrect", false); - Log::insert(LogType::BAD_2FA, null, "Username: " . $VARS['username']); - } -} else if ($VARS['progress'] == "chpasswd") { - engageRateLimit(); - if (!empty($_SESSION['username'])) { - $user = User::byUsername($_SESSION['username']); - - try { - $result = $user->changePassword($VARS['oldpass'], $VARS['newpass'], $VARS['conpass']); - - if ($result === TRUE) { - $alert = $Strings->get(MESSAGES["password_updated"]["string"], false); - $alerttype = MESSAGES["password_updated"]["type"]; - } - } catch (PasswordMatchException $e) { - $alert = $Strings->get(MESSAGES["passwords_same"]["string"], false); - $alerttype = "danger"; - } catch (PasswordMismatchException $e) { - $alert = $Strings->get(MESSAGES["new_password_mismatch"]["string"], false); - $alerttype = "danger"; - } catch (IncorrectPasswordException $e) { - $alert = $Strings->get(MESSAGES["old_password_mismatch"]["string"], false); - $alerttype = "danger"; - } catch (WeakPasswordException $e) { - $alert = $Strings->get(MESSAGES["weak_password"]["string"], false); - $alerttype = "danger"; - } - } else { - session_destroy(); - header('Location: index.php'); - die(); - } -} +if (!empty($_GET['logout'])) { + // Show a logout message instead of immediately redirecting to login flow + ?> + + + + -header("Link: ; rel=preload; as=style", false); -header("Link: ; rel=preload; as=style", false); -header("Link: ; rel=preload; as=style", false); -header("Link: ; rel=preload; as=style", false); -header("Link: ; rel=preload; as=script", false); -header("Link: ; rel=preload; as=script", false); -?> - - - - - - + <?php echo $SETTINGS['site_title']; ?> - <?php echo $SETTINGS['site_title']; ?> + - + + + - - - - - - - - +
-
- +
+

get("You have been logged out.") ?>

-
-
- + + + getMessage()); + } +} \ No newline at end of file diff --git a/langs/en/login.json b/langs/en/login.json new file mode 100644 index 0000000..9add11c --- /dev/null +++ b/langs/en/login.json @@ -0,0 +1,12 @@ +{ + "Login to {app}": "Login to {app}", + "Username not found.": "Username not found.", + "Password for {user}": "Password for {user}", + "Password incorrect.": "Password incorrect.", + "Two-factor code": "Two-factor code", + "Code incorrect.": "Code incorrect.", + "Current password for {user}": "Current password for {user}", + "New password": "New password", + "New password (again)": "New password (again)", + "Fill in all three boxes.": "Fill in all three boxes." +} diff --git a/langs/en/titles.json b/langs/en/titles.json index ea261ca..d1be568 100644 --- a/langs/en/titles.json +++ b/langs/en/titles.json @@ -4,5 +4,6 @@ "account options": "Account options", "sync": "Sync settings", "settings": "Settings", - "account": "Account" + "account": "Account", + "Home": "Home" } diff --git a/lib/LoginKeys.lib.php b/lib/LoginKeys.lib.php new file mode 100644 index 0000000..d445568 --- /dev/null +++ b/lib/LoginKeys.lib.php @@ -0,0 +1,33 @@ +has('userloginkeys', ['key' => $code])); + + $database->insert('userloginkeys', ['key' => $code, 'expires' => date("Y-m-d H:i:s", time() + 600), 'appname' => $appname]); + + return $code; + } + + public static function getuid(string $code): int { + global $database; + if (!$database->has('userloginkeys', ["AND" => ['key' => $code, 'uid[!]' => null]])) { + throw new Exception(); + } + + $uid = $database->get('userloginkeys', 'uid', ['key' => $code]); + + return $uid; + } + +} diff --git a/login/index.php b/login/index.php new file mode 100644 index 0000000..2aaca5d --- /dev/null +++ b/login/index.php @@ -0,0 +1,150 @@ +delete("userloginkeys", ["expires[<]" => date("Y-m-d H:i:s")]); + +if (!$database->has("userloginkeys", ["AND" => ["key" => $_GET["code"]], "expires[>]" => date("Y-m-d H:i:s"), "uid" => null])) { + header("Location: $_GET[redirect]"); + die("Invalid auth code."); +} + +$APPNAME = $database->get("userloginkeys", "appname", ["key" => $_GET["code"]]); + +if (empty($_SESSION['thisstep'])) { + $_SESSION['thisstep'] = "username"; +} + +$error = ""; + +function sendUserBack($code, $url, $uid) { + global $database; + $database->update("userloginkeys", ["uid" => $uid], ["key" => $code]); + header("Location: $url"); + die("Click here"); +} + +if (!empty($_SESSION['check'])) { + switch ($_SESSION['check']) { + case "username": + if (empty($_POST['username'])) { + $_SESSION['thisstep'] = "username"; + break; + } + $user = User::byUsername($_POST['username']); + if ($user->exists()) { + $_SESSION['login_uid'] = $user->getUID(); + switch ($user->getStatus()->get()) { + case AccountStatus::LOCKED_OR_DISABLED: + $error = $Strings->get("account locked", false); + break; + case AccountStatus::TERMINATED: + $error = $Strings->get("account terminated", false); + break; + case AccountStatus::ALERT_ON_ACCESS: + $mail_resp = $user->sendAlertEmail(); + case AccountStatus::NORMAL: + $_SESSION['thisstep'] = "password"; + break; + case AccountStatus::CHANGE_PASSWORD: + $_SESSION['thisstep'] = "change_password"; + break; + } + } else { + $error = $Strings->get("Username not found.", false); + } + break; + case "password": + if (empty($_POST['password'])) { + $_SESSION['thisstep'] = "password"; + break; + } + if (empty($_SESSION['login_uid'])) { + $_SESSION['thisstep'] = "username"; + break; + } + $user = new User($_SESSION['login_uid']); + if ($user->checkPassword($_POST['password'])) { + $_SESSION['login_pwd'] = true; + if ($user->has2fa()) { + $_SESSION['thisstep'] = "totp"; + } else { + sendUserBack($_GET['code'], $_GET['redirect'], $_SESSION['login_uid']); + } + } else { + $error = $Strings->get("Password incorrect.", false); + } + break; + case "change_password": + if (empty($_POST['oldpassword']) || empty($_POST['newpassword']) || empty($_POST['newpassword2'])) { + $_SESSION['thisstep'] = "change_password"; + $error = $Strings->get("Fill in all three boxes.", false); + break; + } + + $user = new User($_SESSION['login_uid']); + try { + $result = $user->changePassword($_POST['oldpassword'], $_POST['newpassword'], $_POST['newpassword2']); + + if ($result === TRUE) { + if ($user->has2fa()) { + $_SESSION['thisstep'] = "totp"; + } else { + sendUserBack($_GET['code'], $_GET['redirect'], $_SESSION['login_uid']); + } + } + } catch (PasswordMatchException $e) { + $error = $Strings->get(MESSAGES["passwords_same"]["string"], false); + } catch (PasswordMismatchException $e) { + $error = $Strings->get(MESSAGES["new_password_mismatch"]["string"], false); + } catch (IncorrectPasswordException $e) { + $error = $Strings->get(MESSAGES["old_password_mismatch"]["string"], false); + } catch (WeakPasswordException $e) { + $error = $Strings->get(MESSAGES["weak_password"]["string"], false); + } + break; + case "totp": + if (empty($_POST['totp']) || empty($_SESSION['login_uid'])) { + $_SESSION['thisstep'] = "username"; + break; + } + $user = new User($_SESSION['login_uid']); + if ($user->check2fa($_POST['totp'])) { + sendUserBack($_GET['code'], $_GET['redirect'], $_SESSION['login_uid']); + } else { + $error = $Strings->get("Code incorrect.", false); + } + break; + } +} + +include __DIR__ . "/parts/header.php"; + +switch ($_SESSION['thisstep']) { + case "username": + require __DIR__ . "/parts/username.php"; + break; + case "password": + require __DIR__ . "/parts/password.php"; + break; + case "change_password": + require __DIR__ . "/parts/change_password.php"; + break; + case "totp": + require __DIR__ . "/parts/totp.php"; + break; +} + +include __DIR__ . "/parts/footer.php"; diff --git a/login/parts/change_password.php b/login/parts/change_password.php new file mode 100644 index 0000000..2402bb9 --- /dev/null +++ b/login/parts/change_password.php @@ -0,0 +1,51 @@ +getUsername(); +?> + +
+
+ get("password expired"); ?> +
+
+ +
+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
\ No newline at end of file diff --git a/login/parts/footer.php b/login/parts/footer.php new file mode 100644 index 0000000..2896d7f --- /dev/null +++ b/login/parts/footer.php @@ -0,0 +1,15 @@ + + +
+
+
+ + + + \ No newline at end of file diff --git a/login/parts/header.php b/login/parts/header.php new file mode 100644 index 0000000..edf11e6 --- /dev/null +++ b/login/parts/header.php @@ -0,0 +1,44 @@ +; rel=preload; as=style", false); +header("Link: <../static/css/bootstrap.min.css>; rel=preload; as=style", false); +header("Link: <../static/css/login.css>; rel=preload; as=style", false); +header("Link: <../static/css/svg-with-js.min.css>; rel=preload; as=style", false); +header("Link: <../static/js/fontawesome-all.min.js>; rel=preload; as=script", false); +?> + + + + + +<?php echo $SETTINGS['site_title']; ?> + + + + + + + +
+
+
+

build("Login to {app}", ["app" => htmlentities($APPNAME)]); ?>

+
+ +
+
+
+ +
+ +
+ \ No newline at end of file diff --git a/login/parts/password.php b/login/parts/password.php new file mode 100644 index 0000000..a35a34c --- /dev/null +++ b/login/parts/password.php @@ -0,0 +1,29 @@ +getUsername(); +?> + +
+
+ +
+
+ +
+ +
+ Enter your password. +
+ +
+ +
+
\ No newline at end of file diff --git a/login/parts/totp.php b/login/parts/totp.php new file mode 100644 index 0000000..b3e8800 --- /dev/null +++ b/login/parts/totp.php @@ -0,0 +1,28 @@ + + +
+
+ +
+
+ +
+ +
+ Enter the two-factor code from your mobile device. +
+ +
+ +
+
\ No newline at end of file diff --git a/login/parts/username.php b/login/parts/username.php new file mode 100644 index 0000000..5d71061 --- /dev/null +++ b/login/parts/username.php @@ -0,0 +1,28 @@ + + +
+
+ +
+
+ +
+ +
+ Enter your username. +
+ +
+ +
+
\ No newline at end of file diff --git a/static/css/login.css b/static/css/login.css new file mode 100644 index 0000000..8b68a67 --- /dev/null +++ b/static/css/login.css @@ -0,0 +1,11 @@ +/* +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +.display-5 { + font-size: 3rem; + font-weight: 300; + line-height: 1.2; +} \ No newline at end of file