Browse Source

Add app passwords (close #15)

master
Skylar Ittner 8 months ago
parent
commit
22fb97d0c4

+ 10
- 1
api/actions/auth.php View File

@@ -7,7 +7,16 @@
*/

$user = User::byUsername($VARS['username']);
if ($user->checkPassword($VARS['password'])) {

$ok = false;
if (empty($VARS['apppass']) && ($user->checkPassword($VARS['password']) || $user->checkAppPassword($VARS['password']))) {
$ok = true;
} else {
if ((!$user->has2fa() && $user->checkPassword($VARS['password'])) || $user->checkAppPassword($VARS['password'])) {
$ok = true;
}
}
if ($ok) {
Log::insert(LogType::API_AUTH_OK, null, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
sendJsonResp($Strings->get("login successful", false), "OK");
} else {

+ 1
- 1
api/actions/login.php View File

@@ -8,7 +8,7 @@

engageRateLimit();
$user = User::byUsername($VARS['username']);
if ($user->checkPassword($VARS['password'])) {
if ((!$user->has2fa() && $user->checkPassword($VARS['password'])) || $user->checkAppPassword($VARS['password'])) {
switch ($user->getStatus()->getString()) {
case "LOCKED_OR_DISABLED":
Log::insert(LogType::API_LOGIN_FAILED, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());

+ 2
- 1
api/apisettings.php View File

@@ -19,7 +19,8 @@ $APIS = [
"load" => "auth.php",
"vars" => [
"username" => "string",
"password" => "string"
"password" => "string",
"apppass (optional)" => "/[0-1]/"
],
"keytype" => "AUTH"
],

+ 6
- 4
api/functions.php View File

@@ -90,11 +90,13 @@ function checkVars($vars, $or = false) {
continue;
}
}
$checkmethod = "is_$val";
if ($checkmethod($VARS[$key]) !== true) {
$ok[$key] = false;

if (strpos($val, "/") === 0) {
// regex
$ok[$key] = preg_match($val, $VARS[$key]) === 1;
} else {
$ok[$key] = true;
$checkmethod = "is_$val";
$ok[$key] = !($checkmethod($VARS[$key]) !== true);
}
}
if ($or) {

BIN
database.mwb View File


+ 20
- 0
database.sql View File

@@ -1,5 +1,5 @@
-- MySQL Script generated by MySQL Workbench
-- Mon 11 Feb 2019 04:07:57 PM MST
-- Model: New Model Version: 1.0
-- MySQL Workbench Forward Engineering

@@ -335,6 +335,25 @@ CREATE TABLE IF NOT EXISTS `userloginkeys` (
ENGINE = InnoDB;


-- -----------------------------------------------------
-- Table `apppasswords`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `apppasswords` (
`passid` INT NOT NULL AUTO_INCREMENT,
`hash` VARCHAR(255) NOT NULL,
`uid` INT NOT NULL,
`description` VARCHAR(255) NOT NULL,
PRIMARY KEY (`passid`, `uid`),
UNIQUE INDEX `passid_UNIQUE` (`passid` ASC),
INDEX `fk_apppasswords_accounts1_idx` (`uid` ASC),
CONSTRAINT `fk_apppasswords_accounts1`
FOREIGN KEY (`uid`)
REFERENCES `accounts` (`uid`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB;


SET SQL_MODE=@OLD_SQL_MODE;
SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;
SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS;

+ 17
- 1
database_upgrade/2.1_2.2.sql View File

@@ -29,4 +29,20 @@ ADD COLUMN `appname` VARCHAR(255) NOT NULL AFTER `uid`;
ALTER TABLE `userloginkeys`
ADD COLUMN `appicon` TINYTEXT NULL DEFAULT NULL AFTER `appname`;
ALTER TABLE `apikeys`
ADD COLUMN `type` VARCHAR(45) NOT NULL DEFAULT 'FULL' AFTER `notes`;
ADD COLUMN `type` VARCHAR(45) NOT NULL DEFAULT 'FULL' AFTER `notes`;

CREATE TABLE IF NOT EXISTS `apppasswords` (
`passid` INT(11) NOT NULL AUTO_INCREMENT,
`hash` VARCHAR(255) NOT NULL,
`uid` INT(11) NOT NULL,
`description` VARCHAR(255) NOT NULL,
PRIMARY KEY (`passid`, `uid`),
UNIQUE INDEX `passid_UNIQUE` (`passid` ASC),
INDEX `fk_apppasswords_accounts1_idx` (`uid` ASC),
CONSTRAINT `fk_apppasswords_accounts1`
FOREIGN KEY (`uid`)
REFERENCES `accounthub`.`accounts` (`uid`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;

+ 11
- 0
langs/en/apppasswords.json View File

@@ -0,0 +1,11 @@
{
"App Passwords": "App Passwords",
"app passwords explained": "Use app passwords instead of your actual password when logging into apps with your {site_name} login. App passwords are required in some places when you have 2-factor authentication enabled.",
"app password setup instructions": "Use the username and password below to log in to {app_name}. You'll only be shown this password one time.",
"App name": "App name",
"Generate password": "Generate password",
"Revoke password": "Revoke password",
"You don't have any app passwords.": "You don't have any app passwords.",
"Done": "Done",
"App passwords are not allowed here.": "App passwords are not allowed here."
}

+ 16
- 0
lib/User.lib.php View File

@@ -19,6 +19,7 @@ class User {
private $authsecret;
private $has2fa = false;
private $exists = false;
private $apppasswords = [];

public function __construct(int $uid, string $username = "") {
global $database;
@@ -32,6 +33,7 @@ class User {
$this->authsecret = $user['authsecret'];
$this->has2fa = !empty($user['authsecret']);
$this->exists = true;
$this->apppasswords = $database->select('apppasswords', 'hash', ['uid' => $this->uid]);
} else {
$this->uid = $uid;
$this->username = $username;
@@ -107,6 +109,20 @@ class User {
return password_verify($password, $this->passhash);
}

/**
* Check the given password against the user's app passwords.
* @param string $apppassword
* @return bool
*/
function checkAppPassword(string $apppassword): bool {
foreach ($this->apppasswords as $hash) {
if (password_verify($apppassword, $hash)) {
return true;
}
}
return false;
}

/**
* Change the user's password.
* @global $database $database

+ 3
- 0
login/index.php View File

@@ -93,6 +93,9 @@ if (!empty($_SESSION['check'])) {
}
} else {
$error = $Strings->get("Password incorrect.", false);
if ($user->checkAppPassword($_POST['password'])) {
$error = $Strings->get("App passwords are not allowed here.", false);
}
Log::insert(LogType::LOGIN_FAILED, $user);
}
break;

+ 89
- 1
pages/security.php View File

@@ -10,6 +10,12 @@ use Endroid\QrCode\ErrorCorrectionLevel;
use Endroid\QrCode\QrCode;

$user = new User($_SESSION['uid']);

if (!empty($_GET['delpass'])) {
if ($database->has("apppasswords", ["AND" => ["uid" => $_SESSION['uid'], "passid" => $_GET['delpass']]])) {
$database->delete("apppasswords", ["AND" => ["uid" => $_SESSION['uid'], "passid" => $_GET['delpass']]]);
}
}
?>
<div class="row justify-content-center">

@@ -138,5 +144,87 @@ $user = new User($_SESSION['uid']);
?>
</div>
</div>

<div class="col-sm-10 col-md-6 col-lg-4 col-xl-4">
<div class="card mb-4">
<?php
if (!empty($_GET['apppassword']) && $_GET['apppassword'] == "generate" && !empty($_POST['desc'])) {
$code = strtoupper(substr(md5(mt_rand() . uniqid("", true)), 0, 20));
$desc = htmlspecialchars($_POST['desc']);
$chunk_code = str_replace(" ", "-", trim(chunk_split($code, 5, ' ')));
$database->insert('apppasswords', ['uid' => $_SESSION['uid'], 'hash' => password_hash($chunk_code, PASSWORD_DEFAULT), 'description' => $desc]);
?>
<div class="card-body">
<h5 class="card-title"><i class="fas fa-shield-alt"></i> <?php $Strings->get("App Passwords"); ?></h5>
<hr />

<?php $Strings->build("app password setup instructions", ["app_name" => $desc]); ?>
</div>
<div class="list-group list-group-flush">
<div class="list-group-item d-flex justify-content-between align-items-baseline">
<div><?php $Strings->get("username"); ?>:</div>
<div class="text-monospace text-right"><?php echo $_SESSION['username']; ?></div>
</div>
<div class="list-group-item d-flex justify-content-between align-items-baseline">
<div><?php $Strings->get("password"); ?></div>
<div class="text-monospace text-right"><?php echo $chunk_code; ?></div>
</div>
</div>
<div class="card-body">
<a class="btn btn-success btn-block" href="app.php?page=security"><?php $Strings->get("Done"); ?></a>
</div>
<?php
} else {
$activecodes = $database->select("apppasswords", ["passid", "description"], ["uid" => $_SESSION['uid']]);
?>
<div class="card-body">
<h5 class="card-title"><i class="fas fa-shield-alt"></i> <?php $Strings->get("App Passwords"); ?></h5>
<hr />
<p class="card-text">
<?php $Strings->build("app passwords explained", ["site_name" => $SETTINGS['site_title']]); ?>
</p>
<form action="app.php?page=security&apppassword=generate" method="POST">
<input type="text" name="desc" class="form-control" placeholder="<?php $Strings->get("App name"); ?>" required />
<button class="btn btn-success btn-block mt-2" type="submit">
<?php $Strings->get("Generate password"); ?>
</button>
</form>
</div>
<div class="list-group list-group-flush">
<div class="list-group-item">
<b><?php $Strings->get("App Passwords"); ?></b>
</div>
<?php
if (count($activecodes) > 0) {
foreach ($activecodes as $c) {
?>
<div class="list-group-item d-flex justify-content-between align-items-center">
<div>
<div class="">
<?php echo $c['description']; ?>
</div>
</div>
<div>
<a class="btn btn-danger btn-sm m-1" href="app.php?page=security&delpass=<?php echo $c['passid']; ?>" data-toggle="tooltip" data-placement="bottom" title="<?php $Strings->get("Revoke password"); ?>">
<i class='fas fa-trash'></i><noscript> <?php $Strings->get("Revoke password"); ?></noscript>
</a>
</div>
</div>
<?php
}
} else {
?>
<div class="list-group-item">
<?php $Strings->get("You don't have any app passwords."); ?>
</div>
<?php
}
?>
</div>
<?php
}
?>
</div>

</div>
</div>

Loading…
Cancel
Save