diff --git a/src/main/java/org/whispersystems/textsecuregcm/auth/StoredVerificationCode.java b/src/main/java/org/whispersystems/textsecuregcm/auth/StoredVerificationCode.java new file mode 100644 index 000000000..6dad07ecc --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/auth/StoredVerificationCode.java @@ -0,0 +1,39 @@ +package org.whispersystems.textsecuregcm.auth; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.security.MessageDigest; +import java.util.concurrent.TimeUnit; + +public class StoredVerificationCode { + + @JsonProperty + private final String code; + + @JsonProperty + private final long timestamp; + + public StoredVerificationCode(String code, long timestamp) { + this.code = code; + this.timestamp = timestamp; + } + + public String getCode() { + return code; + } + + public long getTimestamp() { + return timestamp; + } + + public boolean isValid(String theirCodeString) { + if (timestamp + TimeUnit.MINUTES.toMillis(30) < System.currentTimeMillis()) { + return false; + } + + byte[] ourCode = code.getBytes(); + byte[] theirCode = theirCodeString.getBytes(); + + return MessageDigest.isEqual(ourCode, theirCode); + } +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java b/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java index d8793b2ec..d999f6da3 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java +++ b/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java @@ -29,6 +29,7 @@ import org.whispersystems.textsecuregcm.auth.AuthorizationHeader; import org.whispersystems.textsecuregcm.auth.AuthorizationToken; import org.whispersystems.textsecuregcm.auth.AuthorizationTokenGenerator; import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException; +import org.whispersystems.textsecuregcm.auth.StoredVerificationCode; import org.whispersystems.textsecuregcm.auth.TurnToken; import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator; import org.whispersystems.textsecuregcm.entities.AccountAttributes; @@ -137,8 +138,11 @@ public class AccountController { throw new WebApplicationException(Response.status(422).build()); } - VerificationCode verificationCode = generateVerificationCode(number); - pendingAccounts.store(number, verificationCode.getVerificationCode()); + VerificationCode verificationCode = generateVerificationCode(number); + StoredVerificationCode storedVerificationCode = new StoredVerificationCode(verificationCode.getVerificationCode(), + System.currentTimeMillis()); + + pendingAccounts.store(number, storedVerificationCode); if (testDevices.containsKey(number)) { // noop @@ -168,11 +172,9 @@ public class AccountController { rateLimiters.getVerifyLimiter().validate(number); - Optional storedVerificationCode = pendingAccounts.getCodeForNumber(number); + Optional storedVerificationCode = pendingAccounts.getCodeForNumber(number); - if (!storedVerificationCode.isPresent() || - !verificationCode.equals(storedVerificationCode.get())) - { + if (!storedVerificationCode.isPresent() || !storedVerificationCode.get().isValid(verificationCode)) { throw new WebApplicationException(Response.status(403).build()); } diff --git a/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java b/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java index 22aa7e1bb..7bf6bdb85 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java +++ b/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java @@ -24,6 +24,7 @@ import org.slf4j.LoggerFactory; import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials; import org.whispersystems.textsecuregcm.auth.AuthorizationHeader; import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException; +import org.whispersystems.textsecuregcm.auth.StoredVerificationCode; import org.whispersystems.textsecuregcm.entities.AccountAttributes; import org.whispersystems.textsecuregcm.entities.DeviceInfo; import org.whispersystems.textsecuregcm.entities.DeviceInfoList; @@ -49,7 +50,6 @@ import javax.ws.rs.Produces; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.LinkedList; @@ -134,8 +134,11 @@ public class DeviceController { throw new WebApplicationException(Response.Status.UNAUTHORIZED); } - VerificationCode verificationCode = generateVerificationCode(); - pendingDevices.store(account.getNumber(), verificationCode.getVerificationCode()); + VerificationCode verificationCode = generateVerificationCode(); + StoredVerificationCode storedVerificationCode = new StoredVerificationCode(verificationCode.getVerificationCode(), + System.currentTimeMillis()); + + pendingDevices.store(account.getNumber(), storedVerificationCode); return verificationCode; } @@ -157,11 +160,9 @@ public class DeviceController { rateLimiters.getVerifyDeviceLimiter().validate(number); - Optional storedVerificationCode = pendingDevices.getCodeForNumber(number); + Optional storedVerificationCode = pendingDevices.getCodeForNumber(number); - if (!storedVerificationCode.isPresent() || - !MessageDigest.isEqual(verificationCode.getBytes(), storedVerificationCode.get().getBytes())) - { + if (!storedVerificationCode.isPresent() || !storedVerificationCode.get().isValid(verificationCode)) { throw new WebApplicationException(Response.status(403).build()); } @@ -171,7 +172,13 @@ public class DeviceController { throw new WebApplicationException(Response.status(403).build()); } - if (account.get().getActiveDeviceCount() >= MAX_DEVICES) { + int maxDeviceLimit = MAX_DEVICES; + + if (maxDeviceConfiguration.containsKey(account.get().getNumber())) { + maxDeviceLimit = maxDeviceConfiguration.get(account.get().getNumber()); + } + + if (account.get().getActiveDeviceCount() >= maxDeviceLimit) { throw new DeviceLimitExceededException(account.get().getDevices().size(), MAX_DEVICES); } diff --git a/src/main/java/org/whispersystems/textsecuregcm/storage/PendingAccounts.java b/src/main/java/org/whispersystems/textsecuregcm/storage/PendingAccounts.java index 7b943a190..2cd45b5b8 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/storage/PendingAccounts.java +++ b/src/main/java/org/whispersystems/textsecuregcm/storage/PendingAccounts.java @@ -16,22 +16,40 @@ */ package org.whispersystems.textsecuregcm.storage; +import org.skife.jdbi.v2.StatementContext; import org.skife.jdbi.v2.sqlobject.Bind; import org.skife.jdbi.v2.sqlobject.SqlQuery; import org.skife.jdbi.v2.sqlobject.SqlUpdate; +import org.skife.jdbi.v2.sqlobject.customizers.Mapper; +import org.skife.jdbi.v2.tweak.ResultSetMapper; +import org.whispersystems.textsecuregcm.auth.StoredVerificationCode; + +import java.sql.ResultSet; +import java.sql.SQLException; public interface PendingAccounts { - @SqlUpdate("WITH upsert AS (UPDATE pending_accounts SET verification_code = :verification_code WHERE number = :number RETURNING *) " + - "INSERT INTO pending_accounts (number, verification_code) SELECT :number, :verification_code WHERE NOT EXISTS (SELECT * FROM upsert)") - void insert(@Bind("number") String number, @Bind("verification_code") String verificationCode); + @SqlUpdate("WITH upsert AS (UPDATE pending_accounts SET verification_code = :verification_code, timestamp = :timestamp WHERE number = :number RETURNING *) " + + "INSERT INTO pending_accounts (number, verification_code, timestamp) SELECT :number, :verification_code, :timestamp WHERE NOT EXISTS (SELECT * FROM upsert)") + void insert(@Bind("number") String number, @Bind("verification_code") String verificationCode, @Bind("timestamp") long timestamp); - @SqlQuery("SELECT verification_code FROM pending_accounts WHERE number = :number") - String getCodeForNumber(@Bind("number") String number); + @Mapper(StoredVerificationCodeMapper.class) + @SqlQuery("SELECT verification_code, timestamp FROM pending_accounts WHERE number = :number") + StoredVerificationCode getCodeForNumber(@Bind("number") String number); @SqlUpdate("DELETE FROM pending_accounts WHERE number = :number") void remove(@Bind("number") String number); @SqlUpdate("VACUUM pending_accounts") public void vacuum(); + + public static class StoredVerificationCodeMapper implements ResultSetMapper { + @Override + public StoredVerificationCode map(int i, ResultSet resultSet, StatementContext statementContext) + throws SQLException + { + return new StoredVerificationCode(resultSet.getString("verification_code"), + resultSet.getLong("timestamp")); + } + } } diff --git a/src/main/java/org/whispersystems/textsecuregcm/storage/PendingAccountsManager.java b/src/main/java/org/whispersystems/textsecuregcm/storage/PendingAccountsManager.java index 3c23e5d2e..e899e7ce4 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/storage/PendingAccountsManager.java +++ b/src/main/java/org/whispersystems/textsecuregcm/storage/PendingAccountsManager.java @@ -16,27 +16,40 @@ */ package org.whispersystems.textsecuregcm.storage; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.auth.StoredVerificationCode; +import org.whispersystems.textsecuregcm.util.SystemMapper; + +import java.io.IOException; + import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; public class PendingAccountsManager { - private static final String CACHE_PREFIX = "pending_account::"; + private final Logger logger = LoggerFactory.getLogger(PendingAccountsManager.class); + + private static final String CACHE_PREFIX = "pending_account2::"; private final PendingAccounts pendingAccounts; private final JedisPool cacheClient; + private final ObjectMapper mapper; public PendingAccountsManager(PendingAccounts pendingAccounts, JedisPool cacheClient) { this.pendingAccounts = pendingAccounts; this.cacheClient = cacheClient; + this.mapper = SystemMapper.getMapper(); } - public void store(String number, String code) { + public void store(String number, StoredVerificationCode code) { memcacheSet(number, code); - pendingAccounts.insert(number, code); + pendingAccounts.insert(number, code.getCode(), code.getTimestamp()); } public void remove(String number) { @@ -44,8 +57,8 @@ public class PendingAccountsManager { pendingAccounts.remove(number); } - public Optional getCodeForNumber(String number) { - Optional code = memcacheGet(number); + public Optional getCodeForNumber(String number) { + Optional code = memcacheGet(number); if (!code.isPresent()) { code = Optional.fromNullable(pendingAccounts.getCodeForNumber(number)); @@ -58,15 +71,23 @@ public class PendingAccountsManager { return code; } - private void memcacheSet(String number, String code) { + private void memcacheSet(String number, StoredVerificationCode code) { try (Jedis jedis = cacheClient.getResource()) { - jedis.set(CACHE_PREFIX + number, code); + jedis.set(CACHE_PREFIX + number, mapper.writeValueAsString(code)); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException(e); } } - private Optional memcacheGet(String number) { + private Optional memcacheGet(String number) { try (Jedis jedis = cacheClient.getResource()) { - return Optional.fromNullable(jedis.get(CACHE_PREFIX + number)); + String json = jedis.get(CACHE_PREFIX + number); + + if (json == null) return Optional.absent(); + else return Optional.of(mapper.readValue(json, StoredVerificationCode.class)); + } catch (IOException e) { + logger.warn("PendingAccountsManager", "Error deserializing value..."); + return Optional.absent(); } } diff --git a/src/main/java/org/whispersystems/textsecuregcm/storage/PendingDevices.java b/src/main/java/org/whispersystems/textsecuregcm/storage/PendingDevices.java index 5ceff5bcc..74a1d459c 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/storage/PendingDevices.java +++ b/src/main/java/org/whispersystems/textsecuregcm/storage/PendingDevices.java @@ -16,19 +16,38 @@ */ package org.whispersystems.textsecuregcm.storage; +import org.skife.jdbi.v2.StatementContext; import org.skife.jdbi.v2.sqlobject.Bind; import org.skife.jdbi.v2.sqlobject.SqlQuery; import org.skife.jdbi.v2.sqlobject.SqlUpdate; +import org.skife.jdbi.v2.sqlobject.customizers.Mapper; +import org.skife.jdbi.v2.tweak.ResultSetMapper; +import org.whispersystems.textsecuregcm.auth.StoredVerificationCode; + +import java.sql.ResultSet; +import java.sql.SQLException; public interface PendingDevices { - @SqlUpdate("WITH upsert AS (UPDATE pending_devices SET verification_code = :verification_code WHERE number = :number RETURNING *) " + - "INSERT INTO pending_devices (number, verification_code) SELECT :number, :verification_code WHERE NOT EXISTS (SELECT * FROM upsert)") - void insert(@Bind("number") String number, @Bind("verification_code") String verificationCode); + @SqlUpdate("WITH upsert AS (UPDATE pending_devices SET verification_code = :verification_code, timestamp = :timestamp WHERE number = :number RETURNING *) " + + "INSERT INTO pending_devices (number, verification_code, timestamp) SELECT :number, :verification_code, :timestamp WHERE NOT EXISTS (SELECT * FROM upsert)") + void insert(@Bind("number") String number, @Bind("verification_code") String verificationCode, @Bind("timestamp") long timestamp); - @SqlQuery("SELECT verification_code FROM pending_devices WHERE number = :number") - String getCodeForNumber(@Bind("number") String number); + @Mapper(StoredVerificationCodeMapper.class) + @SqlQuery("SELECT verification_code, timestamp FROM pending_devices WHERE number = :number") + StoredVerificationCode getCodeForNumber(@Bind("number") String number); @SqlUpdate("DELETE FROM pending_devices WHERE number = :number") void remove(@Bind("number") String number); + + public static class StoredVerificationCodeMapper implements ResultSetMapper { + @Override + public StoredVerificationCode map(int i, ResultSet resultSet, StatementContext statementContext) + throws SQLException + { + return new StoredVerificationCode(resultSet.getString("verification_code"), + resultSet.getLong("timestamp")); + } + } + } diff --git a/src/main/java/org/whispersystems/textsecuregcm/storage/PendingDevicesManager.java b/src/main/java/org/whispersystems/textsecuregcm/storage/PendingDevicesManager.java index dbac8c462..20e418542 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/storage/PendingDevicesManager.java +++ b/src/main/java/org/whispersystems/textsecuregcm/storage/PendingDevicesManager.java @@ -16,28 +16,40 @@ */ package org.whispersystems.textsecuregcm.storage; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.auth.StoredVerificationCode; +import org.whispersystems.textsecuregcm.util.SystemMapper; + +import java.io.IOException; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; public class PendingDevicesManager { - private static final String CACHE_PREFIX = "pending_devices::"; + private final Logger logger = LoggerFactory.getLogger(PendingDevicesManager.class); + + private static final String CACHE_PREFIX = "pending_devices2::"; private final PendingDevices pendingDevices; private final JedisPool cacheClient; + private final ObjectMapper mapper; public PendingDevicesManager(PendingDevices pendingDevices, JedisPool cacheClient) { this.pendingDevices = pendingDevices; this.cacheClient = cacheClient; + this.mapper = SystemMapper.getMapper(); } - public void store(String number, String code) { + public void store(String number, StoredVerificationCode code) { memcacheSet(number, code); - pendingDevices.insert(number, code); + pendingDevices.insert(number, code.getCode(), code.getTimestamp()); } public void remove(String number) { @@ -45,8 +57,8 @@ public class PendingDevicesManager { pendingDevices.remove(number); } - public Optional getCodeForNumber(String number) { - Optional code = memcacheGet(number); + public Optional getCodeForNumber(String number) { + Optional code = memcacheGet(number); if (!code.isPresent()) { code = Optional.fromNullable(pendingDevices.getCodeForNumber(number)); @@ -59,15 +71,23 @@ public class PendingDevicesManager { return code; } - private void memcacheSet(String number, String code) { + private void memcacheSet(String number, StoredVerificationCode code) { try (Jedis jedis = cacheClient.getResource()) { - jedis.set(CACHE_PREFIX + number, code); + jedis.set(CACHE_PREFIX + number, mapper.writeValueAsString(code)); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException(e); } } - private Optional memcacheGet(String number) { + private Optional memcacheGet(String number) { try (Jedis jedis = cacheClient.getResource()) { - return Optional.fromNullable(jedis.get(CACHE_PREFIX + number)); + String json = jedis.get(CACHE_PREFIX + number); + + if (json == null) return Optional.absent(); + else return Optional.of(mapper.readValue(json, StoredVerificationCode.class)); + } catch (IOException e) { + logger.warn("Could not parse pending device stored verification json"); + return Optional.absent(); } } diff --git a/src/main/resources/accountsdb.xml b/src/main/resources/accountsdb.xml index e6d225c32..f2a9c315c 100644 --- a/src/main/resources/accountsdb.xml +++ b/src/main/resources/accountsdb.xml @@ -4,7 +4,8 @@ xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog - http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-2.0.xsd"> + http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-2.0.xsd" + logicalFilePath="migrations.xml"> @@ -171,4 +172,18 @@ + + + + + + + + + + + + + + diff --git a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java index 8855a3e86..f8b927ee1 100644 --- a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java +++ b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java @@ -8,6 +8,7 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider; +import org.whispersystems.textsecuregcm.auth.StoredVerificationCode; import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator; import org.whispersystems.textsecuregcm.controllers.AccountController; import org.whispersystems.textsecuregcm.entities.AccountAttributes; @@ -26,6 +27,7 @@ import javax.ws.rs.client.Entity; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.util.HashMap; +import java.util.concurrent.TimeUnit; import io.dropwizard.testing.junit.ResourceTestRule; import static org.assertj.core.api.Assertions.assertThat; @@ -34,7 +36,8 @@ import static org.mockito.Mockito.*; public class AccountControllerTest { - private static final String SENDER = "+14152222222"; + private static final String SENDER = "+14152222222"; + private static final String SENDER_OLD = "+14151111111"; private PendingAccountsManager pendingAccountsManager = mock(PendingAccountsManager.class); private AccountsManager accountsManager = mock(AccountsManager.class ); @@ -72,7 +75,8 @@ public class AccountControllerTest { when(timeProvider.getCurrentTimeMillis()).thenReturn(System.currentTimeMillis()); - when(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn(Optional.of("1234")); + when(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn(Optional.of(new StoredVerificationCode("1234", System.currentTimeMillis()))); + when(pendingAccountsManager.getCodeForNumber(SENDER_OLD)).thenReturn(Optional.of(new StoredVerificationCode("1234", System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(31)))); } @Test @@ -117,6 +121,21 @@ public class AccountControllerTest { verify(accountsManager, times(1)).create(isA(Account.class)); } + @Test + public void testVerifyCodeOld() throws Exception { + Response response = + resources.getJerseyTest() + .target(String.format("/v1/accounts/code/%s", "1234")) + .request() + .header("Authorization", AuthHelper.getAuthHeader(SENDER_OLD, "bar")) + .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 2222), + MediaType.APPLICATION_JSON_TYPE)); + + assertThat(response.getStatus()).isEqualTo(403); + + verifyNoMoreInteractions(accountsManager); + } + @Test public void testVerifyBadCode() throws Exception { Response response = diff --git a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DeviceControllerTest.java b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DeviceControllerTest.java index c3836784d..5fc674d3a 100644 --- a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DeviceControllerTest.java +++ b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DeviceControllerTest.java @@ -22,6 +22,7 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider; +import org.whispersystems.textsecuregcm.auth.StoredVerificationCode; import org.whispersystems.textsecuregcm.controllers.DeviceController; import org.whispersystems.textsecuregcm.entities.AccountAttributes; import org.whispersystems.textsecuregcm.entities.DeviceResponse; @@ -39,9 +40,9 @@ import javax.ws.rs.Path; import javax.ws.rs.client.Entity; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; - import java.util.HashMap; import java.util.Map; +import java.util.concurrent.TimeUnit; import io.dropwizard.jersey.validation.ConstraintViolationExceptionMapper; import io.dropwizard.testing.junit.ResourceTestRule; @@ -105,8 +106,8 @@ public class DeviceControllerTest { when(account.getNextDeviceId()).thenReturn(42L); // when(maxedAccount.getActiveDeviceCount()).thenReturn(6); - when(pendingDevicesManager.getCodeForNumber(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of("5678901")); - when(pendingDevicesManager.getCodeForNumber(AuthHelper.VALID_NUMBER_TWO)).thenReturn(Optional.of("1112223")); + when(pendingDevicesManager.getCodeForNumber(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of(new StoredVerificationCode("5678901", System.currentTimeMillis()))); + when(pendingDevicesManager.getCodeForNumber(AuthHelper.VALID_NUMBER_TWO)).thenReturn(Optional.of(new StoredVerificationCode("1112223", System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(31)))); when(accountsManager.get(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of(account)); when(accountsManager.get(AuthHelper.VALID_NUMBER_TWO)).thenReturn(Optional.of(maxedAccount)); } @@ -134,6 +135,38 @@ public class DeviceControllerTest { verify(pendingDevicesManager).remove(AuthHelper.VALID_NUMBER); } + @Test + public void invalidDeviceRegisterTest() throws Exception { + VerificationCode deviceCode = resources.getJerseyTest() + .target("/v1/devices/provisioning/code") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) + .get(VerificationCode.class); + + assertThat(deviceCode).isEqualTo(new VerificationCode(5678901)); + + Response response = resources.getJerseyTest() + .target("/v1/devices/5678902") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, "password1")) + .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 1234), + MediaType.APPLICATION_JSON_TYPE)); + + assertThat(response.getStatus()).isEqualTo(403); + } + + @Test + public void oldDeviceRegisterTest() throws Exception { + Response response = resources.getJerseyTest() + .target("/v1/devices/1112223") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER_TWO, AuthHelper.VALID_PASSWORD_TWO)) + .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 1234), + MediaType.APPLICATION_JSON_TYPE)); + + assertThat(response.getStatus()).isEqualTo(403); + } + @Test public void maxDevicesTest() throws Exception { Response response = resources.getJerseyTest()