/******************************************************************************* * Copyright (c) 2013, 2014 Lectorius, Inc. * Authors: * Vijay Pandurangan (vijayp@mitro.co) * Evan Jones (ej@mitro.co) * Adam Hilss (ahilss@mitro.co) * * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * * You can contact the authors at inbound@mitro.co. *******************************************************************************/ package co.mitro.core.servlets; import static org.hamcrest.CoreMatchers.containsString; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.IOException; import java.sql.SQLException; import org.junit.Before; import org.junit.Test; import co.mitro.core.exceptions.DoEmailVerificationException; import co.mitro.core.exceptions.DoTwoFactorAuthException; import co.mitro.core.exceptions.MitroServletException; import co.mitro.core.server.Manager; import co.mitro.core.server.data.DBDeviceSpecificInfo; import co.mitro.core.server.data.DBEmailQueue; import co.mitro.core.server.data.DBIdentity; import co.mitro.core.server.data.RPC.GetMyPrivateKeyRequest; import co.mitro.core.server.data.RPC.GetMyPrivateKeyResponse; import co.mitro.core.servlets.MitroServlet.MitroRequestContext; import co.mitro.twofactor.TwoFactorTests; public class GetMyPrivateKeyTest extends TwoFactorTests { GetMyPrivateKey servlet; @Before public void setUp() throws SQLException { servlet = new GetMyPrivateKey(managerFactory, keyFactory); testIdentity2.setEncryptedPrivateKeyString("encrypted private key"); manager.identityDao.update(testIdentity2); } // TODO: Merge GetMyPrivateKeyTest and GetMyDeviceKeyTest, make this private public static GetMyPrivateKeyResponse tryLogin(GetMyPrivateKey servlet, DBIdentity identity, Manager manager, String deviceId, String loginToken, String loginTokenSignature, String twoFactorCode, boolean automatic) throws IOException, SQLException, MitroServletException { GetMyPrivateKeyRequest request = new GetMyPrivateKeyRequest(); request.userId = identity.getName(); request.deviceId = deviceId; request.loginToken = loginToken; request.loginTokenSignature = loginTokenSignature; request.twoFactorCode = twoFactorCode; request.automatic = automatic; return (GetMyPrivateKeyResponse) servlet.processCommand( new MitroRequestContext(null, gson.toJson(request), manager, null)); } private GetMyPrivateKeyResponse tryLogin(DBIdentity identity, Manager manager, String deviceId, String loginToken, String loginTokenSignature, String twoFactorCode, boolean automatic) throws IOException, SQLException, MitroServletException { return tryLogin(servlet, identity, manager, deviceId, loginToken, loginTokenSignature, twoFactorCode, automatic); } @Test public void simpleLoginWithoutTwoFactorAuth() throws Exception { // returns the private key but no device key GetMyPrivateKeyResponse response = tryLogin(testIdentity2, manager, DEVICE_ID, null, null, null, false); assertEquals(testIdentity2.getEncryptedPrivateKeyString(), response.encryptedPrivateKey); assertFalse(response.changePasswordOnNextLogin); assertThat(response.unsignedLoginToken, containsString(testIdentity2.getName())); assertTrue(response.verified); assertNull(response.deviceKeyString); String deprecatedMyId = response.myUserId; assertEquals(testIdentity2.getName(), deprecatedMyId); } @Test public void invitedUserLogin() throws Exception { // Change password on login means invited user; we must give them the private key // TODO: we should probably pass some signed token from the invitation email? testIdentity2.setChangePasswordOnNextLogin(true); // delete existing devices for testIdentity2 manager.deviceSpecificDao.delete(manager.deviceSpecificDao.queryForEq( DBDeviceSpecificInfo.IDENTITY_FIELD_NAME, testIdentity2.getId())); manager.identityDao.update(testIdentity2); // returns the private key but no device key GetMyPrivateKeyResponse response = tryLogin(testIdentity2, manager, "newdevice", null, null, null, false); assertEquals(testIdentity2.getEncryptedPrivateKeyString(), response.encryptedPrivateKey); assertTrue(response.changePasswordOnNextLogin); assertThat(response.unsignedLoginToken, containsString(testIdentity2.getName())); assertTrue(response.verified); assertNull(response.deviceKeyString); // this also works for a second new device // TODO: Limit this to N devices? Previously was limited to 1, but I suspect some users // ran into trouble (e.g. click on one, don't change password, click on another, get error) tryLogin(testIdentity2, manager, "anotherdevice", null, null, null, false); // Reset the "change password" flag; next request still returns the key because the device is registered testIdentity2.setChangePasswordOnNextLogin(false); manager.identityDao.update(testIdentity2); response = tryLogin(testIdentity2, manager, "newdevice", null, null, null, false); assertEquals(testIdentity2.getEncryptedPrivateKeyString(), response.encryptedPrivateKey); } @Test public void checkNoEmailOnAutomaticLoginAttempt() throws Exception { long old = manager.emailDao.countOf(); // try authenticating with an unknown device, non automatic. // it should send an email try { tryLogin(testIdentity, manager, "Device1", testIdentityLoginToken, testIdentityLoginTokenSignature, null, false); fail("should have thrown"); } catch (DoEmailVerificationException e) { assertEquals(++old, manager.emailDao.countOf()); } try { tryLogin(testIdentity, manager, "Device1", testIdentityLoginToken, testIdentityLoginTokenSignature, null, true); fail("should have thrown"); } catch (DoEmailVerificationException e) { // no new email sent here. assertEquals(old, manager.emailDao.countOf()); } } @Test public void checkVerificationEmailsDebounced() throws Exception { long old = manager.emailDao.countOf(); // try authenticating with an unknown device, non automatic. // it should send an email try { tryLogin(testIdentity, manager, "Device1", testIdentityLoginToken, testIdentityLoginTokenSignature, null, false); fail("should have thrown"); } catch (DoEmailVerificationException e) { assertEquals(++old, manager.emailDao.countOf()); } // try again // it should not send an email try { tryLogin(testIdentity, manager, "Device1", testIdentityLoginToken, testIdentityLoginTokenSignature, null, false); fail("should have thrown"); } catch (DoEmailVerificationException e) { assertEquals(old, manager.emailDao.countOf()); } } // TODO: Merge with simpleLoginWithoutTwoFactorAuth @Test public void loginWithoutTwoFactorAuth() throws Exception { try { tryLogin(testIdentity2, manager, null, null, null, null, false); fail("should have thrown"); } catch (DoEmailVerificationException e) { ; } try { tryLogin(testIdentity2, manager, "dev2", null, null, null, false); fail("should have thrown"); } catch (DoEmailVerificationException e) { ; } authorizeIdentityForDevice(testIdentity2, "dev2"); tryLogin(testIdentity2, manager, "dev2", null, null, null, false); } private GetMyPrivateKeyResponse getPrivateKeyWith2FA(String code) throws IOException, SQLException, MitroServletException { return tryLogin(testIdentity, manager, DEVICE_ID, null, null, code, false); } @Test public void twoFactorLoginWithCorrectCode() throws Exception { assertNotNull(twoFactorData.validTimeCode); GetMyPrivateKeyResponse response = getPrivateKeyWith2FA(twoFactorData.validTimeCode); assertEquals(testIdentity.getEncryptedPrivateKeyString(), response.encryptedPrivateKey); } @Test(expected=DoTwoFactorAuthException.class) public void twoFactorLoginWithIncorrectCode() throws Exception { // TODO: 7 chars so this cannot equal a valid code or backup code final String BADCODE = "0123456"; // fails with DoTwoFactorAuthException getPrivateKeyWith2FA(BADCODE); } @Test(expected=DoTwoFactorAuthException.class) public void twoFactorLoginWithoutCode() throws Exception { // fails with DoTwoFactorAuthException tryLogin(testIdentity, manager, DEVICE_ID, null, null, null, false); } }