From 452aa082561893f1085973f757b27d65ad6a5a2b Mon Sep 17 00:00:00 2001
From: Jan Zickermann <jan.zickermann@dataport.de>
Date: Thu, 21 Nov 2024 17:24:36 +0100
Subject: [PATCH] #3 OZG-7112 auth2: Configure resource parameter

---
 pom.xml                                       |  7 --
 .../osiv2/config/WebClientConfiguration.java  | 41 ++++++--
 src/main/resources/application.yml            | 13 ++-
 .../osiv2/OsiPostfachRemoteServiceITCase.java | 71 +++++++++++---
 .../postfach/osiv2/extension/Jwt.java         | 89 ++++++++++++++++++
 .../postfach/osiv2/extension/JwtParser.java   | 47 ----------
 .../{JwtParserTest.java => JwtTest.java}      | 23 ++++-
 .../extension/OsiMockServerExtension.java     | 94 ++++++-------------
 .../postfach/osiv2/factory/JsonUtil.java      | 14 +++
 .../postfach/osiv2/factory/JwtFactory.java    | 51 ++++++++++
 10 files changed, 305 insertions(+), 145 deletions(-)
 create mode 100644 src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/Jwt.java
 delete mode 100644 src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/JwtParser.java
 rename src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/{JwtParserTest.java => JwtTest.java} (66%)
 create mode 100644 src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/JsonUtil.java
 create mode 100644 src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/JwtFactory.java

diff --git a/pom.xml b/pom.xml
index b887470..17cf614 100644
--- a/pom.xml
+++ b/pom.xml
@@ -19,7 +19,6 @@
 	<properties>
 		<api-lib.version>0.13.0</api-lib.version>
 		<nachrichten-manager.version>2.14.0</nachrichten-manager.version>
-		<testcontainers-keycloak.version>3.2.0</testcontainers-keycloak.version>
 		<mockserver-client.version>5.15.0</mockserver-client.version>
 	</properties>
 	<dependencies>
@@ -61,12 +60,6 @@
 			<type>test-jar</type>
 			<scope>test</scope>
 		</dependency>
-		<dependency>
-			<groupId>com.github.dasniko</groupId>
-			<artifactId>testcontainers-keycloak</artifactId>
-			<version>${testcontainers-keycloak.version}</version>
-			<scope>test</scope>
-		</dependency>
 		<dependency>
 			<groupId>org.testcontainers</groupId>
 			<artifactId>testcontainers</artifactId>
diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/config/WebClientConfiguration.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/config/WebClientConfiguration.java
index 454f52e..f851b78 100644
--- a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/config/WebClientConfiguration.java
+++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/config/WebClientConfiguration.java
@@ -8,18 +8,26 @@ import org.springframework.context.annotation.Primary;
 import org.springframework.core.env.Environment;
 import org.springframework.security.oauth2.client.AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager;
 import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService;
+import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProvider;
 import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProviderBuilder;
+import org.springframework.security.oauth2.client.endpoint.WebClientReactiveClientCredentialsTokenResponseClient;
 import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
 import org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
 import org.springframework.web.reactive.function.client.WebClient;
 
+import lombok.RequiredArgsConstructor;
+
 @Configuration
+@RequiredArgsConstructor
 public class WebClientConfiguration {
 
+	private final Environment environment;
+
 	@Bean("osi2PostfachWebClient")
 	public WebClient osi2PostfachWebClient(
-			ServerOAuth2AuthorizedClientExchangeFilterFunction serverOAuth2AuthorizedClientExchangeFilterFunction,
-			Environment environment) {
+			ServerOAuth2AuthorizedClientExchangeFilterFunction serverOAuth2AuthorizedClientExchangeFilterFunction) {
 		var url = Objects.requireNonNull(
 				environment.getProperty("ozgcloud.osiv2-postfach.api.url"),
 				"ozgcloud.osiv2-postfach.api.url is not set");
@@ -46,12 +54,33 @@ public class WebClientConfiguration {
 		var authorizedClientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
 				clientRegistrations, clientService);
 
-		authorizedClientManager.setAuthorizedClientProvider(
-				ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
-						.clientCredentials()
-						.build());
+		authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider());
 
 		return authorizedClientManager;
 	}
 
