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