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;
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");
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;
"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`)
+);
--- /dev/null
+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`)
+);
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));
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);
+ }
+ }
+
}
--- /dev/null
+<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>
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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);
+ }
+}