+	ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider() {
+		return ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
+				.clientCredentials(builder ->
+						builder.accessTokenResponseClient(getClientCredentialsTokenResponseClient())
+				)
+				.build();
+	}
+
+	WebClientReactiveClientCredentialsTokenResponseClient getClientCredentialsTokenResponseClient() {
+		var client = new WebClientReactiveClientCredentialsTokenResponseClient();
+		var resource = Objects.requireNonNull(
+				environment.getProperty("ozgcloud.osiv2-postfach.api.resource"),
+				"ozgcloud.osiv2-postfach.api.resource is not set"
+		);
+
+		client.addParametersConverter(source -> {
+			MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
+			// Pass a resource indicator parameter https://datatracker.ietf.org/doc/html/rfc8707
+			parameters.add("resource", resource);
+			return parameters;
+		});
+		return client;
+	}
+
 }
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index a93d8da..879647a 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -12,10 +12,19 @@ spring:
             client-secret: 'changeme'
             scope: default, access_urn:some:scope:for:ozgkopfstelle
             authorization-grant-type: 'client_credentials'
+            client-authentication-method: client_secret_post
         provider:
           osi2:
-            token-uri: http://localhost:8080/realms/master/protocol/openid-connect/token
+            token-uri: 'https://idp.serviceportal-stage.schleswig-holstein.de/webidp2/connect/token'
 ozgcloud:
   osiv2-postfach:
+    enabled: true
     api:
-      url: 'replaceme'
+      resource: 'urn:dataport:osi:postfach:rz2:stage:sh'
+      url: 'https://api-gateway-stage.dataport.de:443/api/osi_postfach/1.0.0'
+      tenant: 'SH'
+      name-identifier: 'ozgkopfstelle'
+    http-proxy:
+      host: 10.65.108.2
+      port: 3128
+      authentication-required: false
diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceITCase.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceITCase.java
index 4633ec3..d622702 100644
--- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceITCase.java
+++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceITCase.java
@@ -1,8 +1,13 @@
 package de.ozgcloud.nachrichten.postfach.osiv2;
 
+import static de.ozgcloud.nachrichten.postfach.osiv2.factory.JwtFactory.*;
 import static org.assertj.core.api.Assertions.*;
+import static org.mockserver.matchers.Times.*;
+import static org.mockserver.model.Header.*;
 import static org.mockserver.model.HttpRequest.*;
 import static org.mockserver.model.HttpResponse.*;
+import static org.mockserver.model.Parameter.*;
+import static org.mockserver.model.ParameterBody.*;
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
@@ -10,15 +15,16 @@ import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.RegisterExtension;
 import org.mockserver.client.MockServerClient;
-import org.mockserver.matchers.Times;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.test.context.DynamicPropertyRegistry;
 import org.springframework.test.context.DynamicPropertySource;
 
 import de.ozgcloud.nachrichten.postfach.PostfachNachricht;
-import de.ozgcloud.nachrichten.postfach.osiv2.extension.JwtParser;
+import de.ozgcloud.nachrichten.postfach.osiv2.extension.Jwt;
 import de.ozgcloud.nachrichten.postfach.osiv2.extension.OsiMockServerExtension;
