From f30390550befc35f8aad456d117450532aec2290 Mon Sep 17 00:00:00 2001 From: OZGCloud <ozgcloud@mgm-tp.com> Date: Wed, 1 Dec 2021 12:50:26 +0100 Subject: [PATCH] OZG-1614 OZG-1804 OZG-1805 impl link, endpoints and filter for generating jwt token --- goofy-server/pom.xml | 10 ++ .../itvsh/goofy/JwtAuthenticationFilter.java | 58 +++++++++ .../java/de/itvsh/goofy/JwtTokenUtil.java | 104 ++++++++++++++++ .../java/de/itvsh/goofy/RootController.java | 36 ++++++ .../common/file/OzgFileModelAssembler.java | 6 +- .../goofy/common/user/CurrentUserService.java | 3 +- .../src/main/resources/application.yml | 3 + .../goofy/JwtAuthenticationFilterITCase.java | 36 ++++++ .../goofy/JwtAuthenticationFilterTest.java | 90 ++++++++++++++ .../java/de/itvsh/goofy/JwtTokenUtilTest.java | 112 ++++++++++++++++++ .../de/itvsh/goofy/RootControllerTest.java | 64 ++++++++++ .../de/itvsh/goofy/SecurityTestFactory.java | 16 +++ .../file/OzgFileModelAssemblerTest.java | 2 +- pom.xml | 17 ++- 14 files changed, 551 insertions(+), 6 deletions(-) create mode 100644 goofy-server/src/main/java/de/itvsh/goofy/JwtAuthenticationFilter.java create mode 100644 goofy-server/src/main/java/de/itvsh/goofy/JwtTokenUtil.java create mode 100644 goofy-server/src/test/java/de/itvsh/goofy/JwtAuthenticationFilterITCase.java create mode 100644 goofy-server/src/test/java/de/itvsh/goofy/JwtAuthenticationFilterTest.java create mode 100644 goofy-server/src/test/java/de/itvsh/goofy/JwtTokenUtilTest.java create mode 100644 goofy-server/src/test/java/de/itvsh/goofy/SecurityTestFactory.java diff --git a/goofy-server/pom.xml b/goofy-server/pom.xml index 2ffea0040b..1c0b3d31b8 100644 --- a/goofy-server/pom.xml +++ b/goofy-server/pom.xml @@ -72,6 +72,16 @@ <groupId>org.keycloak</groupId> <artifactId>keycloak-admin-client</artifactId> </dependency> + + <!-- jwt --> + <dependency> + <groupId>com.auth0</groupId> + <artifactId>java-jwt</artifactId> + </dependency> + <dependency> + <groupId>io.jsonwebtoken</groupId> + <artifactId>jjwt</artifactId> + </dependency> <!-- own projects --> <dependency> diff --git a/goofy-server/src/main/java/de/itvsh/goofy/JwtAuthenticationFilter.java b/goofy-server/src/main/java/de/itvsh/goofy/JwtAuthenticationFilter.java new file mode 100644 index 0000000000..965b7f4e9a --- /dev/null +++ b/goofy-server/src/main/java/de/itvsh/goofy/JwtAuthenticationFilter.java @@ -0,0 +1,58 @@ +package de.itvsh.goofy; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import com.auth0.jwt.exceptions.JWTVerificationException; + +import de.itvsh.goofy.common.errorhandling.TechnicalException; +import lombok.extern.log4j.Log4j2; + +@Log4j2 +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + static final String PARAM_DOWNLOAD_TOKEN = "downloadToken"; + + @Autowired + private JwtTokenUtil jwtTokenUtil; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + var token = getDownloadToken(request); + + if (StringUtils.isNotBlank(token)) { + LOG.debug("JwtAuthenticationFilter download token found"); + try { + LOG.debug("JwtAuthenticationFilter verify..."); + jwtTokenUtil.verifyToken(token); + LOG.debug("JwtAuthenticationFilter verification successfull."); + + doFilter(request, response, filterChain); + } catch (JWTVerificationException e) { + LOG.warn("JwtVerficationException", e); + throw new TechnicalException("download token not valid", e.getCause()); + } + } else { + doFilter(request, response, filterChain); + } + } + + private String getDownloadToken(HttpServletRequest request) { + return request.getParameter(PARAM_DOWNLOAD_TOKEN); + } + + void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException { + filterChain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/goofy-server/src/main/java/de/itvsh/goofy/JwtTokenUtil.java b/goofy-server/src/main/java/de/itvsh/goofy/JwtTokenUtil.java new file mode 100644 index 0000000000..dc802861e1 --- /dev/null +++ b/goofy-server/src/main/java/de/itvsh/goofy/JwtTokenUtil.java @@ -0,0 +1,104 @@ +package de.itvsh.goofy; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Component; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.JWTVerificationException; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; + +@Component +public class JwtTokenUtil implements Serializable { + + private static final long serialVersionUID = -2550185165626007488L; + public static final long JWT_TOKEN_VALIDITY_MS = 60000; + public static final String TOKEN_TYPE = "JWT"; + public static final String TOKEN_ISSUER = "secure-api"; + public static final String TOKEN_AUDIENCE = "secure-app"; + public static final String ROLE_CLAIM = "roles"; + public static final String FIRSTNAME_CLAIM = "firstName"; + public static final String LASTNAME_CLAIM = "lastName"; + + @Value("${kop.auth.token-secret}") + private String secret; + + public String getUserIdFromToken(String token) { + return getClaimFromToken(token, Claims::getSubject); + } + + private <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) { + final Claims claims = getAllClaimsFromToken(token); + return claimsResolver.apply(claims); + } + + public String getUserFirstNameFromToken(String token) { + Claims claims = getAllClaimsFromToken(token); + return claims.get(FIRSTNAME_CLAIM).toString(); + } + + public String getUserLastNameFromToken(String token) { + Claims claims = getAllClaimsFromToken(token); + return claims.get(LASTNAME_CLAIM).toString(); + } + + private ArrayList<Map<String, String>> getRoleClaims(String token) { + Claims claims = getAllClaimsFromToken(token); + return (ArrayList<Map<String, String>>) claims.get(ROLE_CLAIM); + } + + private Claims getAllClaimsFromToken(String token) { + return Jwts.parser().setSigningKey(secret.getBytes()).parseClaimsJws(token).getBody(); + } + + public List<SimpleGrantedAuthority> getRolesFromToken(String token) { + ArrayList<Map<String, String>> claimsList = getRoleClaims(token); + return claimsList.stream()// + .flatMap(rolesMap -> rolesMap.entrySet().stream())// + .map(roleEntry -> new SimpleGrantedAuthority(roleEntry.getValue()))// + .collect(Collectors.toList()); + } + + public String generateToken(String userId, String userFirstName, String userLastName, Collection<? extends GrantedAuthority> authorities) { + Map<String, Object> claims = new HashMap<>(); + claims.put(FIRSTNAME_CLAIM, userFirstName); + claims.put(LASTNAME_CLAIM, userLastName); + claims.put(ROLE_CLAIM, authorities); + + return doGenerateToken(claims, userId); + } + + private String doGenerateToken(Map<String, Object> claims, String subject) { + return Jwts.builder()// + .setClaims(claims)// + .setSubject(subject)// + .setHeaderParam("typ", TOKEN_TYPE)// + .setIssuer(TOKEN_ISSUER).setIssuedAt(new Date(System.currentTimeMillis()))// + .setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY_MS))// + .setAudience(TOKEN_AUDIENCE)// + .signWith(SignatureAlgorithm.HS512, secret.getBytes())// + .compact(); + } + + public void verifyToken(String token) throws JWTVerificationException { + var algorithm = Algorithm.HMAC512(secret); + var verifier = JWT.require(algorithm).build(); + + verifier.verify(token); + } +} \ No newline at end of file diff --git a/goofy-server/src/main/java/de/itvsh/goofy/RootController.java b/goofy-server/src/main/java/de/itvsh/goofy/RootController.java index 69ab584dd9..d9bca1d8ec 100644 --- a/goofy-server/src/main/java/de/itvsh/goofy/RootController.java +++ b/goofy-server/src/main/java/de/itvsh/goofy/RootController.java @@ -2,16 +2,22 @@ package de.itvsh.goofy; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*; +import java.net.URI; import java.time.Instant; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.info.BuildProperties; import org.springframework.hateoas.EntityModel; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import de.itvsh.goofy.common.ModelBuilder; +import de.itvsh.goofy.common.file.OzgFileDataController; import de.itvsh.goofy.common.user.CurrentUserService; import de.itvsh.goofy.common.user.UserProfileController; import de.itvsh.goofy.common.user.UserRole; @@ -24,12 +30,17 @@ public class RootController { static final String REL_VORGAENGE = "vorgaenge"; static final String REL_SEARCH = "search"; static final String REL_SEARCH_USER = "search-user-profiles"; + static final String REL_DOWNLOAD_TOKEN = "downloadToken"; + + static final String PARAM_DOWNLOAD_TOKEN = "downloadToken"; @Autowired(required = false) public BuildProperties buildProperties; @Autowired private CurrentUserService userService; + @Autowired + private JwtTokenUtil jwtTokenUtil; @GetMapping public EntityModel<RootResource> getRootResource() { @@ -39,6 +50,7 @@ public class RootController { .addLink(linkTo(methodOn(UserProfileController.class).findUsers(null)).withRel(REL_SEARCH_USER)) .ifMatch(this::isEinheitlicherAnsprechpartner) .addLink(() -> linkTo(methodOn(VorgangController.class).searchVorgangList(0, null)).withRel(REL_SEARCH)) + .addLink(linkTo(RootController.class).withRel(REL_DOWNLOAD_TOKEN)) .buildModel(); } @@ -46,6 +58,30 @@ public class RootController { return userService.hasRole(UserRole.EINHEITLICHER_ANSPRECHPARTNER); } + @PostMapping + public ResponseEntity<Void> downloadToken(@RequestBody String resourceUri) { + return buildResponse(generateToken(getIdFromUri(resourceUri))); + } + + private String generateToken(String resourceId) { + var user = userService.getUser(); + return jwtTokenUtil.generateToken(resourceId, user.getFirstName(), user.getLastName(), user.getAuthorities()); + } + + private ResponseEntity<Void> buildResponse(String token) { + var uriString = linkTo(OzgFileDataController.class).slash("downloadToken").toUri().toString() + "?" + PARAM_DOWNLOAD_TOKEN + "=" + token; + return ResponseEntity.created(URI.create(uriString)).build(); + } + + @GetMapping("/downloadToken") + public ResponseEntity<String> getDownloadToken(@RequestParam String downloadToken) { + return ResponseEntity.ok(downloadToken); + } + + private String getIdFromUri(String uri) { + return uri.substring(uri.lastIndexOf("/") + 1, uri.length()); + } + class RootResource { public String getVersion() { diff --git a/goofy-server/src/main/java/de/itvsh/goofy/common/file/OzgFileModelAssembler.java b/goofy-server/src/main/java/de/itvsh/goofy/common/file/OzgFileModelAssembler.java index bac671d5e7..082d2fcab7 100644 --- a/goofy-server/src/main/java/de/itvsh/goofy/common/file/OzgFileModelAssembler.java +++ b/goofy-server/src/main/java/de/itvsh/goofy/common/file/OzgFileModelAssembler.java @@ -2,11 +2,11 @@ package de.itvsh.goofy.common.file; import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.springframework.hateoas.CollectionModel; import org.springframework.hateoas.EntityModel; -import org.springframework.hateoas.LinkRelation; import org.springframework.hateoas.server.RepresentationModelAssembler; import org.springframework.stereotype.Component; @@ -15,7 +15,7 @@ import de.itvsh.goofy.common.ModelBuilder; @Component public class OzgFileModelAssembler implements RepresentationModelAssembler<OzgFile, EntityModel<OzgFile>> { - static final LinkRelation REL_DOWNLOAD = LinkRelation.of("download"); + static final String REL_DOWNLOAD = "download"; @Override public EntityModel<OzgFile> toModel(OzgFile file) { @@ -27,7 +27,7 @@ public class OzgFileModelAssembler implements RepresentationModelAssembler<OzgFi } public CollectionModel<EntityModel<OzgFile>> toCollectionModel(Stream<OzgFile> entities) { - return CollectionModel.of(entities.map(this::toModel).toList(), + return CollectionModel.of(entities.map(this::toModel).collect(Collectors.toList()), linkTo(OzgFileDataController.class).withSelfRel()); } } \ No newline at end of file diff --git a/goofy-server/src/main/java/de/itvsh/goofy/common/user/CurrentUserService.java b/goofy-server/src/main/java/de/itvsh/goofy/common/user/CurrentUserService.java index f1fe67d416..e8b4818ee6 100644 --- a/goofy-server/src/main/java/de/itvsh/goofy/common/user/CurrentUserService.java +++ b/goofy-server/src/main/java/de/itvsh/goofy/common/user/CurrentUserService.java @@ -6,6 +6,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; import org.keycloak.KeycloakPrincipal; import org.keycloak.representations.AccessToken; @@ -51,7 +52,7 @@ public class CurrentUserService { List<String> getOrganisationseinheitId(Map<String, Object> claims) { return Optional.ofNullable(claims.get(USER_ATTRIBUTE_ORGANISATIONSEINHEIT_ID)) .map(col -> (Collection<?>) col).orElse(Collections.emptyList()) // NOSONAR - Collection.class::cast has type-safty issue - .stream().map(Object::toString).toList(); + .stream().map(Object::toString).collect(Collectors.toList()); } private Optional<AccessToken> getCurrentSecurityToken() { diff --git a/goofy-server/src/main/resources/application.yml b/goofy-server/src/main/resources/application.yml index e9f70e6e52..c28a3505f5 100644 --- a/goofy-server/src/main/resources/application.yml +++ b/goofy-server/src/main/resources/application.yml @@ -59,3 +59,6 @@ keycloak: resource: goofy public-client: true +kop: + auth: + token-secret: XPPWagXn3rDwKG6Ywoir \ No newline at end of file diff --git a/goofy-server/src/test/java/de/itvsh/goofy/JwtAuthenticationFilterITCase.java b/goofy-server/src/test/java/de/itvsh/goofy/JwtAuthenticationFilterITCase.java new file mode 100644 index 0000000000..c09d26680d --- /dev/null +++ b/goofy-server/src/test/java/de/itvsh/goofy/JwtAuthenticationFilterITCase.java @@ -0,0 +1,36 @@ +package de.itvsh.goofy; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; + +import org.junit.jupiter.api.Test; +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.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +@SpringBootTest +@AutoConfigureMockMvc +@WithMockUser +class JwtAuthenticationFilterITCase { + + @SpyBean + private JwtAuthenticationFilter filter; + + @Autowired + private MockMvc mockMvc; + + @Test + void shouldCallFilter() throws Exception { + performRequest("/"); + + verify(filter).doFilterInternal(any(), any(), any()); + } + + void performRequest(String path) throws Exception { + mockMvc.perform(get(path)); + } +} \ No newline at end of file diff --git a/goofy-server/src/test/java/de/itvsh/goofy/JwtAuthenticationFilterTest.java b/goofy-server/src/test/java/de/itvsh/goofy/JwtAuthenticationFilterTest.java new file mode 100644 index 0000000000..083f4c8226 --- /dev/null +++ b/goofy-server/src/test/java/de/itvsh/goofy/JwtAuthenticationFilterTest.java @@ -0,0 +1,90 @@ +package de.itvsh.goofy; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.util.UUID; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; + +import com.auth0.jwt.exceptions.JWTVerificationException; + +import de.itvsh.goofy.common.errorhandling.TechnicalException; + +class JwtAuthenticationFilterTest { + + @Spy + @InjectMocks + private JwtAuthenticationFilter filter; + @Mock + private FilterChain filterChain; + @Mock + private HttpServletRequest request; + @Mock + private HttpServletResponse response; + @Mock + private JwtTokenUtil jwtTokenUtil; + + @Test + void shouldCallDoFilter() throws IOException, ServletException { + doFilterInternal(); + + verify(filterChain).doFilter(request, response); + } + + @Test + void shouldNotCallJwtTokenUtil() throws Exception { + doFilterInternal(); + + verifyNoInteractions(jwtTokenUtil); + } + + @Nested + class TestWithParamter { + + final String TOKEN = UUID.randomUUID().toString(); + + @BeforeEach + void mockRequestParameter() { + when(request.getParameter(anyString())).thenReturn(TOKEN); + } + + @Test + void shouldVerifyTokenIfPresent() throws Exception { + doFilterInternal(); + + verify(jwtTokenUtil).verifyToken(TOKEN); + } + + @Test + void shouldThrowExceptionOnInvalidToken() throws Exception { + doThrow(JWTVerificationException.class).when(jwtTokenUtil).verifyToken(anyString()); + + assertThrows(TechnicalException.class, () -> doFilterInternal()); + } + + @Test + void shouldNotFilterOnInvalidToken() throws Exception { + doThrow(JWTVerificationException.class).when(jwtTokenUtil).verifyToken(anyString()); + + assertThrows(TechnicalException.class, () -> doFilterInternal()); + verify(filter, times(0)).doFilter(any(), any(), any()); + } + } + + private void doFilterInternal() throws ServletException, IOException { + filter.doFilterInternal(request, response, filterChain); + } +} \ No newline at end of file diff --git a/goofy-server/src/test/java/de/itvsh/goofy/JwtTokenUtilTest.java b/goofy-server/src/test/java/de/itvsh/goofy/JwtTokenUtilTest.java new file mode 100644 index 0000000000..16569ed3be --- /dev/null +++ b/goofy-server/src/test/java/de/itvsh/goofy/JwtTokenUtilTest.java @@ -0,0 +1,112 @@ +package de.itvsh.goofy; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.lang.reflect.Field; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Spy; + +import com.auth0.jwt.exceptions.JWTVerificationException; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; + +class JwtTokenUtilTest { + + @Spy + private JwtTokenUtil jwtTokenUtil; + + private static final String TOKEN_SECRET = "t0pS3cr3t"; + + @BeforeEach + public void initTest() throws Exception { + Field tokenSecretField = JwtTokenUtil.class.getDeclaredField("secret"); + tokenSecretField.setAccessible(true); + tokenSecretField.set(jwtTokenUtil, TOKEN_SECRET); + } + + @Nested + @DisplayName("Verify token generation") + class TestGenerateToken { + + private String generatedToken; + + @BeforeEach + void initTest() { + generatedToken = jwtTokenUtil.generateToken(SecurityTestFactory.SUBJECT.toString(), SecurityTestFactory.USER_FIRSTNAME, + SecurityTestFactory.USER_LASTNAME, SecurityTestFactory.AUTHORITIES); + } + + @Test + void userId() { + var userId = getParsedBody().getSubject(); + + assertThat(userId).isEqualTo(SecurityTestFactory.SUBJECT.toString()); + } + + @Test + void expirationDate() { + var before = new Date(); + var expirationDate = getParsedBody().getExpiration(); + var after = new Date(System.currentTimeMillis() + 900000); + + assertThat(expirationDate).isAfter(before).isBefore(after); + } + + private Claims getParsedBody() { + return Jwts.parser().setSigningKey(TOKEN_SECRET.getBytes()).parseClaimsJws(generatedToken).getBody(); + } + } + + @Nested + class TestVerifyToken { + + @Test + void shouldDoNotThrowExcepionOnValidToken() { + var token = buildToken(UUID.randomUUID().toString(), TOKEN_SECRET, 60000); + + jwtTokenUtil.verifyToken(token); + } + + @Test + void shouldThrowExceptionOnInvalidToken() { + var token = buildToken(UUID.randomUUID().toString(), "invalid_token", 60000); + + assertThrows(JWTVerificationException.class, () -> jwtTokenUtil.verifyToken(token)); + } + + @Test + void shouldThrowExceptionOnTimeExpired() { + var token = buildToken(UUID.randomUUID().toString(), TOKEN_SECRET, -1000); + + assertThrows(JWTVerificationException.class, () -> jwtTokenUtil.verifyToken(token)); + } + + private String buildToken(String subject, String token, int expiredTime) { + Map<String, Object> claims = new HashMap<>(); + claims.put(JwtTokenUtil.FIRSTNAME_CLAIM, SecurityTestFactory.USER_FIRSTNAME); + claims.put(JwtTokenUtil.LASTNAME_CLAIM, SecurityTestFactory.USER_LASTNAME); + claims.put(JwtTokenUtil.ROLE_CLAIM, SecurityTestFactory.AUTHORITIES); + + return Jwts.builder()// + .setClaims(claims)// + .setSubject(subject)// + .setHeaderParam("typ", JwtTokenUtil.TOKEN_TYPE)// + .setIssuer(JwtTokenUtil.TOKEN_ISSUER).setIssuedAt(new Date(System.currentTimeMillis()))// + .setExpiration(new Date(System.currentTimeMillis() + expiredTime))// + .setAudience(JwtTokenUtil.TOKEN_AUDIENCE)// + .signWith(SignatureAlgorithm.HS512, token.getBytes())// + .compact(); + } + } +} \ No newline at end of file diff --git a/goofy-server/src/test/java/de/itvsh/goofy/RootControllerTest.java b/goofy-server/src/test/java/de/itvsh/goofy/RootControllerTest.java index e6859beb8a..177da70b39 100644 --- a/goofy-server/src/test/java/de/itvsh/goofy/RootControllerTest.java +++ b/goofy-server/src/test/java/de/itvsh/goofy/RootControllerTest.java @@ -4,10 +4,12 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import java.time.LocalDateTime; import java.time.ZoneOffset; +import java.util.UUID; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -23,6 +25,7 @@ import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import de.itvsh.goofy.common.user.CurrentUserService; +import de.itvsh.goofy.common.user.UserTestFactory; class RootControllerTest { @@ -34,6 +37,8 @@ class RootControllerTest { private BuildProperties properties; @Mock private CurrentUserService userService; + @Mock + private JwtTokenUtil jwtTokenUtil; private MockMvc mockMvc; @@ -90,6 +95,14 @@ class RootControllerTest { .isEqualTo("/api/userProfiles?searchBy={searchBy}"); } + @Test + void shoulHaveDownloadTokenLink() { + var model = controller.getRootResource(); + + assertThat(model.getLink(RootController.REL_DOWNLOAD_TOKEN)).isPresent().get().extracting(Link::getHref) + .isEqualTo("/api"); + } + @Test void shouldHaveJavaVersion() throws Exception { callEndpoint().andExpect(jsonPath("$.javaVersion").exists()); @@ -112,4 +125,55 @@ class RootControllerTest { private ResultActions callEndpoint() throws Exception { return mockMvc.perform(get(PATH)).andExpect(status().isOk()); } + + @Nested + class TestDownloadToken { + + @BeforeEach + void mock() { + when(userService.getUser()).thenReturn(UserTestFactory.create()); + when(jwtTokenUtil.generateToken(any(), any(), any(), any())).thenReturn(UUID.randomUUID().toString()); + } + + @Test + void shouldCallCurrentUserService() throws Exception { + callEndpoint(); + + verify(userService).getUser(); + } + + @Test + void shouldGenerateToken() throws Exception { + callEndpoint(); + + verify(jwtTokenUtil).generateToken(any(), any(), any(), any()); + } + + @Test + void shouldHaveReponse() throws Exception { + callEndpoint().andDo(print()); + + } + + private ResultActions callEndpoint() throws Exception { + return mockMvc.perform(post(PATH).content("/api/service/resourceuri")) + .andExpect(status().isCreated()); + } + } + + @Nested + class TestGetDownloadToken { + + static final String DOWNLOAD_TOKEN = "TestDownloadToken"; + + @Test + void shouldReturnDownloadToken() throws Exception { + callEndpoint().andExpect(content().string(DOWNLOAD_TOKEN)); + } + + private ResultActions callEndpoint() throws Exception { + return mockMvc.perform(get(PATH + "/downloadToken").param(RootController.PARAM_DOWNLOAD_TOKEN, DOWNLOAD_TOKEN)) + .andExpect(status().isOk()); + } + } } \ No newline at end of file diff --git a/goofy-server/src/test/java/de/itvsh/goofy/SecurityTestFactory.java b/goofy-server/src/test/java/de/itvsh/goofy/SecurityTestFactory.java new file mode 100644 index 0000000000..f427007901 --- /dev/null +++ b/goofy-server/src/test/java/de/itvsh/goofy/SecurityTestFactory.java @@ -0,0 +1,16 @@ +package de.itvsh.goofy; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +public class SecurityTestFactory { + + static final String SUBJECT = UUID.randomUUID().toString(); + static final String USER_FIRSTNAME = "Tim"; + static final String USER_LASTNAME = "Tester"; + static final String ROLE = "Testrolle"; + static final List<SimpleGrantedAuthority> AUTHORITIES = Arrays.asList(new SimpleGrantedAuthority(ROLE)); +} diff --git a/goofy-server/src/test/java/de/itvsh/goofy/common/file/OzgFileModelAssemblerTest.java b/goofy-server/src/test/java/de/itvsh/goofy/common/file/OzgFileModelAssemblerTest.java index 20153aa5fe..b6f88a56f7 100644 --- a/goofy-server/src/test/java/de/itvsh/goofy/common/file/OzgFileModelAssemblerTest.java +++ b/goofy-server/src/test/java/de/itvsh/goofy/common/file/OzgFileModelAssemblerTest.java @@ -22,7 +22,7 @@ class OzgFileModelAssemblerTest { @Test void shouldHaveDownloadLink() throws Exception { - var link = getLinkFromModel(OzgFileTestFactory.create(), OzgFileModelAssembler.REL_DOWNLOAD); + var link = getLinkFromModel(OzgFileTestFactory.create(), LinkRelation.of(OzgFileModelAssembler.REL_DOWNLOAD)); assertThat(link).isPresent(); assertThat(link.get().getHref()).isEqualTo(PATH + OzgFileTestFactory.ID); diff --git a/pom.xml b/pom.xml index f0b0243df2..8251a17f53 100644 --- a/pom.xml +++ b/pom.xml @@ -1,8 +1,8 @@ <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> - <modelVersion>4.0.0</modelVersion> + <groupId>de.itvsh.ozg</groupId> <artifactId>goofy</artifactId> <version>0.16.0-SNAPSHOT</version> @@ -33,6 +33,9 @@ <lombok.version>edge-SNAPSHOT</lombok.version> <lorem.version>2.1</lorem.version> + + <java-jwt.version>3.18.2</java-jwt.version> + <jjwt.version>0.9.1</jjwt.version> <!-- plugins --> <maven-jar-plugin.version>3.2.0</maven-jar-plugin.version> @@ -117,6 +120,18 @@ <artifactId>lombok</artifactId> <version>${lombok.version}</version> </dependency> + + <!-- jwt --> + <dependency> + <groupId>com.auth0</groupId> + <artifactId>java-jwt</artifactId> + <version>${java-jwt.version}</version> + </dependency> + <dependency> + <groupId>io.jsonwebtoken</groupId> + <artifactId>jjwt</artifactId> + <version>${jjwt.version}</version> + </dependency> </dependencies> </dependencyManagement> -- GitLab