From 990757eda35116723e769f84cf8c4de997d3224c Mon Sep 17 00:00:00 2001
From: Chris Lemaire <c.lemaire@student.tudelft.nl>
Date: Thu, 14 May 2020 10:44:06 +0200
Subject: [PATCH] Add test for TOTPController

Additionally rename a few methods to fit in the new naming scheme and
remove the need to check the availability of key information.
---
 .../labracore/config/WebMvcConfiguration.java |   5 +
 .../labracore/controller/TOTPController.java  |  19 +-
 .../nl/tudelft/labracore/model/TOTPKey.java   |   2 +-
 .../security/AuthorizationService.java        |   2 +-
 .../AuthenticatedPersonParameterResolver.java |   2 +
 .../labracore/service/PermissionService.java  |   2 +-
 .../labracore/service/TOTPService.java        |   4 +-
 .../controller/TOTPControllerTest.java        | 214 ++++++++++++++++++
 .../test/MockUserDetailsService.java          |  35 ++-
 9 files changed, 262 insertions(+), 23 deletions(-)
 create mode 100644 src/test/java/nl/tudelft/labracore/controller/TOTPControllerTest.java

diff --git a/src/main/java/nl/tudelft/labracore/config/WebMvcConfiguration.java b/src/main/java/nl/tudelft/labracore/config/WebMvcConfiguration.java
index 4d78ea94..c1a28293 100644
--- a/src/main/java/nl/tudelft/labracore/config/WebMvcConfiguration.java
+++ b/src/main/java/nl/tudelft/labracore/config/WebMvcConfiguration.java
@@ -19,6 +19,7 @@ package nl.tudelft.labracore.config;
 
 import java.util.List;
 
+import nl.tudelft.labracore.security.user.AuthenticatedPersonParameterResolver;
 import nl.tudelft.labracore.versioning.VersionArgumentResolver;
 
 import org.springframework.beans.factory.annotation.Autowired;
@@ -31,8 +32,12 @@ public class WebMvcConfiguration implements WebMvcConfigurer {
 	@Autowired
 	private VersionArgumentResolver versionArgumentResolver;
 
+	@Autowired
+	private AuthenticatedPersonParameterResolver authenticatedPersonParameterResolver;
+
 	@Override
 	public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
 		resolvers.add(versionArgumentResolver);
+		resolvers.add(authenticatedPersonParameterResolver);
 	}
 }
diff --git a/src/main/java/nl/tudelft/labracore/controller/TOTPController.java b/src/main/java/nl/tudelft/labracore/controller/TOTPController.java
index f238161b..7170b5d6 100644
--- a/src/main/java/nl/tudelft/labracore/controller/TOTPController.java
+++ b/src/main/java/nl/tudelft/labracore/controller/TOTPController.java
@@ -17,8 +17,6 @@
  */
 package nl.tudelft.labracore.controller;
 
-import javax.persistence.EntityNotFoundException;
-
 import nl.tudelft.labracore.dto.view.TOTPKeyInfoViewDTO;
 import nl.tudelft.labracore.model.Person;
 import nl.tudelft.labracore.security.user.AuthenticatedPerson;
