summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Dolgov <[email protected]>2019-11-01 15:03:57 +0300
committerAndrew Dolgov <[email protected]>2019-11-01 15:03:57 +0300
commit249130e58ddd20c5ad937f75e0e6cf3e4f6792a3 (patch)
tree8e896bc621989df3b8c1baae8078a7fb9371d6b2
parentb158103f2f6a3295d00dc4a1344b8bc38bcb43a4 (diff)
implement app password checking / management UI
-rw-r--r--classes/pref/prefs.php107
-rw-r--r--js/PrefHelpers.js38
-rw-r--r--plugins/auth_internal/init.php22
3 files changed, 163 insertions, 4 deletions
diff --git a/classes/pref/prefs.php b/classes/pref/prefs.php
index d9482b966..76dc526ab 100644
--- a/classes/pref/prefs.php
+++ b/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();
+ }
}
diff --git a/js/PrefHelpers.js b/js/PrefHelpers.js
index a3d122029..6a62cb593 100644
--- a/js/PrefHelpers.js
+++ b/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...");
diff --git a/plugins/auth_internal/init.php b/plugins/auth_internal/init.php
index 576f8ef05..a374c0948 100644
--- a/plugins/auth_internal/init.php
+++ b/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;
}