]> WPIA git - gigi.git/commitdiff
add: internal api for password reset (with assurance)
authorFelix Dörre <felix@dogcraft.de>
Fri, 13 Nov 2015 17:58:57 +0000 (18:58 +0100)
committerFelix Dörre <felix@dogcraft.de>
Fri, 13 Nov 2015 18:25:58 +0000 (19:25 +0100)
src/org/cacert/gigi/Gigi.java
src/org/cacert/gigi/database/DatabaseConnection.java
src/org/cacert/gigi/database/tableStructure.sql
src/org/cacert/gigi/database/upgrade/from_5.sql [new file with mode: 0644]
src/org/cacert/gigi/dbObjects/User.java
src/org/cacert/gigi/pages/PasswordResetForm.templ [new file with mode: 0644]
src/org/cacert/gigi/pages/PasswordResetPage.java [new file with mode: 0644]
tests/org/cacert/gigi/TestPasswordReset.java [new file with mode: 0644]

index f558a6e44543a37fbdbb0374833a3f8bcd36e1df..59273a32e4608ecde1ebe96c583acb26cce0a209 100644 (file)
@@ -36,14 +36,15 @@ import org.cacert.gigi.pages.LoginPage;
 import org.cacert.gigi.pages.LogoutPage;
 import org.cacert.gigi.pages.MainPage;
 import org.cacert.gigi.pages.Page;
+import org.cacert.gigi.pages.PasswordResetPage;
 import org.cacert.gigi.pages.PolicyIndex;
 import org.cacert.gigi.pages.RootCertPage;
 import org.cacert.gigi.pages.StaticPage;
 import org.cacert.gigi.pages.TestSecure;
 import org.cacert.gigi.pages.Verify;
 import org.cacert.gigi.pages.account.ChangePasswordPage;
-import org.cacert.gigi.pages.account.MyDetails;
 import org.cacert.gigi.pages.account.History;
+import org.cacert.gigi.pages.account.MyDetails;
 import org.cacert.gigi.pages.account.UserTrainings;
 import org.cacert.gigi.pages.account.certs.CertificateAdd;
 import org.cacert.gigi.pages.account.certs.Certificates;