+import de.ozgcloud.nachrichten.postfach.osiv2.factory.JwtFactory;
+import lombok.SneakyThrows;
 
 @SpringBootTest(classes = TestApplication.class, webEnvironment = SpringBootTest.WebEnvironment.NONE)
 public class OsiPostfachRemoteServiceITCase {
@@ -36,46 +42,83 @@ public class OsiPostfachRemoteServiceITCase {
 
 	@DynamicPropertySource
 	static void dynamicProperties(DynamicPropertyRegistry registry) {
-		registry.add("spring.security.oauth2.client.provider.osi2.token-uri", OSI_MOCK_SERVER_EXTENSION::getTokenUri);
-		registry.add("ozgcloud.osiv2-postfach.api.url", OSI_MOCK_SERVER_EXTENSION::getPostfachMockServerUrl);
+		registry.add("spring.security.oauth2.client.provider.osi2.token-uri", OSI_MOCK_SERVER_EXTENSION::getAccessTokenUrl);
+		registry.add("spring.security.oauth2.client.registration.osi2.scope", () -> CLIENT_SCOPES);
+		registry.add("spring.security.oauth2.client.registration.osi2.client-id", () -> CLIENT_ID);
+		registry.add("ozgcloud.osiv2-postfach.api.url", OSI_MOCK_SERVER_EXTENSION::getPostfachFacadeUrl);
+		registry.add("ozgcloud.osiv2-postfach.api.resource", () -> RESOURCE_URN);
 	}
 
-	private MockServerClient mockServerClient;
+	private MockServerClient mockClient;
 
 	@BeforeEach
+	@SneakyThrows
 	public void setup() {
-		mockServerClient = OSI_MOCK_SERVER_EXTENSION.getMockServerClient();
-		mockServerClient
+		mockClient = OSI_MOCK_SERVER_EXTENSION.getMockClient();
+
+		mockClient
 				.when(
 						request()
-								.withMethod("GET")
-								.withPath("/dummy"),
-						Times.exactly(1)
+								.withMethod("POST")
+								.withPath("/access-token")
+								.withHeaders(
+										header("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8")
+								)
+								.withBody(
+										params(
+												param("grant_type", "client_credentials"),
+												param("client_id", CLIENT_ID),
+												param("client_secret", "changeme"),
+												param("scope", String.join(" ", CLIENT_SCOPES)),
+												param("resource", RESOURCE_URN)
+										)
+								),
+						exactly(1)
 				)
 				.respond(
 						response()
 								.withStatusCode(200)
+								.withHeader("Content-Type", "application/json")
+								.withBody(
+										JwtFactory.createTokenResponse(
+												JwtFactory.createAccessTokenExampleWithExpireIn(900)
+										)
+								)
 				);
 	}
 
 	@DisplayName("send message")
 	@Nested
 	class TestSendMessage {
+
 		@DisplayName("should send dummy request with jwt")
 		@Test
 		void shouldSendDummyRequestWithJwt() {
+			mockClient
+					.when(
+							request()
+									.withMethod("GET")
+									.withPath("/dummy"),
+							exactly(1)
+					)
+					.respond(
+							response()
+									.withStatusCode(200)
+					);
+
 			osiPostfachRemoteService.sendMessage(postfachNachricht);
 
-			var requests = mockServerClient.retrieveRecordedRequests(
+			var requests = mockClient.retrieveRecordedRequests(
 					request()
 							.withMethod("GET")
 							.withPath("/dummy")
 			);
 			assertThat(requests).hasSize(1);
-			String clientId = JwtParser.parseBody(
+			var jwt = Jwt.parseAuthHeaderValue(
 					requests[0].getHeader("Authorization").getFirst()
-			).read("$.client_id");
-			assertThat(clientId).isEqualTo("OZG-Kopfstelle");
+			);
+			assertThat(jwt.body().read("$.client_id", String.class)).isEqualTo(CLIENT_ID);
+			assertThat(jwt.body().read("$.aud", String.class)).isEqualTo(RESOURCE_URN);
 		}
 
 	}
diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/Jwt.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/Jwt.java
new file mode 100644
index 0000000..7d029e6
--- /dev/null
+++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/Jwt.java
@@ -0,0 +1,89 @@
+package de.ozgcloud.nachrichten.postfach.osiv2.extension;
+
+import static de.ozgcloud.nachrichten.postfach.osiv2.factory.JsonUtil.*;
+
+import java.io.Serializable;
+import java.util.Map;
+
+import org.eclipse.jgit.util.Base64;
+
+import com.jayway.jsonpath.JsonPath;
+import com.jayway.jsonpath.ReadContext;
+
+import lombok.SneakyThrows;
+
+public record Jwt(String token) {
+
+	public record JwtParts(ReadContext header, ReadContext body, byte[] signature) {
+	}
+
+	@SneakyThrows
+	public static JwtParts parseAuthHeaderValue(String authorizationHeaderValue) {
+		return new Jwt(discardBearerPrefix(authorizationHeaderValue)).parse();
+
+	}
+
+	public String scope() {
+		return parse().body().read("$.scope");
+	}
+
+	public Integer issuedAt() {
+		return parse().body().read("$.iat", Integer.class);
+	}
+
+	public Integer expireTime() {
+		return parse().body().read("$.exp", Integer.class);
+	}
+
+	public JwtParts parse() {
+		var jwtParts = splitIntoHeaderAndBodyAndSignature(this.token);
+		return new JwtParts(
+				parseJsonPath(base64UrlDecode(jwtParts[0])),
+				parseJsonPath(base64UrlDecode(jwtParts[1])),
+				base64UrlDecode(jwtParts[2])
+		);
+
+	}
+
+	private static ReadContext parseJsonPath(byte[] bytes) {
+		return JsonPath.parse(new String(bytes));
+	}
+
+	private static String discardBearerPrefix(String authorizationHeaderValue) {
+		return authorizationHeaderValue.substring("Bearer ".length());
+	}
+
+	private static String[] splitIntoHeaderAndBodyAndSignature(String jwt) {
+		var jwtParts = jwt.split("\\.");
+		if (jwtParts.length != 3) {
+			throw new IllegalArgumentException("Invalid JWT token");
+		}
+		return jwtParts;
+	}
+
+	private static byte[] base64UrlDecode(String input) {
+		// Replace URL-safe characters
+		String base64 = input.replace('-', '+').replace('_', '/');
+		// Add padding if necessary
+		int padding = 4 - (base64.length() % 4);
+		if (padding < 4) {
+			base64 += "=".repeat(padding);
+		}
+		return Base64.decode(base64);
+	}
+
+	@SneakyThrows
+	public static Jwt create(Map<String, Serializable> header, Map<String, Serializable> body, byte[] signature) {
+		var headerJson = toJson(header);
+		var bodyJson = toJson(body);
+		var token = base64UrlEncode(headerJson) + "." + base64UrlEncode(bodyJson) + "." + base64UrlEncode(new String(signature));
+		return new Jwt(token);
+	}
+
+	private static String base64UrlEncode(String input) {
+		String base64 = Base64.encodeBytes(input.getBytes());
+		return base64.replace('+', '-').replace('/', '_').replace("=", "");
+
+	}
+
+}
diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/JwtParser.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/JwtParser.java
deleted file mode 100644
index 85c18ed..0000000
--- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/JwtParser.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package de.ozgcloud.nachrichten.postfach.osiv2.extension;
-
-import org.eclipse.jgit.util.Base64;
-
-import com.jayway.jsonpath.JsonPath;
-import com.jayway.jsonpath.ReadContext;
-
-import lombok.SneakyThrows;
-
-public class JwtParser {
-
-	@SneakyThrows
-	public static ReadContext parseBody(String authorizationHeaderValue) {
-		var jwtParts = splitIntoSignatureAndHeaderAndBody(
-				discardBearerPrefix(authorizationHeaderValue)
-		);
-		var bodyPart = jwtParts[1];
-		return parseJsonPartFromUrlEncodedBase64(bodyPart);
-	}
-
-	private static ReadContext parseJsonPartFromUrlEncodedBase64(String base64EncodedPayload) {
-		return JsonPath.parse(new String(base64UrlDecode(base64EncodedPayload)));
-	}
-
-	private static String discardBearerPrefix(String authorizationHeaderValue) {
-		return authorizationHeaderValue.substring("Bearer ".length());
-	}
-
-	private static String[] splitIntoSignatureAndHeaderAndBody(String jwt) {
-		var jwtParts =  jwt.split("\\.");
-		if (jwtParts.length != 3) {
-			throw new IllegalArgumentException("Invalid JWT token");
-		}
-		return jwtParts;
-	}
-
-	private static byte[] base64UrlDecode(String input) {
-		// Replace URL-safe characters
-		String base64 = input.replace('-', '+').replace('_', '/');
-		// Add padding if necessary
-		int padding = 4 - (base64.length() % 4);
-		if (padding < 4) {
-			base64 += "=".repeat(padding);
-		}
-		return Base64.decode(base64);
-	}
-}
diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/JwtParserTest.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/JwtTest.java
similarity index 66%
rename from src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/JwtParserTest.java
rename to src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/JwtTest.java
index 9e5ab35..0552893 100644
--- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/JwtParserTest.java
+++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/JwtTest.java
@@ -2,18 +2,35 @@ package de.ozgcloud.nachrichten.postfach.osiv2.extension;
 
 import static org.assertj.core.api.Assertions.*;
 
+import java.util.Map;
+
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 
-class JwtParserTest {
+class JwtTest {
 
 	@DisplayName("should parse")
 	@Test
 	void shouldParse() {
 		var headerValue = "Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ1aV9EaHVXUzdocFhzV3dZTHRlOHFIRkR4bnNFYldlVmJBZ0pzaWpsWGw4In0.eyJleHAiOjE3MzEwNzA5NTEsImlhdCI6MTczMTA3MDg5MSwianRpIjoiZTFjNWE4YjEtZWEyYS00Mzg5LTkyNDQtZWE5Mjc4M2IyZDA1IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDozMjkyNy9yZWFsbXMvbWFzdGVyIiwic3ViIjoiNTg1MzdjMGQtMzU3MS00MDExLWIxM2ItZDY1MGZjOGUwZjQ0IiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiT1pHLUtvcGZzdGVsbGUiLCJzY29wZSI6ImFjY2Vzc191cm46c29tZTpzY29wZTpmb3I6b3pna29wZnN0ZWxsZSBkZWZhdWx0IiwiY2xpZW50SG9zdCI6IjE3Mi4xNy4wLjEiLCJjbGllbnRBZGRyZXNzIjoiMTcyLjE3LjAuMSIsImNsaWVudF9pZCI6Ik9aRy1Lb3Bmc3RlbGxlIn0.MRGusCVssO-fHRp8-tEcdQWE7QVi3P0iHdmO4rGUwj_17KtHzQAT8ShZEVvE8oL-y-XKAPh7eT9will3oON1qhW6GHbZk5Xds4P5u8D0iHNl8nCSi_YS122v9Q1gwPrwPtVH26AKrdNM_YYv0AzT63gOVUoK4YY4jLhow3Uid2AVr2OMNAtcSPMysHXS1VeQRrhOm33JF_WVlguIHNjRpvRqCULkwywBRXDJm2mHOohkXFf10nM3ORAlmeElJCZa7Lg0zeg3q957Z9Mv5KbZA1X_QiHR5qpaDvimn0R_TTCZTGWM00GfyEHi2UU1s2ZfBeZTLOTNg2MUuDgA1cI7CQ";
-		var context = JwtParser.parseBody(headerValue);
+		var context = Jwt.parseAuthHeaderValue(headerValue);
 
-		String value = context.read("$.client_id");
+		String value = context.body().read("$.client_id");
 		assertThat(value).isEqualTo("OZG-Kopfstelle");
 	}
+
+	@DisplayName("should create")
+	@Test
+	void shouldCreate() {
+		var jwt = Jwt.create(Map.of(
+				"alg", "RS256"
+		), Map.of(
+				"sub", "58537c0d-3571-4011-b13b-d650fc8e0f44"
+		), "signature".getBytes());
+
+		var jwtParts = jwt.parse();
+		assertThat(jwtParts.header().read("$.alg", String.class)).isEqualTo("RS256");
+		assertThat(jwtParts.body().read("$.sub", String.class)).isEqualTo("58537c0d-3571-4011-b13b-d650fc8e0f44");
+		assertThat(jwtParts.signature()).isEqualTo("signature".getBytes());
+	}
 }
diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/OsiMockServerExtension.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/OsiMockServerExtension.java
index ad7c560..08835b5 100644
--- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/OsiMockServerExtension.java
+++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/OsiMockServerExtension.java
@@ -1,107 +1,69 @@
 package de.ozgcloud.nachrichten.postfach.osiv2.extension;
 
-import static org.assertj.core.api.Assertions.*;
-
-import java.util.List;
-import java.util.function.Supplier;
-
-import jakarta.ws.rs.core.Response;
-
 import org.junit.jupiter.api.extension.AfterAllCallback;
 import org.junit.jupiter.api.extension.AfterEachCallback;
 import org.junit.jupiter.api.extension.BeforeAllCallback;
 import org.junit.jupiter.api.extension.ExtensionContext;
-import org.keycloak.admin.client.resource.RealmResource;
-import org.keycloak.representations.idm.ClientRepresentation;
-import org.keycloak.representations.idm.ClientScopeRepresentation;
 import org.mockserver.client.MockServerClient;
 import org.testcontainers.containers.MockServerContainer;
+import org.testcontainers.containers.output.OutputFrame;
 import org.testcontainers.utility.DockerImageName;
 
-import dasniko.testcontainers.keycloak.KeycloakContainer;
 import lombok.Getter;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
 
+@Log4j2
 @Getter
 @RequiredArgsConstructor
 public class OsiMockServerExtension implements BeforeAllCallback, AfterAllCallback, AfterEachCallback {
 
-	private MockServerClient mockServerClient;
-	private MockServerContainer mockServerContainer;
-	private KeycloakContainer keycloakContainer;
+	private MockServerClient mockClient;
+	private final MockServerContainer mockServerContainer = new MockServerContainer(DockerImageName.parse("mockserver/mockserver")
+			.withTag("mockserver-5.15.0"))
+			.withLogConsumer(outputFrame -> {
+				var line = outputFrame.getUtf8String().stripTrailing();
+				if (outputFrame.getType() == OutputFrame.OutputType.STDERR) {
+					LOG.error(line);
+				} else {
+					LOG.info(line);
+				}
+			});
 
 	@Override
 	public void beforeAll(ExtensionContext context) {
-		setupPostfachMockServer();
-		setupKeycloak();
+		setupMockServer();
 	}
 
 	@Override
 	public void afterEach(ExtensionContext context) {
-		mockServerClient.reset();
+		mockClient.reset();
 	}
 
 	@Override
 	public void afterAll(ExtensionContext context) {
 		mockServerContainer.stop();
-		mockServerClient.stop();
-		keycloakContainer.stop();
+		mockClient.stop();
 	}
 
-	private void setupPostfachMockServer() {
-		mockServerContainer = new MockServerContainer(DockerImageName.parse("mockserver/mockserver")
-				.withTag("mockserver-5.15.0"));
+	private void setupMockServer() {
 		mockServerContainer.start();
-		mockServerClient = new MockServerClient(mockServerContainer.getHost(), mockServerContainer.getServerPort());
-	}
-
-	private void setupKeycloak() {
-		keycloakContainer = new KeycloakContainer("quay.io/keycloak/keycloak:24.0.5");
-		keycloakContainer.start();
-		try (var keycloak = keycloakContainer.getKeycloakAdminClient()) {
-			keycloak.tokenManager().getAccessToken();
-			var masterRealm = keycloak.realm("master");
-			var clientScopes = List.of("default", "access_urn:some:scope:for:ozgkopfstelle");
-			clientScopes.forEach(scope -> createClientScope(masterRealm, scope));
-			createPostfachClient(masterRealm, clientScopes);
-		}
-	}
-
-	private void createPostfachClient(RealmResource realmResource, List<String> clientScopes) {
-		var clients = realmResource.clients();
-		var postfach = new ClientRepresentation();
-		postfach.setClientId("OZG-Kopfstelle");
-		postfach.setSecret("changeme");
-		postfach.setOptionalClientScopes(clientScopes);
-		postfach.setServiceAccountsEnabled(true);
-		postfach.setEnabled(true);
-		verifyResponseOk(() -> clients.create(postfach));
-	}
-
-	private void createClientScope(RealmResource realmResource, String scopeName) {
-		var clientScopes = realmResource.clientScopes();
-		var clientScopeRepresentation = new ClientScopeRepresentation();
-		clientScopeRepresentation.setName(scopeName);
-		clientScopeRepresentation.setProtocol("openid-connect");
-		verifyResponseOk(() -> clientScopes.create(clientScopeRepresentation));
-	}
-
-	private void verifyResponseOk(Supplier<Response> responseSupplier) {
-		try (var response = responseSupplier.get()) {
-			assertThat(response.getStatus()).isEqualTo(201);
-		}
+		mockClient = new MockServerClient(
+				mockServerContainer.getHost(),
+				mockServerContainer.getServerPort()
+		);
 	}
 
-	public String getTokenUri() {
-		return getAuthProtocolUrl() + "/token";
+	public String getAccessTokenUrl() {
+		return getMockServerUrl() + "/access-token";
 	}
 
-	public String getAuthProtocolUrl() {
-		return keycloakContainer.getAuthServerUrl() + "/realms/master/protocol/openid-connect";
+	public String getPostfachFacadeUrl() {
+		return getMockServerUrl();
 	}
 
-	public String getPostfachMockServerUrl() {
-		return "http://" + mockServerClient.remoteAddress().getHostName() + ":" + mockServerClient.remoteAddress().getPort();
+	private String getMockServerUrl() {
+		return "http://" + mockClient.remoteAddress().getHostName() + ":" + mockClient.remoteAddress().getPort();
 	}
 
 }
diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/JsonUtil.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/JsonUtil.java
new file mode 100644
index 0000000..5353606
--- /dev/null
+++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/JsonUtil.java
@@ -0,0 +1,14 @@
+package de.ozgcloud.nachrichten.postfach.osiv2.factory;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import lombok.SneakyThrows;
+
+public class JsonUtil {
+	private static final ObjectMapper jsonMapper = new ObjectMapper();
+
+	@SneakyThrows
+	public static String toJson(Object object) {
+		return jsonMapper.writeValueAsString(object);
+	}
+}
diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/JwtFactory.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/JwtFactory.java
new file mode 100644
index 0000000..0804976
--- /dev/null
+++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/JwtFactory.java
@@ -0,0 +1,51 @@
+package de.ozgcloud.nachrichten.postfach.osiv2.factory;
+
+import static groovy.json.JsonOutput.*;
+
+import java.util.List;
+import java.util.Map;
+
+
+import de.ozgcloud.nachrichten.postfach.osiv2.extension.Jwt;
+import lombok.SneakyThrows;
+
+public class JwtFactory {
+
+	public static final String CLIENT_ID = "ITCase OZG Kopfstelle";
+	public static final String RESOURCE_URN = "urn:dataport:osi:postfach:rz2:itcase:sh";
+	public static final List<String> CLIENT_SCOPES = List.of("default", "access_urn:itcase:scope:ozgkopfstelle");
+
+	public static Jwt createAccessTokenExampleWithExpireIn(Integer expireInSeconds) {
+		var nowEpochSeconds = System.currentTimeMillis() / 1000;
+		return Jwt.create(
+				Map.of(
+						"alg", "RS256",
+						"kid", "...",
+						"x5t", "...",
+						"typ", "at+jwt"
+				),
+				Map.of(
+						"iss", "https://idp.serviceportal-stage.schleswig-holstein.de/webidp2",
+						"nbf", nowEpochSeconds,
+						"iat", nowEpochSeconds,
+						"exp", nowEpochSeconds + expireInSeconds,
+						"aud", RESOURCE_URN,
+						"scope", String.join(" ", CLIENT_SCOPES),
+						"client_id", CLIENT_ID,
+						"client_tenant", "urn:osp:names:realm:stage:sh",
+						"jti", "..."
+				),
+				"signature".getBytes());
+	}
+
+	@SneakyThrows
+	public static String createTokenResponse(Jwt accessToken) {
+		return toJson(Map.of(
+				"access_token", accessToken.token(),
+				"token_type", "Bearer",
+				"expires_in", accessToken.expireTime() - accessToken.issuedAt(),
+				"scope", accessToken.scope()
+		));
+	}
+
+}
-- 
GitLab