From 2cf1d821888adaf8f76cc7eb9a3ae951b5d8406f Mon Sep 17 00:00:00 2001 From: Jeffrey Bakker Date: Thu, 3 Aug 2017 20:06:36 +0200 Subject: [PATCH 1/3] implement the blocking of cards after 3 consecutive failed attempts --- .../honours/ing/banq/auth/AuthService.java | 2 +- .../ing/banq/auth/AuthServiceImpl.java | 12 +++++++- .../ing/banq/auth/CardBlockedError.java | 28 +++++++++++++++++++ src/main/java/honours/ing/banq/card/Card.java | 22 +++++++++++++++ .../ing/banq/redirect/RedirectService.java | 10 ++++--- 5 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 src/main/java/honours/ing/banq/auth/CardBlockedError.java diff --git a/src/main/java/honours/ing/banq/auth/AuthService.java b/src/main/java/honours/ing/banq/auth/AuthService.java index 33a9f68..8dcbd6a 100644 --- a/src/main/java/honours/ing/banq/auth/AuthService.java +++ b/src/main/java/honours/ing/banq/auth/AuthService.java @@ -17,6 +17,6 @@ public interface AuthService { void deleteForCustomer(Customer customer); Customer getAuthorizedCustomer(String token) throws NotAuthorizedError; - BankAccount getAuthorizedAccount(String iBAN, String pinCard, String pinCode) throws InvalidPINError; + BankAccount getAuthorizedAccount(String iBAN, String pinCard, String pinCode) throws InvalidPINError, CardBlockedError; } diff --git a/src/main/java/honours/ing/banq/auth/AuthServiceImpl.java b/src/main/java/honours/ing/banq/auth/AuthServiceImpl.java index aac7f4f..fc0eb9c 100644 --- a/src/main/java/honours/ing/banq/auth/AuthServiceImpl.java +++ b/src/main/java/honours/ing/banq/auth/AuthServiceImpl.java @@ -92,7 +92,7 @@ public Customer getAuthorizedCustomer(String token) throws NotAuthorizedError { } @Override - public BankAccount getAuthorizedAccount(String iBAN, String pinCard, String pinCode) throws InvalidPINError { + public BankAccount getAuthorizedAccount(String iBAN, String pinCard, String pinCode) throws InvalidPINError, CardBlockedError { if (iBAN == null || iBAN.length() <= 8 || pinCard == null || pinCode == null) { throw new InvalidParamValueError("One of the parameters is null or the IBAN is not long enough"); } @@ -106,8 +106,18 @@ public BankAccount getAuthorizedAccount(String iBAN, String pinCard, String pinC throw new InvalidParamValueError("Card does not exist"); } + if (card.isBlocked()) { + throw new CardBlockedError( + "There have been " + card.getFailedAttempts() + " consecutive failed attempts."); + } + if (!card.getPin().equals(pinCode)) { + card.addFailedAttempt(); + cardRepository.save(card); throw new InvalidPINError(); + } else if (card.getFailedAttempts() > 0) { + card.resetAttempts(); + cardRepository.save(card); } return account; diff --git a/src/main/java/honours/ing/banq/auth/CardBlockedError.java b/src/main/java/honours/ing/banq/auth/CardBlockedError.java new file mode 100644 index 0000000..c790337 --- /dev/null +++ b/src/main/java/honours/ing/banq/auth/CardBlockedError.java @@ -0,0 +1,28 @@ +package honours.ing.banq.auth; + +/** + * @author jeffrey + * @since 3-8-17 + */ +public class CardBlockedError extends Exception { + + public CardBlockedError() { + super(); + } + + public CardBlockedError(String s) { + super(s); + } + + public CardBlockedError(String s, Throwable throwable) { + super(s, throwable); + } + + public CardBlockedError(Throwable throwable) { + super(throwable); + } + + protected CardBlockedError(String s, Throwable throwable, boolean b, boolean b1) { + super(s, throwable, b, b1); + } +} diff --git a/src/main/java/honours/ing/banq/card/Card.java b/src/main/java/honours/ing/banq/card/Card.java index 7faa20a..b0b117e 100644 --- a/src/main/java/honours/ing/banq/card/Card.java +++ b/src/main/java/honours/ing/banq/card/Card.java @@ -15,6 +15,8 @@ @Entity public class Card { + private static final int PIN_ATTEMPTS_BEFORE_BLOCK = 3; + @SuppressWarnings("NumericOverflow") private static final long DURABILITY = 1000 * 60 * 60 * 24 * 365 * 5; // 5 years @@ -33,6 +35,8 @@ public class Card { private String pin; // TODO: add hashing private Date expirationDate; + private int failedAttempts; + /** * @deprecated empty constructor for spring */ @@ -48,6 +52,8 @@ public Card(Customer holder, BankAccount account, String cardNumber) { Calendar expiration = Calendar.getInstance(); expiration.setTimeInMillis(System.currentTimeMillis() + DURABILITY); expirationDate = expiration.getTime(); + + failedAttempts = 0; } public Integer getId() { @@ -74,6 +80,22 @@ public Date getExpirationDate() { return expirationDate; } + public int getFailedAttempts() { + return failedAttempts; + } + + public boolean isBlocked() { + return failedAttempts >= PIN_ATTEMPTS_BEFORE_BLOCK; + } + + public void addFailedAttempt() { + failedAttempts++; + } + + public void resetAttempts() { + failedAttempts = 0; + } + private static String generatePin() { StringBuilder res = new StringBuilder(); Random rnd = new Random(); diff --git a/src/main/java/honours/ing/banq/redirect/RedirectService.java b/src/main/java/honours/ing/banq/redirect/RedirectService.java index ed3fd3b..9e29d7e 100644 --- a/src/main/java/honours/ing/banq/redirect/RedirectService.java +++ b/src/main/java/honours/ing/banq/redirect/RedirectService.java @@ -69,11 +69,13 @@ public String redirect(@RequestBody String json) { try { Object service = context.getBean(serviceClass); - JsonRpcService annotation = AnnotationUtils.findAnnotation(service.getClass(), - JsonRpcService.class); + JsonRpcService annotation = AnnotationUtils.findAnnotation( + service.getClass(), JsonRpcService.class); + HttpPost httpPost = new HttpPost(DEFAULT_URL + annotation.value()); - StringEntity msg = new StringEntity(json, ContentType.create("application/json", - "UTF-8")); + StringEntity msg = new StringEntity( + json, ContentType.create("application/json", "UTF-8")); + httpPost.setEntity(msg); HttpResponse response = httpclient.execute(httpPost); From c17082d7a6441a887a0d6124f436d0dd2eca070f Mon Sep 17 00:00:00 2001 From: Jeffrey Bakker Date: Thu, 3 Aug 2017 20:16:39 +0200 Subject: [PATCH 2/3] implement the unblockCard method --- .../honours/ing/banq/card/CardService.java | 30 +++++++++ .../ing/banq/card/CardServiceImpl.java | 67 +++++++++++++++++++ .../banq/transaction/TransactionService.java | 5 +- .../transaction/TransactionServiceImpl.java | 5 +- 4 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 src/main/java/honours/ing/banq/card/CardService.java create mode 100644 src/main/java/honours/ing/banq/card/CardServiceImpl.java diff --git a/src/main/java/honours/ing/banq/card/CardService.java b/src/main/java/honours/ing/banq/card/CardService.java new file mode 100644 index 0000000..4d35f0a --- /dev/null +++ b/src/main/java/honours/ing/banq/card/CardService.java @@ -0,0 +1,30 @@ +package honours.ing.banq.card; + +import com.googlecode.jsonrpc4j.JsonRpcParam; +import com.googlecode.jsonrpc4j.JsonRpcService; +import honours.ing.banq.InvalidParamValueError; +import honours.ing.banq.access.NoEffectError; +import honours.ing.banq.auth.NotAuthorizedError; + +/** + * @author jeffrey + * @since 3-8-17 + */ +@JsonRpcService("/api/card") +public interface CardService { + + /** + * A PIN card that has been blocked can be unblocked if the user logs in and calls this unblock card method. + * @param authToken the authentication token, obtained with {@code getAuthToken()} + * @param iBAN the number of the bank account + * @param pinCard the number of the pin card + * @return an empty dictionary if successful + * @throws InvalidParamValueError one or more parameter has an invalid value. See the message + * @throws NotAuthorizedError the authenticated user is not authorized to perform this action + * @throws NoEffectError if the card is not blocked this method will have no effect + */ + Object unblockCard(@JsonRpcParam("authToken") String authToken, @JsonRpcParam("iBAN") String iBAN, + @JsonRpcParam("pinCard") String pinCard) + throws InvalidParamValueError, NotAuthorizedError, NoEffectError; + +} diff --git a/src/main/java/honours/ing/banq/card/CardServiceImpl.java b/src/main/java/honours/ing/banq/card/CardServiceImpl.java new file mode 100644 index 0000000..1088e6b --- /dev/null +++ b/src/main/java/honours/ing/banq/card/CardServiceImpl.java @@ -0,0 +1,67 @@ +package honours.ing.banq.card; + +import com.googlecode.jsonrpc4j.spring.AutoJsonRpcServiceImpl; +import honours.ing.banq.InvalidParamValueError; +import honours.ing.banq.access.NoEffectError; +import honours.ing.banq.account.BankAccount; +import honours.ing.banq.account.BankAccountRepository; +import honours.ing.banq.auth.AuthService; +import honours.ing.banq.auth.NotAuthorizedError; +import honours.ing.banq.customer.Customer; +import honours.ing.banq.util.IBANUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@AutoJsonRpcServiceImpl +@Transactional(readOnly = true) +public class CardServiceImpl implements CardService { + + // Services + @Autowired + private AuthService auth; + + // Repositories + @Autowired + private CardRepository repository; + + @Autowired + private BankAccountRepository accountRepository; + + @Transactional + @Override + public Object unblockCard(String authToken, String iBAN, String pinCard) + throws InvalidParamValueError, NotAuthorizedError, NoEffectError { + Customer customer = auth.getAuthorizedCustomer(authToken); + + // Retrieve the bank account and check whether we are authorized to access it + BankAccount account = accountRepository.findOne((int) IBANUtil.getAccountNumber(iBAN)); + if (account == null) { + throw new InvalidParamValueError("There is no bank account with the given iBAN"); + } + + if (account.getPrimaryHolder() != customer && !account.getHolders().contains(customer)) { + throw new NotAuthorizedError(); + } + + // Retrieve the pin card and check whether we are authorized to access it + Card card = repository.findByAccountAndCardNumber(account, pinCard); + if (card == null) { + throw new InvalidParamValueError("There is no pin card with the given card number"); + } + + if (card.getHolder() != customer) { + throw new NotAuthorizedError(); + } + + if (!card.isBlocked()) { + throw new NoEffectError(); + } + + card.resetAttempts(); + repository.save(card); + + return new Object(); + } +} diff --git a/src/main/java/honours/ing/banq/transaction/TransactionService.java b/src/main/java/honours/ing/banq/transaction/TransactionService.java index 565d765..3b9c99d 100644 --- a/src/main/java/honours/ing/banq/transaction/TransactionService.java +++ b/src/main/java/honours/ing/banq/transaction/TransactionService.java @@ -4,6 +4,7 @@ import com.googlecode.jsonrpc4j.JsonRpcService; import honours.ing.banq.InvalidParamValueError; import honours.ing.banq.account.BankAccount; +import honours.ing.banq.auth.CardBlockedError; import honours.ing.banq.auth.InvalidPINError; import honours.ing.banq.auth.NotAuthorizedError; import honours.ing.banq.card.Card; @@ -30,7 +31,7 @@ void depositIntoAccount(@JsonRpcParam("iBAN") String iBAN, @JsonRpcParam("pinCar pinCard, @JsonRpcParam("pinCode") String pinCode, @JsonRpcParam("amount") Double amount) - throws InvalidParamValueError, InvalidPINError; + throws InvalidParamValueError, InvalidPINError, CardBlockedError; /** * Transfer money to a {@link BankAccount} using a {@link Card} @@ -46,7 +47,7 @@ void depositIntoAccount(@JsonRpcParam("iBAN") String iBAN, @JsonRpcParam("pinCar void payFromAccount(@JsonRpcParam("sourceIBAN") String sourceIBAN, @JsonRpcParam ("targetIBAN") String targetIBAN, @JsonRpcParam("pinCard") String pinCard, @JsonRpcParam("pinCode") String pinCode, @JsonRpcParam("amount") Double - amount) throws InvalidParamValueError, InvalidPINError; + amount) throws InvalidParamValueError, InvalidPINError, CardBlockedError; /** * Transer money from one {@link BankAccount} to another {@link BankAccount} diff --git a/src/main/java/honours/ing/banq/transaction/TransactionServiceImpl.java b/src/main/java/honours/ing/banq/transaction/TransactionServiceImpl.java index cd461d9..c6f7002 100644 --- a/src/main/java/honours/ing/banq/transaction/TransactionServiceImpl.java +++ b/src/main/java/honours/ing/banq/transaction/TransactionServiceImpl.java @@ -5,6 +5,7 @@ import honours.ing.banq.account.BankAccount; import honours.ing.banq.account.BankAccountRepository; import honours.ing.banq.auth.AuthService; +import honours.ing.banq.auth.CardBlockedError; import honours.ing.banq.auth.InvalidPINError; import honours.ing.banq.auth.NotAuthorizedError; import honours.ing.banq.card.Card; @@ -45,7 +46,7 @@ public class TransactionServiceImpl implements TransactionService { @Override public void depositIntoAccount(String iBAN, String pinCard, String pinCode, Double amount) - throws InvalidParamValueError, InvalidPINError { + throws InvalidParamValueError, InvalidPINError, CardBlockedError { if (!IBANUtil.isValidIBAN(iBAN)) { throw new InvalidParamValueError("The given IBAN is not valid."); } @@ -69,7 +70,7 @@ public void depositIntoAccount(String iBAN, String pinCard, String pinCode, Doub @Override public void payFromAccount(String sourceIBAN, String targetIBAN, String pinCard, String pinCode, Double amount) - throws InvalidParamValueError, InvalidPINError { + throws InvalidParamValueError, InvalidPINError, CardBlockedError { if (!IBANUtil.isValidIBAN(sourceIBAN)) { throw new InvalidParamValueError("The given source IBAN is not valid."); } From 95b14b08f655129f12b0880df5f52ef173012c79 Mon Sep 17 00:00:00 2001 From: Jeffrey Bakker Date: Thu, 3 Aug 2017 20:41:32 +0200 Subject: [PATCH 3/3] add tests for the newly implemented code for Extension 2 --- .../honours/ing/banq/BoilerplateTest.java | 22 +++++---- .../ing/banq/card/CardServiceTest.java | 47 +++++++++++++++++++ 2 files changed, 60 insertions(+), 9 deletions(-) create mode 100644 src/test/java/honours/ing/banq/card/CardServiceTest.java diff --git a/src/test/java/honours/ing/banq/BoilerplateTest.java b/src/test/java/honours/ing/banq/BoilerplateTest.java index f2904ac..c4ef8b6 100644 --- a/src/test/java/honours/ing/banq/BoilerplateTest.java +++ b/src/test/java/honours/ing/banq/BoilerplateTest.java @@ -46,19 +46,23 @@ public class BoilerplateTest { @Before public void setUp() throws Exception { - account1 = new AccountInfo(bankAccountService.openAccount("Jan", "Jansen", "J.", "1996-1-1", - "1234567890", "Klaverstraat 1", "0612345678", "janjansen@gmail.com", "jantje96", - "1234"), "jantje96", "1234"); - account2 = new AccountInfo(bankAccountService.openAccount("Piet", "Pietersen", "p.p", "1998-8-8", - "012345789", "Huisstraat 1", "0607080910", "piet@gmail.com", "piet1", "1234"), "piet1", "1234"); + account1 = new AccountInfo( + bankAccountService.openAccount( + "Jan", "Jansen", "J.", "1996-1-1", + "1234567890", "Klaverstraat 1", "0612345678", + "janjansen@gmail.com", "jantje96", "1234"), + "jantje96", "1234"); + account2 = new AccountInfo( + bankAccountService.openAccount("Piet", "Pietersen", "p.p", "1998-8-8", + "012345789", "Huisstraat 1", "0607080910", + "piet@gmail.com", "piet1", "1234"), + "piet1", "1234"); account1.token = authService.getAuthToken("jantje96", "1234").getAuthToken(); account2.token = authService.getAuthToken("piet1", "1234").getAuthToken(); - assertThat(infoService.getBalance(account1.token, account1.iBan).getBalance(), equalTo - (0d)); - assertThat(infoService.getBalance(account2.token, account2.iBan).getBalance(), equalTo - (0d)); + assertThat(infoService.getBalance(account1.token, account1.iBan).getBalance(), equalTo(0d)); + assertThat(infoService.getBalance(account2.token, account2.iBan).getBalance(), equalTo(0d)); } @After diff --git a/src/test/java/honours/ing/banq/card/CardServiceTest.java b/src/test/java/honours/ing/banq/card/CardServiceTest.java new file mode 100644 index 0000000..c48c784 --- /dev/null +++ b/src/test/java/honours/ing/banq/card/CardServiceTest.java @@ -0,0 +1,47 @@ +package honours.ing.banq.card; + +import honours.ing.banq.BoilerplateTest; +import honours.ing.banq.auth.CardBlockedError; +import honours.ing.banq.auth.InvalidPINError; +import honours.ing.banq.transaction.TransactionService; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import static org.junit.Assert.*; + +/** + * @author jeffrey + * @since 3-8-17 + */ +public class CardServiceTest extends BoilerplateTest { + + @Autowired + private CardService service; + + @Autowired + private TransactionService transactionService; + + @Test(expected = CardBlockedError.class) + public void blockCard() throws Exception { + for (int i = 0; i < 3; i++) { + try { + transactionService.depositIntoAccount(account1.iBan, account1.cardNumber, "0000", 10.0); + } catch (InvalidPINError ignored) { } + } + + transactionService.depositIntoAccount(account1.iBan, account1.cardNumber, account1.pin, 10.0); + } + + @Test + public void unblockCard() throws Exception { + try { + blockCard(); + } catch (CardBlockedError ignored) { } + + service.unblockCard(account1.token, account1.iBan, account1.cardNumber); + + // Now test if the card is usable again + transactionService.depositIntoAccount(account1.iBan, account1.cardNumber, account1.pin, 10.0); + } + +} \ No newline at end of file