Browse Source

Add app passwords (close #15)

master
Skylar Ittner 6 months ago
parent
commit
22fb97d0c4

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

@@ -7,7 +7,16 @@
7 7
  */
8 8
 
9 9
 $user = User::byUsername($VARS['username']);
10
-if ($user->checkPassword($VARS['password'])) {
10
+
11
+$ok = false;
12
+if (empty($VARS['apppass']) && ($user->checkPassword($VARS['password']) || $user->checkAppPassword($VARS['password']))) {
13
+    $ok = true;
14
+} else {
15
+    if ((!$user->has2fa() && $user->checkPassword($VARS['password'])) || $user->checkAppPassword($VARS['password'])) {
16
+        $ok = true;
17
+    }
18
+}
19
+if ($ok) {
11 20
     Log::insert(LogType::API_AUTH_OK, null, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey());
12 21
     sendJsonResp($Strings->get("login successful", false), "OK");
13 22
 } else {

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

@@ -8,7 +8,7 @@
8 8
 
9 9
 engageRateLimit();
10 10
 $user = User::byUsername($VARS['username']);
11
-if ($user->checkPassword($VARS['password'])) {
11
+if ((!$user->has2fa() && $user->checkPassword($VARS['password'])) || $user->checkAppPassword($VARS['password'])) {
12 12
     switch ($user->getStatus()->getString()) {
13 13
         case "LOCKED_OR_DISABLED":
14 14
             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 = [
19 19
         "load" => "auth.php",
20 20
         "vars" => [
21 21
             "username" => "string",
22
-            "password" => "string"
22
+            "password" => "string",
23
+            "apppass (optional)" => "/[0-1]/"
23 24
         ],
24 25
         "keytype" => "AUTH"
25 26
     ],

+ 6
- 4
api/functions.php View File

@@ -90,11 +90,13 @@ function checkVars($vars, $or = false) {
90 90
                 continue;
91 91
             }
92 92
         }
93
-        $checkmethod = "is_$val";
94
-        if ($checkmethod($VARS[$key]) !== true) {
95
-            $ok[$key] = false;
93
+
94
+        if (strpos($val, "/") === 0) {
95
+            // regex
96
+            $ok[$key] = preg_match($val, $VARS[$key]) === 1;
96 97
         } else {
97
-            $ok[$key] = true;
98
+            $checkmethod = "is_$val";
99
+            $ok[$key] = !($checkmethod($VARS[$key]) !== true);
98 100
         }
99 101
     }
100 102
     if ($or) {

BIN
database.mwb View File


+ 20
- 0
database.sql View File

@@ -1,5 +1,5 @@
1 1
 -- MySQL Script generated by MySQL Workbench
2
+-- Mon 11 Feb 2019 04:07:57 PM MST
2 3
 -- Model: New Model    Version: 1.0
3 4
 -- MySQL Workbench Forward Engineering
4 5
 
@@ -335,6 +335,25 @@ CREATE TABLE IF NOT EXISTS `userloginkeys` (
335 335
 ENGINE = InnoDB;
336 336
 
337 337
 
338
+-- -----------------------------------------------------
339
+-- Table `apppasswords`
340
+-- -----------------------------------------------------
341
+CREATE TABLE IF NOT EXISTS `apppasswords` (
342
+  `passid` INT NOT NULL AUTO_INCREMENT,
343
+  `hash` VARCHAR(255) NOT NULL,
344
+  `uid` INT NOT NULL,
345
+  `description` VARCHAR(255) NOT NULL,
346
+  PRIMARY KEY (`passid`, `uid`),
347
+  UNIQUE INDEX `passid_UNIQUE` (`passid` ASC),
348
+  INDEX `fk_apppasswords_accounts1_idx` (`uid` ASC),
349
+  CONSTRAINT `fk_apppasswords_accounts1`
350
+    FOREIGN KEY (`uid`)
351
+    REFERENCES `accounts` (`uid`)
352
+    ON DELETE NO ACTION
353
+    ON UPDATE NO ACTION)
354
+ENGINE = InnoDB;
355
+
356
+
338 357
 SET SQL_MODE=@OLD_SQL_MODE;
339 358
 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;
340 359
 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`;
29 29
 ALTER TABLE `userloginkeys`
30 30
 ADD COLUMN `appicon` TINYTEXT NULL DEFAULT NULL AFTER `appname`;
31 31
 ALTER TABLE `apikeys`
32
-ADD COLUMN `type` VARCHAR(45) NOT NULL DEFAULT 'FULL' AFTER `notes`;
32
+ADD COLUMN `type` VARCHAR(45) NOT NULL DEFAULT 'FULL' AFTER `notes`;
33
+
34
+CREATE TABLE IF NOT EXISTS `apppasswords` (
35
+  `passid` INT(11) NOT NULL AUTO_INCREMENT,
36
+  `hash` VARCHAR(255) NOT NULL,
37
+  `uid` INT(11) NOT NULL,
38
+  `description` VARCHAR(255) NOT NULL,
39
+  PRIMARY KEY (`passid`, `uid`),
40
+  UNIQUE INDEX `passid_UNIQUE` (`passid` ASC),
41
+  INDEX `fk_apppasswords_accounts1_idx` (`uid` ASC),
42
+  CONSTRAINT `fk_apppasswords_accounts1`
43
+    FOREIGN KEY (`uid`)
44
+    REFERENCES `accounthub`.`accounts` (`uid`)
45
+    ON DELETE NO ACTION
46
+    ON UPDATE NO ACTION)
47
+ENGINE = InnoDB
48
+DEFAULT CHARACTER SET = utf8;

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

@@ -0,0 +1,11 @@
1
+{
2
+    "App Passwords": "App Passwords",
3
+    "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.",
4
+    "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.",
5
+    "App name": "App name",
6
+    "Generate password": "Generate password",
7
+    "Revoke password": "Revoke password",
8
+    "You don't have any app passwords.": "You don't have any app passwords.",
9
+    "Done": "Done",
10
+    "App passwords are not allowed here.": "App passwords are not allowed here."
11
+}

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

@@ -19,6 +19,7 @@ class User {
19 19
     private $authsecret;
20 20
     private $has2fa = false;
21 21
     private $exists = false;
22
+    private $apppasswords = [];
22 23
 
23 24
     public function __construct(int $uid, string $username = "") {
24 25
         global $database;
@@ -32,6 +33,7 @@ class User {
32 33
             $this->authsecret = $user['authsecret'];
33 34
             $this->has2fa = !empty($user['authsecret']);
34 35
             $this->exists = true;
36
+            $this->apppasswords = $database->select('apppasswords', 'hash', ['uid' => $this->uid]);
35 37
         } else {
36 38
             $this->uid = $uid;
37 39
             $this->username = $username;
@@ -107,6 +109,20 @@ class User {
107 109
         return password_verify($password, $this->passhash);
108 110
     }
109 111
 
112
+    /**
113
+     * Check the given password against the user's app passwords.
114
+     * @param string $apppassword
115
+     * @return bool
116
+     */
117
+    function checkAppPassword(string $apppassword): bool {
118
+        foreach ($this->apppasswords as $hash) {
119
+            if (password_verify($apppassword, $hash)) {
120
+                return true;
121
+            }
122
+        }
123
+        return false;
124
+    }
125
+
110 126
     /**
111 127
      * Change the user's password.
112 128
      * @global $database $database

+ 3
- 0
login/index.php View File

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

+ 89
- 1
pages/security.php View File

@@ -10,6 +10,12 @@ use Endroid\QrCode\ErrorCorrectionLevel;
10 10
 use Endroid\QrCode\QrCode;
11 11
 
12 12
 $user = new User($_SESSION['uid']);
13
+
14
+if (!empty($_GET['delpass'])) {
15
+    if ($database->has("apppasswords", ["AND" => ["uid" => $_SESSION['uid'], "passid" => $_GET['delpass']]])) {
16
+        $database->delete("apppasswords", ["AND" => ["uid" => $_SESSION['uid'], "passid" => $_GET['delpass']]]);
17
+    }
18
+}
13 19
 ?>
14 20
 <div class="row justify-content-center">
15 21
 
@@ -138,5 +144,87 @@ $user = new User($_SESSION['uid']);
138 144
             ?>
139 145
         </div>
140 146
     </div>
141
-    
147
+
148
+    <div class="col-sm-10 col-md-6 col-lg-4 col-xl-4">
149
+        <div class="card mb-4">
150
+            <?php
151
+            if (!empty($_GET['apppassword']) && $_GET['apppassword'] == "generate" && !empty($_POST['desc'])) {
152
+                $code = strtoupper(substr(md5(mt_rand() . uniqid("", true)), 0, 20));
153
+                $desc = htmlspecialchars($_POST['desc']);
154
+                $chunk_code = str_replace(" ", "-", trim(chunk_split($code, 5, ' ')));
155
+                $database->insert('apppasswords', ['uid' => $_SESSION['uid'], 'hash' => password_hash($chunk_code, PASSWORD_DEFAULT), 'description' => $desc]);
156
+                ?>
157
+                <div class="card-body">
158
+                    <h5 class="card-title"><i class="fas fa-shield-alt"></i> <?php $Strings->get("App Passwords"); ?></h5>
159
+                    <hr />
160
+
161
+                    <?php $Strings->build("app password setup instructions", ["app_name" => $desc]); ?>
162
+                </div>
163
+                <div class="list-group list-group-flush">
164
+                    <div class="list-group-item d-flex justify-content-between align-items-baseline">
165
+                        <div><?php $Strings->get("username"); ?>:</div>
166
+                        <div class="text-monospace text-right"><?php echo $_SESSION['username']; ?></div>
167
+                    </div>
168
+                    <div class="list-group-item d-flex justify-content-between align-items-baseline">
169
+                        <div><?php $Strings->get("password"); ?></div>
170
+                        <div class="text-monospace text-right"><?php echo $chunk_code; ?></div>
171
+                    </div>
172
+                </div>
173
+                <div class="card-body">
174
+                    <a class="btn btn-success btn-block" href="app.php?page=security"><?php $Strings->get("Done"); ?></a>
175
+                </div>
176
+                <?php
177
+            } else {
178
+                $activecodes = $database->select("apppasswords", ["passid", "description"], ["uid" => $_SESSION['uid']]);
179
+                ?>
180
+                <div class="card-body">
181
+                    <h5 class="card-title"><i class="fas fa-shield-alt"></i> <?php $Strings->get("App Passwords"); ?></h5>
182
+                    <hr />
183
+                    <p class="card-text">
184
+                        <?php $Strings->build("app passwords explained", ["site_name" => $SETTINGS['site_title']]); ?>
185
+                    </p>
186
+                    <form action="app.php?page=security&apppassword=generate" method="POST">
187
+                        <input type="text" name="desc" class="form-control" placeholder="<?php $Strings->get("App name"); ?>" required />
188
+                        <button class="btn btn-success btn-block mt-2" type="submit">
189
+                            <?php $Strings->get("Generate password"); ?>
190
+                        </button>
191
+                    </form>
192
+                </div>
193
+                <div class="list-group list-group-flush">
194
+                    <div class="list-group-item">
195
+                        <b><?php $Strings->get("App Passwords"); ?></b>
196
+                    </div>
197
+                    <?php
198
+                    if (count($activecodes) > 0) {
199
+                        foreach ($activecodes as $c) {
200
+                            ?>
201
+                            <div class="list-group-item d-flex justify-content-between align-items-center">
202
+                                <div>
203
+                                    <div class="">
204
+                                        <?php echo $c['description']; ?>
205
+                                    </div>
206
+                                </div>
207
+                                <div>
208
+                                    <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"); ?>">
209
+                                        <i class='fas fa-trash'></i><noscript> <?php $Strings->get("Revoke password"); ?></noscript>
210
+                                    </a>
211
+                                </div>
212
+                            </div>
213
+                            <?php
214
+                        }
215
+                    } else {
216
+                        ?>
217
+                        <div class="list-group-item">
218
+                            <?php $Strings->get("You don't have any app passwords."); ?>
219
+                        </div>
220
+                        <?php
221
+                    }
222
+                    ?>
223
+                </div>
224
+                <?php
225
+            }
226
+            ?>
227
+        </div>
228
+
229
+    </div>
142 230
 </div>

Loading…
Cancel
Save