@@ -153,6 +154,9 @@ public class Gigi extends HttpServlet {
             putPage(History.SUPPORT_PATH, new History(true), null);
             putPage(UserTrainings.PATH, new UserTrainings(false), "My Account");
             putPage(UserTrainings.SUPPORT_PATH, new UserTrainings(true), null);
+
+            putPage(PasswordResetPage.PATH, new PasswordResetPage(), null);
+
             if (testing) {
                 try {
                     Class<?> manager = Class.forName("org.cacert.gigi.pages.Manager");
index 67364d0a06fe95ee02760d5c270e8121a6c25e9b..b8c09e5ab76a7d37a30b0a1ea9db171b24786abd 100644 (file)
@@ -17,7 +17,7 @@ import org.cacert.gigi.database.SQLFileManager.ImportType;
 
 public class DatabaseConnection {
 
-    public static final int CURRENT_SCHEMA_VERSION = 5;
+    public static final int CURRENT_SCHEMA_VERSION = 6;
 
     public static final int CONNECTION_TIMEOUT = 24 * 60 * 60;
 
index c4e517376db378608a17ca4f4ebe9cd8c55716fc..301dabd28f9602855a72bee12b7ece1465c81056 100644 (file)
@@ -373,4 +373,16 @@ CREATE TABLE "schemeVersion" (
   "version" smallint NOT NULL,
   PRIMARY KEY ("version")
 );
-INSERT INTO "schemeVersion" (version)  VALUES(5);
+INSERT INTO "schemeVersion" (version)  VALUES(6);
+
+DROP TABLE IF EXISTS `passwordResetTickets`;
+CREATE TABLE `passwordResetTickets` (
+  `id` serial NOT NULL,
+  `memid` int NOT NULL,
+  `creator` int NOT NULL,
+  `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `used` timestamp NULL DEFAULT NULL,
+  `token` varchar(32) NOT NULL,
+  `private_token` varchar(255) NOT NULL,
+  PRIMARY KEY (`id`)
+);
diff --git a/src/org/cacert/gigi/database/upgrade/from_5.sql b/src/org/cacert/gigi/database/upgrade/from_5.sql
new file mode 100644 (file)
index 0000000..6f9d8f7
--- /dev/null
@@ -0,0 +1,11 @@
+DROP TABLE IF EXISTS `passwordResetTickets`;
+CREATE TABLE `passwordResetTickets` (
+  `id` serial NOT NULL,
+  `memid` int NOT NULL,
+  `creator` int NOT NULL,
+  `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `used` timestamp NULL DEFAULT NULL,
+  `token` varchar(32) NOT NULL,
+  `private_token` varchar(255) NOT NULL,
+  PRIMARY KEY (`id`)
+);
index 8d1d40fc45b55b1da27289e1105ef452030b01e4..fc7b1c3f07aaa0260d5111dc46ce2b46e18f7dd1 100644 (file)
@@ -109,7 +109,11 @@ public class User extends CertificateOwner {
                 throw new GigiApiException("Old password does not match.");
             }
         }
+        setPassword(newPass);
+    }
 
+    private void setPassword(String newPass) throws GigiApiException {
+        GigiPreparedStatement ps;
         PasswordStrengthChecker.assertStrongPassword(newPass, getName(), getEmail());
         ps = DatabaseConnection.getInstance().prepare("UPDATE users SET `password`=? WHERE id=?");
         ps.setString(1, PasswordHash.hash(newPass));
@@ -479,4 +483,41 @@ public class User extends CertificateOwner {
 
         return entries.toArray(new String[0]);
     }
+
+    public int generatePasswordResetTicket(User actor, String token, String privateToken) {
+        GigiPreparedStatement ps = DatabaseConnection.getInstance().prepare("INSERT INTO `passwordResetTickets` SET `memid`=?, `creator`=?, `token`=?, `private_token`=?");
+        ps.setInt(1, getId());
+        ps.setInt(2, getId());
+        ps.setString(3, token);
+        ps.setString(4, PasswordHash.hash(privateToken));
+        ps.execute();
+        return ps.lastInsertId();
+    }
+
+    public static User getResetWithToken(int id, String token) {
+        GigiPreparedStatement ps = DatabaseConnection.getInstance().prepare("SELECT `memid` FROM `passwordResetTickets` WHERE `id`=? AND `token`=?");
+        ps.setInt(1, id);
+        ps.setString(2, token);
+        GigiResultSet res = ps.executeQuery();
+        if ( !res.next()) {
+            return null;
+        }
+        return User.getById(res.getInt(1));
+    }
+
+    public void consumePasswordResetTicket(int id, String private_token, String newPassword) throws GigiApiException {
+        GigiPreparedStatement ps = DatabaseConnection.getInstance().prepare("SELECT `private_token` FROM `passwordResetTickets` WHERE `id`=? AND `memid`=?");
+        ps.setInt(1, id);
+        ps.setInt(2, getId());
+        try (GigiResultSet rs = ps.executeQuery()) {
+            if ( !rs.next()) {
+                throw new GigiApiException("Token not found... very bad.");
+            }
+            if (PasswordHash.verifyHash(private_token, rs.getString(1)) == null) {
+                throw new GigiApiException("Private token does not match.");
+            }
+            setPassword(newPassword);
+        }
+    }
+
 }
diff --git a/src/org/cacert/gigi/pages/PasswordResetForm.templ b/src/org/cacert/gigi/pages/PasswordResetForm.templ
new file mode 100644 (file)
index 0000000..aa5cce5
--- /dev/null
@@ -0,0 +1,27 @@
+<table class="wrapper dataTable">
+  <thead>
+  <tr>
+    <th colspan="2" class="title"><?=_Change Pass Phrase?></th>
+  </tr>
+  </thead>
+  <tbody>
+  <tr>
+    <td><?=_Password reset token?>: </td>
+    <td><input type="password" name="private_token"></td>
+  </tr>
+  <tr>
+    <td><?=_New Pass Phrase?><span class="formMandatory">*</span>: </td>
+    <td><input type="password" name="pword1"></td>
+  </tr>
+  <tr>
+    <td><?=_Pass Phrase Again?><span class="formMandatory">*</span>: </td>
+    <td><input type="password" name="pword2"></td>
+  </tr>
+  <tr>
+    <td colspan="2"><span class="formMandatory">*</span><?=_Please note, in the interests of good security, the pass phrase must be made up of an upper case letter, lower case letter, number and symbol (all white spaces at the beginning and end are removed).?></td>
+  </tr>
+  <tr>
+    <td colspan="2"><input type="submit" name="process" value="<?=_Update Pass Phrase?>"></td>
+  </tr>
+  </tbody>
+</table>
diff --git a/src/org/cacert/gigi/pages/PasswordResetPage.java b/src/org/cacert/gigi/pages/PasswordResetPage.java
new file mode 100644 (file)
index 0000000..8faaf82
--- /dev/null
@@ -0,0 +1,93 @@
+package org.cacert.gigi.pages;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.cacert.gigi.GigiApiException;
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.localisation.Language;
+import org.cacert.gigi.output.template.Form;
+import org.cacert.gigi.output.template.Template;
+import org.cacert.gigi.util.AuthorizationContext;
+
+public class PasswordResetPage extends Page {
+
+    public static final String PATH = "/passwordReset";
+
+    public PasswordResetPage() {
+        super("Password Reset");
+    }
+
+    public static class PasswordResetForm extends Form {
+
+        private static Template t = new Template(PasswordResetForm.class.getResource("PasswordResetForm.templ"));
+
+        private User u;
+
+        private int id;
+
+        public PasswordResetForm(HttpServletRequest hsr) throws GigiApiException {
+            super(hsr, PATH);
+            id = Integer.parseInt(hsr.getParameter("id"));
+            u = User.getResetWithToken(id, hsr.getParameter("token"));
+            if (u == null) {
+                throw new GigiApiException("User missing or token invalid");
+            }
+
+        }
+
+        @Override
+        public boolean submit(PrintWriter out, HttpServletRequest req) throws GigiApiException {
+            String p1 = req.getParameter("pword1");
+            String p2 = req.getParameter("pword2");
+            String tok = req.getParameter("private_token");
+            if (p1 == null || p2 == null || tok == null) {
+                throw new GigiApiException("Missing form parameter.");
+            }
+            if ( !p1.equals(p2)) {
+                throw new GigiApiException("New passwords differ.");
+            }
+            u.consumePasswordResetTicket(id, tok, p1);
+            return true;
+        }
+
+        @Override
+        protected void outputContent(PrintWriter out, Language l, Map<String, Object> vars) {
+
+            t.output(out, l, vars);
+        }
+
+    }
+
+    @Override
+    public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        PasswordResetForm form = Form.getForm(req, PasswordResetForm.class);
+        try {
+            form.submit(resp.getWriter(), req);
+            resp.getWriter().println(getLanguage(req).getTranslation("Password reset successful."));
+            return;
+        } catch (GigiApiException e) {
+            e.format(resp.getWriter(), getLanguage(req));
+        }
+        form.output(resp.getWriter(), getLanguage(req), new HashMap<String, Object>());
+    }
+
+    @Override
+    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+        try {
+            new PasswordResetForm(req).output(resp.getWriter(), getLanguage(req), new HashMap<String, Object>());
+        } catch (GigiApiException e) {
+            e.format(resp.getWriter(), getLanguage(req));
+        }
+    }
+
+    @Override
+    public boolean isPermitted(AuthorizationContext ac) {
+        return true;
+    }
+}
diff --git a/tests/org/cacert/gigi/TestPasswordReset.java b/tests/org/cacert/gigi/TestPasswordReset.java
new file mode 100644 (file)
index 0000000..384ec31
--- /dev/null
@@ -0,0 +1,56 @@
+package org.cacert.gigi;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+
+import org.cacert.gigi.dbObjects.User;
+import org.cacert.gigi.testUtils.ClientTest;
+import org.cacert.gigi.util.RandomToken;
+import org.junit.Test;
+
+public class TestPasswordReset extends ClientTest {
+
+    String pub = RandomToken.generateToken(32);
+
+    String priv = RandomToken.generateToken(32);
+
+    int id = u.generatePasswordResetTicket(u, pub, priv);
+
+    @Test
+    public void testInternal() throws IOException, GigiApiException {
+        User u2 = User.getResetWithToken(id, pub);
+        assertSame(u, u2);
+        assertNotNull(login(u.getEmail(), TEST_PASSWORD));
+        u2.consumePasswordResetTicket(id, priv, TEST_PASSWORD + "'");
+        assertEquals("", login(u.getEmail(), TEST_PASSWORD));
+        assertNotNull(login(u.getEmail(), TEST_PASSWORD + "'"));
+    }
+
+    @Test
+    public void testInternalWrongTk() throws IOException, GigiApiException {
+        User u2 = User.getResetWithToken(id, pub + "'");
+        assertNull(u2);
+    }
+
+    @Test
+    public void testInternalWrongId() throws IOException, GigiApiException {
+        User u2 = User.getResetWithToken(id + 1, pub);
+        assertNull(u2);
+    }
+
+    @Test(expected = GigiApiException.class)
+    public void testInternalWeak() throws IOException, GigiApiException {
+        u.consumePasswordResetTicket(id, priv, "");
+    }
+
+    @Test(expected = GigiApiException.class)
+    public void testInternalWrongPriv() throws IOException, GigiApiException {
+        u.consumePasswordResetTicket(id, priv + "'", TEST_PASSWORD);
+    }
+
+    @Test(expected = GigiApiException.class)
+    public void testInternalWrongIdSetting() throws IOException, GigiApiException {
+        u.consumePasswordResetTicket(id + 1, priv, TEST_PASSWORD);
+    }
+}