Browse Source

implement app password checking / management UI

Andrew Dolgov 8 months ago
parent
commit
249130e58d
3 changed files with 163 additions and 4 deletions
  1. 103 4
      classes/pref/prefs.php
  2. 38 0
      js/PrefHelpers.js
  3. 22 0
      plugins/auth_internal/init.php

+ 103 - 4
classes/pref/prefs.php

@@ -395,13 +395,29 @@ class Pref_Prefs extends Handler_Protected {
 			print "</form>";
 
 			print "</div>"; # content pane
-			print "<div dojoType='dijit.layout.ContentPane' title=\"".__('App passwords')."\">";
 
-			print_notice("You can create separate passwords for API clients. Using one is required if you enable OTP.");
+			if ($_SESSION["auth_module"] == "auth_internal") {
 
+				print "<div dojoType='dijit.layout.ContentPane' title=\"" . __('App passwords') . "\">";
 
+				print_notice("You can create separate passwords for the API clients. Using one is required if you enable OTP.");
 
-			print "</div>"; # content pane
+				print "<div id='app_passwords_holder'>";
+				$this->appPasswordList();
+				print "</div>";
+
+				print "<hr>";
+
+				print "<button style='float : left' class='alt-primary' dojoType='dijit.form.Button' 
+					onclick=\"Helpers.AppPasswords.generate()\">" .
+					__('Generate new password') . "</button> ";
+
+				print "<button style='float : left' class='alt-danger' dojoType='dijit.form.Button' 
+					onclick=\"Helpers.AppPasswords.removeSelected()\">" .
+					__('Remove selected passwords') . "</button>";
+
+				print "</div>"; # content pane
+			}
 
 			print "<div dojoType='dijit.layout.ContentPane' title=\"".__('One time passwords / Authenticator')."\">";
 
@@ -450,7 +466,7 @@ class Pref_Prefs extends Handler_Protected {
 				} else {
 
 					print_warning("You will need a compatible Authenticator to use this. Changing your password would automatically disable OTP.");
-					print_notice("You will also need to create a separate App password for API clients if you enable OTP.");
+					print_notice("You will need to use a separate password for the API clients if you enable OTP.");
 
 					if (function_exists("imagecreatefromstring")) {
 						print "<h3>" . __("Scan the following code by the Authenticator application or copy the key manually:") . "</h3>";
@@ -1221,4 +1237,87 @@ class Pref_Prefs extends Handler_Protected {
 		}
 		return "";
 	}
+
+	private function appPasswordList() {
+		print "<div dojoType='fox.Toolbar'>";
+		print "<div dojoType='fox.form.DropDownButton'>" .
+			"<span>" . __('Select') . "</span>";
+		print "<div dojoType='dijit.Menu' style='display: none'>";
+		print "<div onclick=\"Tables.select('app-password-list', true)\"
+				dojoType=\"dijit.MenuItem\">" . __('All') . "</div>";
+		print "<div onclick=\"Tables.select('app-password-list', false)\"
+				dojoType=\"dijit.MenuItem\">" . __('None') . "</div>";
+		print "</div></div>";
+		print "</div>"; #toolbar
+
+		print "<div class='panel panel-scrollable'>";
+		print "<table width='100%' id='app-password-list'>";
+		print "<tr>";
+		print "<th width='2%'></th>";
+		print "<th align='left'>".__("Description")."</th>";
+		print "<th align='right'>".__("Created")."</th>";
+		print "<th align='right'>".__("Last used")."</th>";
+		print "</tr>";
+
+		$sth = $this->pdo->prepare("SELECT id, title, created, last_used 
+			FROM ttrss_app_passwords WHERE owner_uid = ?");
+		$sth->execute([$_SESSION['uid']]);
+
+		while ($row = $sth->fetch()) {
+
+			$row_id = $row["id"];
+
+			print "<tr data-row-id='$row_id'>";
+
+			print "<td align='center'>
+						<input onclick='Tables.onRowChecked(this)' dojoType='dijit.form.CheckBox' type='checkbox'></td>";
+			print "<td>" . htmlspecialchars($row["title"]) . "</td>";
+
+			print "<td align='right' class='text-muted'>";
+			print make_local_datetime($row['created'], false);
+			print "</td>";
+
+			print "<td align='right' class='text-muted'>";
+			print make_local_datetime($row['last_used'], false);
+			print "</td>";
+
+			print "</tr>";
+		}
+
+		print "</table>";
+		print "</div>";
+	}
+
+	private function encryptAppPassword($password) {
+		$salt = substr(bin2hex(get_random_bytes(24)), 0, 24);
+
+		return "SSHA-512:".hash('sha512', $salt . $password). ":$salt";
+	}
+
+	function deleteAppPassword() {
+		$ids = explode(",", clean($_REQUEST['ids']));
+		$ids_qmarks = arr_qmarks($ids);
+
+		$sth = $this->pdo->prepare("DELETE FROM ttrss_app_passwords WHERE id IN ($ids_qmarks) AND owner_uid = ?");
+		$sth->execute(array_merge($ids, [$_SESSION['uid']]));
+
+		$this->appPasswordList();
+	}
+
+	function generateAppPassword() {
+		$title = clean($_REQUEST['title']);
+		$new_password = make_password(16);
+		$new_password_hash = $this->encryptAppPassword($new_password);
+
+		print_warning(T_sprintf("Generated password <strong>%s</strong> for %s. Please remember it for future reference.", $new_password, $title));
+
+		$sth = $this->pdo->prepare("INSERT INTO ttrss_app_passwords 
+    			(title, pwd_hash, service, created, owner_uid)
+    		 VALUES 
+    		    (?, ?, ?, NOW(), ?)");
+
+		$sth->execute([$title, $new_password_hash, Auth_Base::AUTH_SERVICE_API, $_SESSION['uid']]);
+
+		$this->appPasswordList();
+	}
 }

+ 38 - 0
js/PrefHelpers.js

@@ -1,5 +1,43 @@
 define(["dojo/_base/declare"], function (declare) {
 	Helpers = {
+		AppPasswords: {
+			getSelected: function() {
+				return Tables.getSelected("app-password-list");
+			},
+			updateContent: function(data) {
+				$("app_passwords_holder").innerHTML = data;
+				dojo.parser.parse("app_passwords_holder");
+			},
+			removeSelected: function() {
+				const rows = this.getSelected();
+
+				if (rows.length == 0) {
+					alert("No passwords selected.");
+				} else {
+					if (confirm(__("Remove selected app passwords?"))) {
+
+						xhrPost("backend.php", {op: "pref-prefs", method: "deleteAppPassword", ids: rows.toString()}, (transport) => {
+							this.updateContent(transport.responseText);
+							Notify.close();
+						});
+
+						Notify.progress("Loading, please wait...");
+					}
+				}
+			},
+			generate: function() {
+				const title = prompt("Password description:")
+
+				if (title) {
+					xhrPost("backend.php", {op: "pref-prefs", method: "generateAppPassword", title: title}, (transport) => {
+						this.updateContent(transport.responseText);
+						Notify.close();
+					});
+
+					Notify.progress("Loading, please wait...");
+				}
+			},
+		},
 		clearFeedAccessKeys: function() {
 			if (confirm(__("This will invalidate all previously generated feed URLs. Continue?"))) {
 				Notify.progress("Clearing URLs...");

+ 22 - 0
plugins/auth_internal/init.php

@@ -258,6 +258,28 @@
 		}
 
 		private function check_app_password($login, $password, $service) {
+			$sth = $this->pdo->prepare("SELECT p.id, p.pwd_hash, u.id AS uid 
+				FROM ttrss_app_passwords p, ttrss_users u 
+				WHERE p.owner_uid = u.id AND u.login = ? AND service = ?");
+			$sth->execute([$login, $service]);
+
+			while ($row = $sth->fetch()) {
+				list ($algo, $hash, $salt) = explode(":", $row["pwd_hash"]);
+
+				if ($algo == "SSHA-512") {
+					$test_hash = hash('sha512', $salt . $password);
+
+					if ($test_hash == $hash) {
+						$usth = $this->pdo->prepare("UPDATE ttrss_app_passwords SET last_used = NOW() WHERE id = ?");
+						$usth->execute([$row['id']]);
+
+						return $row['uid'];
+					}
+				} else {
+					user_error("Got unknown algo of app password for user $login: $algo");
+				}
+			}
+
 			return false;
 		}