@@ -41,7 +39,7 @@ public class TOTPController {
 	 *
 	 * @param person The authenticated Person that wants to set their TOTP key.
 	 */
-	@PreAuthorize("#authorizationService.canCreateTOTPKey()")
+	@PreAuthorize("@authorizationService.canCreateTOTPKey()")
 	@PutMapping("create")
 	public TOTPKeyInfoViewDTO createTOTPKey(@AuthenticatedPerson Person person) {
 		totpService.disableTOTP(person);
@@ -57,9 +55,9 @@ public class TOTPController {
 	 * @param  person The authenticated Person that wants to see their TOTP key.
 	 * @return        The TOTP key as an array of bytes.
 	 */
-	@PreAuthorize("#authorizationService.canReadTOTPKey()")
+	@PreAuthorize("@authorizationService.canReadTOTPKeySecret()")
 	@GetMapping("secret")
-	public String getKey(@AuthenticatedPerson Person person) {
+	public String getTOTPSecret(@AuthenticatedPerson Person person) {
 		return person.getTwoFA().activeTOTPKey().get().getSecret();
 	}
 
@@ -70,12 +68,9 @@ public class TOTPController {
 	 * @return        The TOTP key as an array of bytes.
 	 */
 	@GetMapping("key-info")
-	@PreAuthorize("#authorizationService.canReadTOTPKey()")
+	@PreAuthorize("@authorizationService.canReadTOTPKeyInfo()")
 	public TOTPKeyInfoViewDTO getTOTPKeyInfo(@AuthenticatedPerson Person person) {
-		return View.convert(person.getTwoFA().activeTOTPKey().orElseThrow(
-				() -> new EntityNotFoundException(
-						"Could not find active TOTP Key for user: " + person.getUsername())),
-				TOTPKeyInfoViewDTO.class);
+		return View.convert(person.getTwoFA().activeTOTPKey().get(), TOTPKeyInfoViewDTO.class);
 	}
 
 	/**
@@ -86,9 +81,9 @@ public class TOTPController {
 	 * @param  password The current TOTP password the user entered.
 	 * @return          {@code true} when the password was correct, {@code false} otherwise.
 	 */
-	@PreAuthorize("#authorizationService.canUseTOTPKey()")
+	@PreAuthorize("@authorizationService.canUseTOTPKey()")
 	@PostMapping("confirm")
-	public boolean confirmPassword(@AuthenticatedPerson Person person, int password) {
+	public boolean confirmTOTPPassword(@AuthenticatedPerson Person person, @RequestParam int password) {
 		return totpService.confirmTOTPPassword(person, password);
 	}
 
diff --git a/src/main/java/nl/tudelft/labracore/model/TOTPKey.java b/src/main/java/nl/tudelft/labracore/model/TOTPKey.java
index f4d3660e..7d76c21e 100644
--- a/src/main/java/nl/tudelft/labracore/model/TOTPKey.java
+++ b/src/main/java/nl/tudelft/labracore/model/TOTPKey.java
@@ -81,7 +81,7 @@ public class TOTPKey {
 	}
 
 	/**
-	 * @return The secret of this key decoded into a (160-bit) byte-array.
+	 * @return The secret of this key decoded into a (192-bit) byte-array.
 	 */
 	public byte[] getSecretAsBytes() {
 		return new Base32().decode(secret);
diff --git a/src/main/java/nl/tudelft/labracore/security/AuthorizationService.java b/src/main/java/nl/tudelft/labracore/security/AuthorizationService.java
index bbb70adc..8d3a0ec6 100644
--- a/src/main/java/nl/tudelft/labracore/security/AuthorizationService.java
+++ b/src/main/java/nl/tudelft/labracore/security/AuthorizationService.java
@@ -128,7 +128,7 @@ public class AuthorizationService {
 	/**
 	 * @return Whether the service can see the key secret of the authenticated user.
 	 */
-	public boolean canReadTOTPKey() {
+	public boolean canReadTOTPKeySecret() {
 		return withAuthenticatedUser(person -> {
 			boolean isCurrent = person.getTwoFA().activeTOTPKey()
 					.map(key -> LocalDateTime.now().isBefore(key.getCreatedAt().plusMinutes(5L)))
diff --git a/src/main/java/nl/tudelft/labracore/security/user/AuthenticatedPersonParameterResolver.java b/src/main/java/nl/tudelft/labracore/security/user/AuthenticatedPersonParameterResolver.java
index 04f1d723..ea0f8b5d 100644
--- a/src/main/java/nl/tudelft/labracore/security/user/AuthenticatedPersonParameterResolver.java
+++ b/src/main/java/nl/tudelft/labracore/security/user/AuthenticatedPersonParameterResolver.java
@@ -23,11 +23,13 @@ import nl.tudelft.labracore.security.api.APIUserDetailsWithPerson;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.core.MethodParameter;
 import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Component;
 import org.springframework.web.bind.support.WebDataBinderFactory;
 import org.springframework.web.context.request.NativeWebRequest;
 import org.springframework.web.method.support.HandlerMethodArgumentResolver;
 import org.springframework.web.method.support.ModelAndViewContainer;
 
+@Component
 public class AuthenticatedPersonParameterResolver implements HandlerMethodArgumentResolver {
 	@Autowired
 	private PersonRepository pr;
diff --git a/src/main/java/nl/tudelft/labracore/service/PermissionService.java b/src/main/java/nl/tudelft/labracore/service/PermissionService.java
index f80bccb2..bdcec942 100644
--- a/src/main/java/nl/tudelft/labracore/service/PermissionService.java
+++ b/src/main/java/nl/tudelft/labracore/service/PermissionService.java
@@ -39,7 +39,7 @@ public class PermissionService {
 	private final List<String> permissionsDomains = List.of(
 			"ASSIGNMENT", "BUILDING", "CLUSTER", "COURSE",
 			"EDITION", "MODULE", "PERSON", "ROLE", "ROOM",
-			"SESSION", "STUDENT_GROUP", "MODULE_DIVISION");
+			"SESSION", "STUDENT_GROUP", "MODULE_DIVISION", "TOTP");
 
 	/**
 	 * A list of permission types (the ending of an authority name). Can also be interpreted as the action a
diff --git a/src/main/java/nl/tudelft/labracore/service/TOTPService.java b/src/main/java/nl/tudelft/labracore/service/TOTPService.java
index 03bc7fdb..4ae76e6c 100644
--- a/src/main/java/nl/tudelft/labracore/service/TOTPService.java
+++ b/src/main/java/nl/tudelft/labracore/service/TOTPService.java
@@ -50,7 +50,7 @@ public class TOTPService {
 	private TOTPKeyRepository tkr;
 
 	/**
-	 * Sets up a new TOTP key for the given authenticated {@link Person}. For this, a 160-bit secret is
+	 * Sets up a new TOTP key for the given authenticated {@link Person}. For this, a 192-bit secret is
 	 * generated and stored in a new TOTP key object.
 	 *
 	 * @param  person The authenticated Person for which a key needs to be set up.
@@ -60,7 +60,7 @@ public class TOTPService {
 	public TOTPKey setupTOTP(Person person) {
 		try {
 			KeyGenerator keyGen = KeyGenerator.getInstance("AES");
-			keyGen.init(160);
+			keyGen.init(192);
 			Key key = keyGen.generateKey();
 
 			return tkr.save(new TOTPKey(person, key.getEncoded()));
diff --git a/src/test/java/nl/tudelft/labracore/controller/TOTPControllerTest.java b/src/test/java/nl/tudelft/labracore/controller/TOTPControllerTest.java
new file mode 100644
index 00000000..7c6dc18d
--- /dev/null
+++ b/src/test/java/nl/tudelft/labracore/controller/TOTPControllerTest.java
@@ -0,0 +1,214 @@
+/*
+ * Labracore - A connecting core service for Labrador products
+ * Copyright (C) 2020-  Delft University of Technology
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+package nl.tudelft.labracore.controller;
+
+import static java.time.LocalDateTime.now;
+import static java.time.temporal.ChronoUnit.SECONDS;
+import static nl.tudelft.labracore.test.JsonContentMatcher.jsonContent;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.within;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import java.util.List;
+
+import javax.transaction.Transactional;
+
+import nl.tudelft.labracore.dto.view.PersonViewDTO;
+import nl.tudelft.labracore.dto.view.TOTPKeyInfoViewDTO;
+import nl.tudelft.labracore.model.Person;
+import nl.tudelft.labracore.model.TOTPKey;
+import nl.tudelft.labracore.repository.PersonRepository;
+import nl.tudelft.labracore.service.TOTPService;
+import nl.tudelft.labracore.test.RestControllerTest;
+import nl.tudelft.labracore.test.TestDatabaseLoader;
+import nl.tudelft.librador.dto.view.View;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.SpyBean;
+import org.springframework.security.test.context.support.WithUserDetails;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+
+@Transactional
+@SpringBootTest
+@AutoConfigureMockMvc
+public class TOTPControllerTest extends RestControllerTest {
+
+	@Autowired
+	private MockMvc mvc;
+
+	@Autowired
+	private TestDatabaseLoader db;
+
+	@Autowired
+	private PersonRepository pr;
+
+	@SpyBean
+	private TOTPService ts;
+
+	private Person p;
+	private TOTPKey key;
+
+	@BeforeEach
+	void setup() {
+		p = pr.getOne(db.getVision().getId());
+		key = p.getTwoFA().activeTOTPKey().get();
+	}
+
+	@WithUserDetails("All-access Key:vision")
+	@Test
+	void createTOTPKeyShouldDisableCurrentTOTP() throws Exception {
+		mvc.perform(put("/api/2fa/totp/create").with(csrf()));
+
+		verify(ts).disableTOTP(eq(p));
+	}
+
+	@WithUserDetails("All-access Key:vision")
+	@Test
+	void createTOTPKeyShouldSetupTOTP() throws Exception {
+		mvc.perform(put("/api/2fa/totp/create").with(csrf()));
+
+		verify(ts).setupTOTP(eq(p));
+	}
+
+	@WithUserDetails("All-access Key:vision")
+	@Test
+	void createTOTPKeyShouldCreateNewKeyAndReturnIt() throws Exception {
+		mvc.perform(put("/api/2fa/totp/create").with(csrf()))
+				.andExpect(status().isOk())
+				.andExpect(jsonContent(TOTPKeyInfoViewDTO.class)
+						.test(info -> {
+							assertThat(info.getRetractedAt()).isNull();
+							assertThat(info.getPerson()).isEqualTo(View.convert(p, PersonViewDTO.class));
+							assertThat(info.getCreatedAt()).isCloseTo(now(), within(5, SECONDS));
+						}));
+	}
+
+	@WithUserDetails("All-access Key:vision")
+	@Test
+	void getSecretShouldBeAllowedForNewKey() throws Exception {
+		key.setCreatedAt(now());
+		mvc.perform(get("/api/2fa/totp/secret").with(csrf()))
+				.andExpect(status().isOk());
+	}
+
+	@WithUserDetails("All-access Key:vision")
+	@Test
+	void getSecretShouldNotBeAllowedForOldKey() throws Exception {
+		key.setCreatedAt(now().minusMinutes(5));
+		mvc.perform(get("/api/2fa/totp/secret").with(csrf()))
+				.andExpect(status().isForbidden());
+	}
+
+	@WithUserDetails("All-access Key:vision")
+	@Test
+	void getSecretShouldActuallyReturnSecret() throws Exception {
+		key.setCreatedAt(now());
+		mvc.perform(get("/api/2fa/totp/secret").with(csrf()))
+				.andExpect(status().isOk())
+				.andExpect(content().string(key.getSecret()));
+	}
+
+	@WithUserDetails("All-access Key:antman")
+	@Test
+	void getSecretNonExistentShouldResultInForbidden() throws Exception {
+		mvc.perform(get("/api/2fa/totp/secret").with(csrf()))
+				.andExpect(status().isForbidden());
+	}
+
+	@WithUserDetails("All-access Key:vision")
+	@Test
+	void getKeyInfoShouldResultInInfo() throws Exception {
+		mvc.perform(get("/api/2fa/totp/key-info").with(csrf()))
+				.andExpect(status().isOk())
+				.andExpect(jsonContent(TOTPKeyInfoViewDTO.class)
+						.test(info -> assertThat(info)
+								.isEqualTo(View.convert(key, TOTPKeyInfoViewDTO.class))));
+	}
+
+	@WithUserDetails("All-access Key:antman")
+	@Test
+	void getKeyInfoNonExistentShouldResultIn403() throws Exception {
+		mvc.perform(get("/api/2fa/totp/key-info").with(csrf()))
+				.andExpect(status().isForbidden());
+	}
+
+	@WithUserDetails("All-access Key:vision")
+	@Test
+	void confirmPasswordDelegatesToService() throws Exception {
+		mvc.perform(post("/api/2fa/totp/confirm")
+				.param("password", "123456")
+				.with(csrf()));
+
+		verify(ts).confirmTOTPPassword(eq(p), eq(123456));
+	}
+
+	@WithUserDetails("All-access Key:antman")
+	@Test
+	void confirmPasswordChecksWhetherUserHasKey() throws Exception {
+		mvc.perform(post("/api/2fa/totp/confirm")
+				.param("password", "123456")
+				.with(csrf())).andExpect(status().isForbidden());
+	}
+
+	@WithUserDetails("All-access Key:vision")
+	@Test
+	void checkTOTPEnabledForReturnsTrue() throws Exception {
+		mvc.perform(get("/api/2fa/totp/is-enabled"))
+				.andExpect(status().isOk())
+				.andExpect(content().string("true"));
+	}
+
+	@WithUserDetails("All-access Key:antman")
+	@Test
+	void checkTOTPEnabledForReturnsFalse() throws Exception {
+		mvc.perform(get("/api/2fa/totp/is-enabled"))
+				.andExpect(status().isOk())
+				.andExpect(content().string("false"));
+	}
+
+	@WithUserDetails("No-access Key:vision")
+	@MethodSource("protectedEndpoints")
+	@ParameterizedTest
+	void endpointsShouldBeAuthorityProtected(MockHttpServletRequestBuilder request) throws Exception {
+		mvc.perform(request.with(csrf()))
+				.andExpect(status().isForbidden());
+	}
+
+	private static List<MockHttpServletRequestBuilder> protectedEndpoints() throws JsonProcessingException {
+		return List.of(
+				put("/api/2fa/totp/create"),
+				get("/api/2fa/totp/secret"),
+				get("/api/2fa/totp/key-info"),
+				//				post("/api/2fa/totp/confirm").param("password", "123456"),
+				get("/api/2fa/totp/is-enabled"));
+	}
+}
diff --git a/src/test/java/nl/tudelft/labracore/test/MockUserDetailsService.java b/src/test/java/nl/tudelft/labracore/test/MockUserDetailsService.java
index 3e59dc6d..fe96ac00 100644
--- a/src/test/java/nl/tudelft/labracore/test/MockUserDetailsService.java
+++ b/src/test/java/nl/tudelft/labracore/test/MockUserDetailsService.java
@@ -20,8 +20,11 @@ package nl.tudelft.labracore.test;
 import java.util.stream.Collectors;
 
 import nl.tudelft.labracore.model.APIKey;
+import nl.tudelft.labracore.model.Person;
 import nl.tudelft.labracore.repository.APIKeyRepository;
+import nl.tudelft.labracore.repository.PersonRepository;
 import nl.tudelft.labracore.security.api.APIUserDetails;
+import nl.tudelft.labracore.security.api.APIUserDetailsWithPerson;
 
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.core.authority.SimpleGrantedAuthority;
@@ -33,17 +36,37 @@ public class MockUserDetailsService implements UserDetailsService {
 	@Autowired
 	private APIKeyRepository apiKeyRepository;
 
+	@Autowired
+	private PersonRepository personRepository;
+
 	@Override
 	public UserDetails loadUserByUsername(String username)
 			throws UsernameNotFoundException {
-		APIKey key = apiKeyRepository.findByName(username);
+		String keyName = username;
+		Person authUser = null;
+		if (username.contains(":")) {
+			keyName = username.split("[:]")[0];
+
+			String u = username.split("[:]")[1];
+			authUser = personRepository.findByUsernameOrThrow(u);
+		}
+
+		APIKey key = apiKeyRepository.findByName(keyName);
 		if (key == null) {
-			throw new UsernameNotFoundException("Could not key with name: " + username);
+			throw new UsernameNotFoundException("Could not find key with name: " + keyName);
 		}
 
-		return new APIUserDetails(key,
-				key.getPermissions().stream()
-						.map(p -> new SimpleGrantedAuthority(p.getName()))
-						.collect(Collectors.toList()));
+		if (authUser == null) {
+			return new APIUserDetails(key,
+					key.getPermissions().stream()
+							.map(p -> new SimpleGrantedAuthority(p.getName()))
+							.collect(Collectors.toList()));
+		} else {
+			return new APIUserDetailsWithPerson(key,
+					key.getPermissions().stream()
+							.map(p -> new SimpleGrantedAuthority(p.getName()))
+							.collect(Collectors.toList()),
+					authUser);
+		}
 	}
 }
-- 
GitLab