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 4afbe0b..9479ae4 100644 Binary files a/database.mwb and b/database.mwb differ 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.") ?>

-
-
-
-
-
get("sign in"); ?>
-
- -
- - -
- - " required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus />
- " required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" />
- -
-
- - - -
- get("2fa prompt"); ?> -
- " required="required" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" autofocus />
- - - -
-
-
- - - -
+
- - - - - \ No newline at end of file +
+ + + 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