diff --git a/README.adoc b/README.adoc
index 9a3046d2..48fc3c0e 100644
--- a/README.adoc
+++ b/README.adoc
@@ -121,6 +121,36 @@ APIM supports two types of authorization server:
^.^|array of string
|===
+=== Specific configuration for Confirmation Method validation
+
+|===
+|Property |Required |Description |Type| Default
+
+.^|confirmationMethodValidation.ignoreMissing
+^.^|-
+|Will ignore CNF validation if the token doesn't contain any CNF information.
+^.^|boolean
+^.^|false
+
+.^|confirmationMethodValidation.certificateBoundThumbprint.enabled
+^.^|-
+|Will validate the certificate thumbprint extracted from the access_token with the one provided by the client.
+^.^|boolean
+^.^|false
+
+.^|confirmationMethodValidation.certificateBoundThumbprint.extractCertificateFromHeader
+^.^|-
+|Enabled to extract the client certificate from request header. Necessary when the M-TLS connection is handled by a proxy.
+^.^|boolean
+^.^|false
+
+.^|confirmationMethodValidation.certificateBoundThumbprint.headerName
+^.^|-
+|Name of the header where to find the client certificate.
+^.^|string
+^.^|ssl-client-cert
+|===
+
=== Configuration example
[source, json]
@@ -131,7 +161,15 @@ APIM supports two types of authorization server:
"oauthCacheResource": "cache-resource-name",
"extractPayload": true,
"checkRequiredScopes": true,
- "requiredScopes": ["openid", "resource:read", "resource:write"]
+ "requiredScopes": ["openid", "resource:read", "resource:write"],
+ "confirmationMethodValidation" : {
+ "ignoreMissing": false,
+ "certificateBoundThumbprint" : {
+ "enabled": false,
+ "extractCertificateFromHeader": false,
+ "headerName": "ssl-client-cert"
+ }
+ }
}
}
----
@@ -154,6 +192,8 @@ APIM supports two types of authorization server:
* Access token can not be validated by authorization server
+* Confirmation method can not be validated
+
.^| ```403```
| Issue encountered:
diff --git a/pom.xml b/pom.xml
index 4c494764..59994673 100644
--- a/pom.xml
+++ b/pom.xml
@@ -38,8 +38,8 @@
3.0.0
1.11.0
4.0.0
- 2.1.1
- 4.0.0-SNAPSHOT
+ 3.0.0
+ 4.1.0-SNAPSHOT
1.1.0
1.3.0
1.4.0
diff --git a/src/main/java/io/gravitee/policy/oauth2/Oauth2Policy.java b/src/main/java/io/gravitee/policy/oauth2/Oauth2Policy.java
index 6b15bd9f..87f7a680 100644
--- a/src/main/java/io/gravitee/policy/oauth2/Oauth2Policy.java
+++ b/src/main/java/io/gravitee/policy/oauth2/Oauth2Policy.java
@@ -196,6 +196,24 @@ private Completable validateOAuth2Payload(
}
}
+ // Check confirmation Method and certificate thumbprint
+ OAuth2PolicyConfiguration.ConfirmationMethodValidation confirmationMethodValidation =
+ oAuth2PolicyConfiguration.getConfirmationMethodValidation();
+ if (confirmationMethodValidation != null && confirmationMethodValidation.getCertificateBoundThumbprint().isEnabled()) {
+ String tokenThumbprint = tokenIntrospectionResult.extractPath(OAUTH_PAYLOAD_CNF).path(OAUTH_PAYLOAD_X5T).asText();
+ if (
+ !isValidCertificateThumbprint(
+ tokenThumbprint,
+ ctx.request().sslSession(),
+ ctx.request().headers(),
+ confirmationMethodValidation.isIgnoreMissing(),
+ confirmationMethodValidation.getCertificateBoundThumbprint()
+ )
+ ) {
+ return sendError(ctx, OAUTH2_INVALID_CERTIFICATE_BOUND_THUMBPRINT);
+ }
+ }
+
// Store OAuth2 payload into execution context if required
if (oAuth2PolicyConfiguration.isExtractPayload()) {
ctx.setAttribute(CONTEXT_ATTRIBUTE_OAUTH_PAYLOAD, tokenIntrospectionResult.getOauth2ResponsePayload());
diff --git a/src/main/java/io/gravitee/policy/oauth2/configuration/OAuth2PolicyConfiguration.java b/src/main/java/io/gravitee/policy/oauth2/configuration/OAuth2PolicyConfiguration.java
index 41af9b14..57e17ff6 100644
--- a/src/main/java/io/gravitee/policy/oauth2/configuration/OAuth2PolicyConfiguration.java
+++ b/src/main/java/io/gravitee/policy/oauth2/configuration/OAuth2PolicyConfiguration.java
@@ -18,11 +18,19 @@
import io.gravitee.policy.api.PolicyConfiguration;
import java.util.ArrayList;
import java.util.List;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
/**
* @author David BRASSELY (david.brassely at graviteesource.com)
* @author GraviteeSource Team
*/
+@NoArgsConstructor
+@AllArgsConstructor
+@Getter
+@Setter
public class OAuth2PolicyConfiguration implements PolicyConfiguration {
private String oauthResource;
@@ -31,61 +39,29 @@ public class OAuth2PolicyConfiguration implements PolicyConfiguration {
private boolean checkRequiredScopes = false;
private List requiredScopes = new ArrayList<>();
private boolean modeStrict = true;
- private boolean propagateAuthHeader = true;
-
- public String getOauthResource() {
- return oauthResource;
- }
-
- public void setOauthResource(String oauthResource) {
- this.oauthResource = oauthResource;
- }
-
- public boolean isExtractPayload() {
- return extractPayload;
- }
-
- public void setExtractPayload(boolean extractPayload) {
- this.extractPayload = extractPayload;
- }
-
- public boolean isCheckRequiredScopes() {
- return checkRequiredScopes;
- }
- public void setCheckRequiredScopes(boolean checkRequiredScopes) {
- this.checkRequiredScopes = checkRequiredScopes;
- }
-
- public List getRequiredScopes() {
- return requiredScopes;
- }
+ private ConfirmationMethodValidation confirmationMethodValidation = new ConfirmationMethodValidation();
- public void setRequiredScopes(List requiredScopes) {
- this.requiredScopes = requiredScopes;
- }
-
- public boolean isModeStrict() {
- return modeStrict;
- }
+ private boolean propagateAuthHeader = true;
- public void setModeStrict(boolean modeStrict) {
- this.modeStrict = modeStrict;
- }
+ @NoArgsConstructor
+ @AllArgsConstructor
+ @Getter
+ @Setter
+ public static class ConfirmationMethodValidation {
- public boolean isPropagateAuthHeader() {
- return propagateAuthHeader;
+ private boolean ignoreMissing = false;
+ private CertificateBoundThumbprint certificateBoundThumbprint = new CertificateBoundThumbprint();
}
- public void setPropagateAuthHeader(boolean propagateAuthHeader) {
- this.propagateAuthHeader = propagateAuthHeader;
- }
-
- public String getOauthCacheResource() {
- return oauthCacheResource;
- }
+ @NoArgsConstructor
+ @AllArgsConstructor
+ @Getter
+ @Setter
+ public static class CertificateBoundThumbprint {
- public void setOauthCacheResource(String oauthCacheResource) {
- this.oauthCacheResource = oauthCacheResource;
+ private boolean enabled = false;
+ private boolean extractCertificateFromHeader = false;
+ private String headerName = "ssl-client-cert";
}
}
diff --git a/src/main/java/io/gravitee/policy/oauth2/introspection/TokenIntrospectionResult.java b/src/main/java/io/gravitee/policy/oauth2/introspection/TokenIntrospectionResult.java
index e79c2af5..480b3053 100644
--- a/src/main/java/io/gravitee/policy/oauth2/introspection/TokenIntrospectionResult.java
+++ b/src/main/java/io/gravitee/policy/oauth2/introspection/TokenIntrospectionResult.java
@@ -60,8 +60,9 @@ public TokenIntrospectionResult(String oauth2ResponsePayload) {
}
public String getClientId() {
- if (hasValidPayload()) {
- return oAuth2ResponseJsonNode.path(OAUTH_PAYLOAD_CLIENT_ID_NODE).asText();
+ JsonNode extractPath = extractPath(OAUTH_PAYLOAD_CLIENT_ID_NODE);
+ if (extractPath != null) {
+ return extractPath.asText();
}
return null;
}
@@ -101,8 +102,16 @@ public List extractScopes(String scopeSeparator) {
}
public String extractUser(String userClaim) {
+ JsonNode extractPath = extractPath(userClaim == null ? OAUTH_PAYLOAD_SUB_NODE : userClaim);
+ if (extractPath != null) {
+ return extractPath.asText();
+ }
+ return null;
+ }
+
+ public JsonNode extractPath(String path) {
if (hasValidPayload()) {
- return oAuth2ResponseJsonNode.path(userClaim == null ? OAUTH_PAYLOAD_SUB_NODE : userClaim).asText();
+ return oAuth2ResponseJsonNode.path(path);
}
return null;
}
diff --git a/src/main/java/io/gravitee/policy/v3/oauth2/Oauth2PolicyV3.java b/src/main/java/io/gravitee/policy/v3/oauth2/Oauth2PolicyV3.java
index 89a63e51..a5d2e173 100644
--- a/src/main/java/io/gravitee/policy/v3/oauth2/Oauth2PolicyV3.java
+++ b/src/main/java/io/gravitee/policy/v3/oauth2/Oauth2PolicyV3.java
@@ -22,11 +22,13 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import io.gravitee.common.http.HttpStatusCode;
+import io.gravitee.common.security.CertificateUtils;
import io.gravitee.gateway.api.ExecutionContext;
import io.gravitee.gateway.api.Request;
import io.gravitee.gateway.api.Response;
import io.gravitee.gateway.api.handler.Handler;
import io.gravitee.gateway.api.http.HttpHeaderNames;
+import io.gravitee.gateway.api.http.HttpHeaders;
import io.gravitee.policy.api.PolicyChain;
import io.gravitee.policy.api.PolicyResult;
import io.gravitee.policy.api.annotations.OnRequest;
@@ -39,11 +41,14 @@
import io.gravitee.resource.oauth2.api.OAuth2Resource;
import io.gravitee.resource.oauth2.api.OAuth2Response;
import java.io.IOException;
+import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
+import java.util.Optional;
+import javax.net.ssl.SSLSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
@@ -64,6 +69,8 @@ public class Oauth2PolicyV3 {
public static final String OAUTH_PAYLOAD_CLIENT_ID_NODE = "client_id";
public static final String OAUTH_PAYLOAD_SUB_NODE = "sub";
public static final String OAUTH_PAYLOAD_EXP = "exp";
+ public static final String OAUTH_PAYLOAD_CNF = "cnf";
+ public static final String OAUTH_PAYLOAD_X5T = "x5t#S256";
public static final String CONTEXT_ATTRIBUTE_PREFIX = "oauth.";
public static final String CONTEXT_ATTRIBUTE_OAUTH_PAYLOAD = CONTEXT_ATTRIBUTE_PREFIX + "payload";
@@ -76,6 +83,7 @@ public class Oauth2PolicyV3 {
public static final String OAUTH2_INVALID_ACCESS_TOKEN_KEY = "OAUTH2_INVALID_ACCESS_TOKEN";
public static final String OAUTH2_INVALID_SERVER_RESPONSE_KEY = "OAUTH2_INVALID_SERVER_RESPONSE";
public static final String OAUTH2_INSUFFICIENT_SCOPE_KEY = "OAUTH2_INSUFFICIENT_SCOPE";
+ public static final String OAUTH2_INVALID_CERTIFICATE_BOUND_THUMBPRINT = "OAUTH2_INVALID_CERTIFICATE_BOUND_THUMBPRINT";
public static final String OAUTH2_SERVER_UNAVAILABLE_KEY = "OAUTH2_SERVER_UNAVAILABLE";
public static final String OAUTH2_UNAUTHORIZED_MESSAGE = "Unauthorized";
@@ -224,6 +232,25 @@ private void handleSuccess(
}
}
+ // Check confirmation Method and certificate thumbprint
+ OAuth2PolicyConfiguration.ConfirmationMethodValidation confirmationMethodValidation =
+ oAuth2PolicyConfiguration.getConfirmationMethodValidation();
+ if (confirmationMethodValidation != null && confirmationMethodValidation.getCertificateBoundThumbprint().isEnabled()) {
+ String tokenThumbprint = oauthResponseNode.path(OAUTH_PAYLOAD_CNF).path(OAUTH_PAYLOAD_X5T).asText();
+ if (
+ !isValidCertificateThumbprint(
+ tokenThumbprint,
+ request.sslSession(),
+ request.headers(),
+ confirmationMethodValidation.isIgnoreMissing(),
+ confirmationMethodValidation.getCertificateBoundThumbprint()
+ )
+ ) {
+ sendError(OAUTH2_INVALID_CERTIFICATE_BOUND_THUMBPRINT, response, policyChain);
+ return;
+ }
+ }
+
// Store OAuth2 payload into execution context if required
if (oAuth2PolicyConfiguration.isExtractPayload()) {
executionContext.setAttribute(CONTEXT_ATTRIBUTE_OAUTH_PAYLOAD, oauth2payload);
@@ -301,4 +328,31 @@ protected static boolean hasRequiredScopes(Collection tokenScopes, List<
return tokenScopes.stream().anyMatch(requiredScopes::contains);
}
}
+
+ protected static boolean isValidCertificateThumbprint(
+ final String tokenThumbprint,
+ final SSLSession sslSession,
+ final HttpHeaders headers,
+ final boolean ignoreMissingCnf,
+ final OAuth2PolicyConfiguration.CertificateBoundThumbprint certificateBoundThumbprint
+ ) {
+ // Ignore empty configuration method
+ if (!StringUtils.hasText(tokenThumbprint) && ignoreMissingCnf) {
+ return true;
+ } else if (!StringUtils.hasText(tokenThumbprint) && !ignoreMissingCnf) {
+ return false;
+ }
+
+ // Compute client certificate thumbprint
+ Optional clientCertificate;
+ if (certificateBoundThumbprint.isExtractCertificateFromHeader()) {
+ clientCertificate = CertificateUtils.extractCertificate(headers, certificateBoundThumbprint.getHeaderName());
+ } else {
+ clientCertificate = CertificateUtils.extractPeerCertificate(sslSession);
+ }
+ return clientCertificate
+ .map(x509Certificate -> CertificateUtils.generateThumbprint(x509Certificate, "SHA-256"))
+ .map(tokenThumbprint::equals)
+ .orElse(false);
+ }
}
diff --git a/src/main/resources/schemas/schema-form.json b/src/main/resources/schemas/schema-form.json
index 793f9e79..b53ca437 100644
--- a/src/main/resources/schemas/schema-form.json
+++ b/src/main/resources/schemas/schema-form.json
@@ -65,6 +65,43 @@
"type": "boolean",
"default": true
},
+ "confirmationMethodValidation": {
+ "type": "object",
+ "title": "Confirmation Method Validation",
+ "properties": {
+ "ignoreMissing": {
+ "title": "Ignore missing CNF",
+ "description": "Will ignore CNF validation if the token doesn't contain any CNF information. Default is false.",
+ "type": "boolean",
+ "default": false
+ },
+ "certificateBoundThumbprint": {
+ "type": "object",
+ "title": "Certificate Bound thumbprint (x5t#S256)",
+ "properties": {
+ "enabled": {
+ "title": "Enable certificate bound thumbprint validation",
+ "description": "Will validate the certificate thumbprint extracted from the access_token with the one provided by the client. The default is false.",
+ "type": "boolean",
+ "default": false
+ },
+ "extractCertificateFromHeader": {
+ "title": "Extract client certificate from headers",
+ "description": "Enabled to extract the client certificate from request header. Necessary when the M-TLS connection is handled by a proxy.",
+ "type": "boolean",
+ "default": false
+ },
+ "headerName": {
+ "title": "Header name",
+ "description": "Name of the header where to find the client certificate.",
+ "type": "string",
+ "default": "ssl-client-cert"
+ }
+ }
+ }
+ },
+ "additionalProperties": false
+ },
"propagateAuthHeader": {
"title": "Permit authorization header to the target endpoints",
"description": "Allows to propagate Authorization header to the target endpoints",
diff --git a/src/test/java/io/gravitee/policy/oauth2/DummyOAuth2Resource.java b/src/test/java/io/gravitee/policy/oauth2/DummyOAuth2Resource.java
index 3feb4a89..c12f964b 100644
--- a/src/test/java/io/gravitee/policy/oauth2/DummyOAuth2Resource.java
+++ b/src/test/java/io/gravitee/policy/oauth2/DummyOAuth2Resource.java
@@ -30,6 +30,7 @@ public class DummyOAuth2Resource extends OAuth2Resource responseHandl
response = new OAuth2Response(true, "{}");
} else if (TOKEN_SUCCESS_WITH_INVALID_PAYLOAD.equals(accessToken)) {
response = new OAuth2Response(true, "{this _is _invalid json");
+ } else if (TOKEN_SUCCESS_WITH_CNF.equals(accessToken)) {
+ response =
+ new OAuth2Response(
+ true,
+ "{\"client_id\": \"" + CLIENT_ID + "\", \"cnf\": { \"x5t#S256\" : \"2oHrNOqScxD8EHkb7_GYmnNvWqGj5M31Dqsrk3Jl2Yk\"}}"
+ );
} else if (TOKEN_FAIL.equals(accessToken)) {
response = new OAuth2Response(false, null);
} else {
diff --git a/src/test/java/io/gravitee/policy/oauth2/Oauth2PolicyV4EmulationEngineCnfIntegrationTest.java b/src/test/java/io/gravitee/policy/oauth2/Oauth2PolicyV4EmulationEngineCnfIntegrationTest.java
new file mode 100644
index 00000000..c2c8b2b3
--- /dev/null
+++ b/src/test/java/io/gravitee/policy/oauth2/Oauth2PolicyV4EmulationEngineCnfIntegrationTest.java
@@ -0,0 +1,491 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.gravitee.policy.oauth2;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.ok;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+import static io.gravitee.policy.oauth2.DummyOAuth2Resource.CLIENT_ID;
+import static java.util.concurrent.TimeUnit.HOURS;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.github.tomakehurst.wiremock.WireMockServer;
+import io.gravitee.apim.gateway.tests.sdk.AbstractPolicyTest;
+import io.gravitee.apim.gateway.tests.sdk.annotations.DeployApi;
+import io.gravitee.apim.gateway.tests.sdk.annotations.GatewayTest;
+import io.gravitee.apim.gateway.tests.sdk.configuration.GatewayConfigurationBuilder;
+import io.gravitee.apim.gateway.tests.sdk.resource.ResourceBuilder;
+import io.gravitee.definition.model.Api;
+import io.gravitee.definition.model.Plan;
+import io.gravitee.gateway.api.service.Subscription;
+import io.gravitee.gateway.api.service.SubscriptionService;
+import io.gravitee.gateway.reactive.api.policy.SecurityToken;
+import io.gravitee.plugin.resource.ResourcePlugin;
+import io.gravitee.policy.oauth2.configuration.OAuth2PolicyConfiguration;
+import io.gravitee.policy.v3.oauth2.Oauth2PolicyV3IntegrationTest;
+import io.reactivex.rxjava3.core.Single;
+import io.vertx.core.http.HttpClientOptions;
+import io.vertx.core.http.HttpMethod;
+import io.vertx.core.net.PemKeyCertOptions;
+import io.vertx.rxjava3.core.http.HttpClient;
+import io.vertx.rxjava3.core.http.HttpClientResponse;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.net.URLEncoder;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Map;
+import java.util.Optional;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.stubbing.OngoingStubbing;
+
+/**
+ * @author GraviteeSource Team
+ */
+public class Oauth2PolicyV4EmulationEngineCnfIntegrationTest {
+
+ public static final String API_ID = "my-api";
+ public static final String PLAN_ID = "plan-id";
+
+ public static void configureHttpClient(HttpClientOptions options, int gatewayPort) {
+ options.setDefaultHost("localhost").setDefaultPort(gatewayPort).setSsl(true).setVerifyHost(false).setTrustAll(true);
+ }
+
+ public static void configureGateway(GatewayConfigurationBuilder gatewayConfigurationBuilder) {
+ gatewayConfigurationBuilder
+ .set("http.secured", true)
+ .set("http.alpn", true)
+ .set("http.ssl.keystore.type", "self-signed")
+ .set("http.ssl.clientAuth", "request")
+ .set("http.ssl.truststore.type", "pkcs12")
+ .set("http.ssl.truststore.path", Oauth2PolicyV3IntegrationTest.class.getResource("/certificate/keystore.p12").getPath())
+ .set("http.ssl.truststore.password", "gravitee");
+ }
+
+ public static void configureResources(Map resources) {
+ resources.put("dummy-oauth2-resource", ResourceBuilder.build("dummy-oauth2-resource", DummyOAuth2Resource.class));
+ }
+
+ public static Subscription fakeSubscriptionFromCache(boolean isExpired) {
+ final Subscription subscription = new Subscription();
+ subscription.setApplication("application-id");
+ subscription.setId("subscription-id");
+ subscription.setPlan(PLAN_ID);
+ if (isExpired) {
+ subscription.setEndingAt(new Date(Instant.now().minus(1, HOURS.toChronoUnit()).toEpochMilli()));
+ }
+ return subscription;
+ }
+
+ public static SecurityToken securityTokenMatcher(String clientId) {
+ return argThat(securityToken ->
+ securityToken.getTokenType().equals(SecurityToken.TokenType.CLIENT_ID.name()) && securityToken.getTokenValue().equals(clientId)
+ );
+ }
+
+ public static Plan createOauth2Plan(final Api api) {
+ Plan oauth2Plan = new Plan();
+ oauth2Plan.setId(PLAN_ID);
+ oauth2Plan.setApi(api.getId());
+ oauth2Plan.setSecurity("OAUTH2");
+ oauth2Plan.setStatus("PUBLISHED");
+ return oauth2Plan;
+ }
+
+ public static void assert401unauthorized(WireMockServer wiremock, Single httpClientResponse)
+ throws InterruptedException {
+ httpClientResponse
+ .flatMapPublisher(response -> {
+ assertThat(response.statusCode()).isEqualTo(401);
+ return response.body().toFlowable();
+ })
+ .test()
+ .await()
+ .assertComplete()
+ .assertValue(body -> {
+ assertThat(body.toString()).isEqualTo("Unauthorized");
+ return true;
+ })
+ .assertNoErrors();
+
+ wiremock.verify(0, getRequestedFor(urlPathEqualTo("/team")));
+ }
+
+ @Nested
+ @GatewayTest
+ @DeployApi("/apis/oauth2.json")
+ public class Oauth2PolicyV4EmulationEngineMissingCnfIntegrationTest extends AbstractOauth2PolicyMissingCnfIntegrationTest {}
+
+ public static class AbstractOauth2PolicyMissingCnfIntegrationTest extends AbstractPolicyTest {
+
+ @Override
+ public void configureGateway(GatewayConfigurationBuilder gatewayConfigurationBuilder) {
+ Oauth2PolicyV4EmulationEngineCnfIntegrationTest.configureGateway(gatewayConfigurationBuilder);
+ }
+
+ @Override
+ protected void configureHttpClient(final HttpClientOptions options) {
+ Oauth2PolicyV4EmulationEngineCnfIntegrationTest.configureHttpClient(options, gatewayPort());
+ }
+
+ @Override
+ public void configureApi(final Api api) {
+ Plan oauth2Plan = createOauth2Plan(api);
+
+ OAuth2PolicyConfiguration configuration = new OAuth2PolicyConfiguration();
+ configuration.setOauthResource("dummy-oauth2-resource");
+ configuration.getConfirmationMethodValidation().setIgnoreMissing(true);
+ configuration.getConfirmationMethodValidation().getCertificateBoundThumbprint().setEnabled(true);
+
+ try {
+ oauth2Plan.setSecurityDefinition(new ObjectMapper().writeValueAsString(configuration));
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException("Failed to set OAuth2 policy configuration", e);
+ }
+
+ api.setPlans(Collections.singletonList(oauth2Plan));
+ }
+
+ @Override
+ public void configureResources(final Map resources) {
+ Oauth2PolicyV4EmulationEngineCnfIntegrationTest.configureResources(resources);
+ }
+
+ @Test
+ void should_access_api_and_ignore_missing_cnf(HttpClient client) throws InterruptedException {
+ wiremock.stubFor(get("/team").willReturn(ok("response from backend")));
+
+ // subscription found is valid
+ whenSearchingSubscription(API_ID, CLIENT_ID, PLAN_ID).thenReturn(Optional.of(fakeSubscriptionFromCache(false)));
+
+ client
+ .rxRequest(HttpMethod.GET, "/test")
+ .flatMap(request ->
+ request.putHeader("Authorization", "Bearer " + DummyOAuth2Resource.TOKEN_SUCCESS_WITH_CLIENT_ID).rxSend()
+ )
+ .flatMapPublisher(response -> {
+ assertThat(response.statusCode()).isEqualTo(200);
+ return response.toFlowable();
+ })
+ .test()
+ .await()
+ .assertComplete()
+ .assertValue(body -> {
+ assertThat(body).hasToString("response from backend");
+ return true;
+ })
+ .assertNoErrors();
+
+ wiremock.verify(1, getRequestedFor(urlPathEqualTo("/team")));
+ }
+
+ protected OngoingStubbing> whenSearchingSubscription(String api, String clientId, String plan) {
+ return when(getBean(SubscriptionService.class).getByApiAndSecurityToken(eq(api), securityTokenMatcher(clientId), eq(plan)));
+ }
+ }
+
+ @Nested
+ @DeployApi("/apis/oauth2.json")
+ @GatewayTest
+ public class Oauth2PolicyV4EmulationEngineCnfHeaderCertificateIntegrationTest
+ extends AbstractOauth2PolicyCnfHeaderCertificateIntegrationTest {}
+
+ public static class AbstractOauth2PolicyCnfHeaderCertificateIntegrationTest
+ extends AbstractPolicyTest {
+
+ @Override
+ public void configureGateway(GatewayConfigurationBuilder gatewayConfigurationBuilder) {
+ Oauth2PolicyV4EmulationEngineCnfIntegrationTest.configureGateway(gatewayConfigurationBuilder);
+ }
+
+ @Override
+ protected void configureHttpClient(final HttpClientOptions options) {
+ Oauth2PolicyV4EmulationEngineCnfIntegrationTest.configureHttpClient(options, gatewayPort());
+ }
+
+ @Override
+ public void configureApi(final Api api) {
+ Plan oauth2Plan = createOauth2Plan(api);
+
+ OAuth2PolicyConfiguration configuration = new OAuth2PolicyConfiguration();
+ configuration.setOauthResource("dummy-oauth2-resource");
+ configuration.getConfirmationMethodValidation().getCertificateBoundThumbprint().setEnabled(true);
+ configuration.getConfirmationMethodValidation().getCertificateBoundThumbprint().setExtractCertificateFromHeader(true);
+ try {
+ oauth2Plan.setSecurityDefinition(new ObjectMapper().writeValueAsString(configuration));
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException("Failed to set OAuth2 policy configuration", e);
+ }
+
+ api.setPlans(Collections.singletonList(oauth2Plan));
+ }
+
+ @Override
+ public void configureResources(final Map resources) {
+ Oauth2PolicyV4EmulationEngineCnfIntegrationTest.configureResources(resources);
+ }
+
+ @Test
+ void should_access_api_with_valid_certificate_from_header(HttpClient client)
+ throws InterruptedException, URISyntaxException, IOException {
+ wiremock.stubFor(get("/team").willReturn(ok("response from backend")));
+
+ // subscription found is valid
+ whenSearchingSubscription(API_ID, CLIENT_ID, PLAN_ID).thenReturn(Optional.of(fakeSubscriptionFromCache(false)));
+
+ String clientCert = Files.readString(
+ Paths.get(Oauth2PolicyV3IntegrationTest.class.getResource("/certificate/client1-crt.pem").toURI())
+ );
+ String encoded = URLEncoder.encode(clientCert, Charset.defaultCharset());
+
+ client
+ .rxRequest(HttpMethod.GET, "/test")
+ .flatMap(request ->
+ request
+ .putHeader("Authorization", "Bearer " + DummyOAuth2Resource.TOKEN_SUCCESS_WITH_CNF)
+ .putHeader("ssl-client-cert", encoded)
+ .rxSend()
+ )
+ .flatMapPublisher(response -> {
+ assertThat(response.statusCode()).isEqualTo(200);
+ return response.toFlowable();
+ })
+ .test()
+ .await()
+ .assertComplete()
+ .assertValue(body -> {
+ assertThat(body).hasToString("response from backend");
+ return true;
+ })
+ .assertNoErrors();
+
+ wiremock.verify(1, getRequestedFor(urlPathEqualTo("/team")));
+ }
+
+ @Test
+ void should_return_401_without_valid_certificate_in_header(HttpClient client) throws InterruptedException {
+ wiremock.stubFor(get("/team").willReturn(ok("response from backend")));
+
+ // subscription found is valid
+ whenSearchingSubscription(API_ID, CLIENT_ID, PLAN_ID).thenReturn(Optional.of(fakeSubscriptionFromCache(false)));
+
+ Single httpClientResponse = client
+ .rxRequest(HttpMethod.GET, "/test")
+ .flatMap(request ->
+ request
+ .putHeader("Authorization", "Bearer " + DummyOAuth2Resource.TOKEN_SUCCESS_WITH_CNF)
+ .putHeader("ssl-client-cert", "wrong")
+ .rxSend()
+ );
+
+ assert401unauthorized(wiremock, httpClientResponse);
+ }
+
+ @Test
+ void should_return_401_with_valid_certificate_in_header_but_without_cnf_in_token(HttpClient client)
+ throws InterruptedException, URISyntaxException, IOException {
+ wiremock.stubFor(get("/team").willReturn(ok("response from backend")));
+ // subscription found is valid
+ whenSearchingSubscription(API_ID, CLIENT_ID, PLAN_ID).thenReturn(Optional.of(fakeSubscriptionFromCache(false)));
+
+ String clientCert = Files.readString(
+ Paths.get(Oauth2PolicyV3IntegrationTest.class.getResource("/certificate/client1-crt.pem").toURI())
+ );
+ String encoded = URLEncoder.encode(clientCert, Charset.defaultCharset());
+
+ Single httpClientResponse = client
+ .rxRequest(HttpMethod.GET, "/test")
+ .flatMap(request ->
+ request
+ .putHeader("Authorization", "Bearer " + DummyOAuth2Resource.TOKEN_SUCCESS_WITH_CLIENT_ID)
+ .putHeader("ssl-client-cert", encoded)
+ .rxSend()
+ );
+
+ assert401unauthorized(wiremock, httpClientResponse);
+ }
+
+ protected OngoingStubbing> whenSearchingSubscription(String api, String clientId, String plan) {
+ return when(getBean(SubscriptionService.class).getByApiAndSecurityToken(eq(api), securityTokenMatcher(clientId), eq(plan)));
+ }
+ }
+
+ @Nested
+ @DeployApi("/apis/oauth2.json")
+ @GatewayTest
+ public class Oauth2PolicyV4EmulationEngineCnfPeerCertificateIntegrationTest
+ extends AbstractOauth2PolicyCnfPeerCertificateIntegrationTest {}
+
+ public static class AbstractOauth2PolicyCnfPeerCertificateIntegrationTest
+ extends AbstractPolicyTest {
+
+ @Override
+ public void configureGateway(GatewayConfigurationBuilder gatewayConfigurationBuilder) {
+ Oauth2PolicyV4EmulationEngineCnfIntegrationTest.configureGateway(gatewayConfigurationBuilder);
+ }
+
+ @Override
+ protected void configureHttpClient(final HttpClientOptions options) {
+ Oauth2PolicyV4EmulationEngineCnfIntegrationTest.configureHttpClient(options, gatewayPort());
+
+ final PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions();
+ pemKeyCertOptions.setCertPath(Oauth2PolicyV3IntegrationTest.class.getResource("/certificate/client1-crt.pem").getPath());
+ pemKeyCertOptions.setKeyPath(Oauth2PolicyV3IntegrationTest.class.getResource("/certificate/client1-key.pem").getPath());
+ options.setPemKeyCertOptions(pemKeyCertOptions);
+ }
+
+ @Override
+ public void configureApi(final Api api) {
+ Plan oauth2Plan = createOauth2Plan(api);
+
+ OAuth2PolicyConfiguration configuration = new OAuth2PolicyConfiguration();
+ configuration.setOauthResource("dummy-oauth2-resource");
+ configuration.getConfirmationMethodValidation().getCertificateBoundThumbprint().setEnabled(true);
+ try {
+ oauth2Plan.setSecurityDefinition(new ObjectMapper().writeValueAsString(configuration));
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException("Failed to set OAuth2 policy configuration", e);
+ }
+
+ api.setPlans(Collections.singletonList(oauth2Plan));
+ }
+
+ @Override
+ public void configureResources(final Map resources) {
+ Oauth2PolicyV4EmulationEngineCnfIntegrationTest.configureResources(resources);
+ }
+
+ @Test
+ void should_access_api_with_valid_certificate_from_ssl_session(HttpClient client)
+ throws InterruptedException, URISyntaxException, IOException {
+ wiremock.stubFor(get("/team").willReturn(ok("response from backend")));
+
+ // subscription found is valid
+ whenSearchingSubscription(API_ID, CLIENT_ID, PLAN_ID).thenReturn(Optional.of(fakeSubscriptionFromCache(false)));
+
+ client
+ .rxRequest(HttpMethod.GET, "/test")
+ .flatMap(request -> request.putHeader("Authorization", "Bearer " + DummyOAuth2Resource.TOKEN_SUCCESS_WITH_CNF).rxSend())
+ .flatMapPublisher(response -> {
+ assertThat(response.statusCode()).isEqualTo(200);
+ return response.toFlowable();
+ })
+ .test()
+ .await()
+ .assertComplete()
+ .assertValue(body -> {
+ assertThat(body).hasToString("response from backend");
+ return true;
+ })
+ .assertNoErrors();
+
+ wiremock.verify(1, getRequestedFor(urlPathEqualTo("/team")));
+ }
+
+ @Test
+ void should_return_401_with_valid_certificate_from_ssl_session_but_without_cnf_in_token(HttpClient client)
+ throws InterruptedException {
+ wiremock.stubFor(get("/team").willReturn(ok("response from backend")));
+
+ // subscription found is valid
+ whenSearchingSubscription(API_ID, CLIENT_ID, PLAN_ID).thenReturn(Optional.of(fakeSubscriptionFromCache(false)));
+
+ Single httpClientResponse = client
+ .rxRequest(HttpMethod.GET, "/test")
+ .flatMap(request ->
+ request.putHeader("Authorization", "Bearer " + DummyOAuth2Resource.TOKEN_SUCCESS_WITH_CLIENT_ID).rxSend()
+ );
+
+ assert401unauthorized(wiremock, httpClientResponse);
+ }
+
+ protected OngoingStubbing> whenSearchingSubscription(String api, String clientId, String plan) {
+ return when(getBean(SubscriptionService.class).getByApiAndSecurityToken(eq(api), securityTokenMatcher(clientId), eq(plan)));
+ }
+ }
+
+ @Nested
+ @DeployApi("/apis/oauth2.json")
+ @GatewayTest
+ public class Oauth2PolicyV4EmulationEngineCnfInvalidPeerCertificateIntegrationTest
+ extends AbstractOauth2PolicyCnfInvalidPeerCertificateIntegrationTest {}
+
+ public static class AbstractOauth2PolicyCnfInvalidPeerCertificateIntegrationTest
+ extends AbstractPolicyTest {
+
+ @Override
+ public void configureGateway(GatewayConfigurationBuilder gatewayConfigurationBuilder) {
+ Oauth2PolicyV4EmulationEngineCnfIntegrationTest.configureGateway(gatewayConfigurationBuilder);
+ }
+
+ @Override
+ protected void configureHttpClient(final HttpClientOptions options) {
+ Oauth2PolicyV4EmulationEngineCnfIntegrationTest.configureHttpClient(options, gatewayPort());
+ }
+
+ @Override
+ public void configureApi(final Api api) {
+ Plan oauth2Plan = createOauth2Plan(api);
+
+ OAuth2PolicyConfiguration configuration = new OAuth2PolicyConfiguration();
+ configuration.setOauthResource("dummy-oauth2-resource");
+ configuration.getConfirmationMethodValidation().getCertificateBoundThumbprint().setEnabled(true);
+ try {
+ oauth2Plan.setSecurityDefinition(new ObjectMapper().writeValueAsString(configuration));
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException("Failed to set OAuth2 policy configuration", e);
+ }
+
+ api.setPlans(Collections.singletonList(oauth2Plan));
+ }
+
+ @Override
+ public void configureResources(final Map resources) {
+ Oauth2PolicyV4EmulationEngineCnfIntegrationTest.configureResources(resources);
+ }
+
+ @Test
+ void should_return_401_without_valid_peer_certificate_from_ssl_session(HttpClient client) throws InterruptedException {
+ wiremock.stubFor(get("/team").willReturn(ok("response from backend")));
+
+ // subscription found is valid
+ whenSearchingSubscription(API_ID, CLIENT_ID, PLAN_ID).thenReturn(Optional.of(fakeSubscriptionFromCache(false)));
+
+ Single httpClientResponse = client
+ .rxRequest(HttpMethod.GET, "/test")
+ .flatMap(request ->
+ request.putHeader("Authorization", "Bearer " + DummyOAuth2Resource.TOKEN_SUCCESS_WITH_CLIENT_ID).rxSend()
+ );
+
+ assert401unauthorized(wiremock, httpClientResponse);
+ }
+
+ protected OngoingStubbing> whenSearchingSubscription(String api, String clientId, String plan) {
+ return when(getBean(SubscriptionService.class).getByApiAndSecurityToken(eq(api), securityTokenMatcher(clientId), eq(plan)));
+ }
+ }
+}
diff --git a/src/test/java/io/gravitee/policy/v3/oauth2/Oauth2PolicyV3CnfIntegrationTest.java b/src/test/java/io/gravitee/policy/v3/oauth2/Oauth2PolicyV3CnfIntegrationTest.java
new file mode 100644
index 00000000..72bc56c4
--- /dev/null
+++ b/src/test/java/io/gravitee/policy/v3/oauth2/Oauth2PolicyV3CnfIntegrationTest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright © 2015 The Gravitee team (http://gravitee.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.gravitee.policy.v3.oauth2;
+
+import static io.gravitee.definition.model.ExecutionMode.V3;
+import static org.mockito.Mockito.when;
+
+import io.gravitee.apim.gateway.tests.sdk.annotations.DeployApi;
+import io.gravitee.apim.gateway.tests.sdk.annotations.GatewayTest;
+import io.gravitee.gateway.api.service.Subscription;
+import io.gravitee.gateway.api.service.SubscriptionService;
+import io.gravitee.policy.oauth2.Oauth2PolicyV4EmulationEngineCnfIntegrationTest;
+import java.util.Optional;
+import org.junit.jupiter.api.Nested;
+import org.mockito.stubbing.OngoingStubbing;
+
+/**
+ * @author GraviteeSource Team
+ */
+public class Oauth2PolicyV3CnfIntegrationTest {
+
+ @Nested
+ @DeployApi("/apis/oauth2.json")
+ @GatewayTest(v2ExecutionMode = V3)
+ public class Oauth2PolicyV3MissingCnfIntegrationTest
+ extends Oauth2PolicyV4EmulationEngineCnfIntegrationTest.AbstractOauth2PolicyMissingCnfIntegrationTest {
+
+ /**
+ * This overrides subscription search :
+ * - in jupiter its searched with getByApiAndSecurityToken
+ * - in V3 its searches with api/clientId/plan
+ */
+ protected OngoingStubbing> whenSearchingSubscription(String api, String clientId, String plan) {
+ return when(getBean(SubscriptionService.class).getByApiAndClientIdAndPlan(api, clientId, plan));
+ }
+ }
+
+ @Nested
+ @DeployApi("/apis/oauth2.json")
+ @GatewayTest(v2ExecutionMode = V3)
+ public class Oauth2PolicyV3CnfHeaderCertificateIntegrationTest
+ extends Oauth2PolicyV4EmulationEngineCnfIntegrationTest.AbstractOauth2PolicyCnfHeaderCertificateIntegrationTest {
+
+ /**
+ * This overrides subscription search :
+ * - in jupiter its searched with getByApiAndSecurityToken
+ * - in V3 its searches with api/clientId/plan
+ */
+ @Override
+ protected OngoingStubbing> whenSearchingSubscription(String api, String clientId, String plan) {
+ return when(getBean(SubscriptionService.class).getByApiAndClientIdAndPlan(api, clientId, plan));
+ }
+ }
+
+ @Nested
+ @DeployApi("/apis/oauth2.json")
+ @GatewayTest(v2ExecutionMode = V3)
+ public class Oauth2PolicyV3CnfPeerCertificateIntegrationTest
+ extends Oauth2PolicyV4EmulationEngineCnfIntegrationTest.AbstractOauth2PolicyCnfPeerCertificateIntegrationTest {
+
+ /**
+ * This overrides subscription search :
+ * - in jupiter its searched with getByApiAndSecurityToken
+ * - in V3 its searches with api/clientId/plan
+ */
+ @Override
+ protected OngoingStubbing> whenSearchingSubscription(String api, String clientId, String plan) {
+ return when(getBean(SubscriptionService.class).getByApiAndClientIdAndPlan(api, clientId, plan));
+ }
+ }
+
+ @Nested
+ @DeployApi("/apis/oauth2.json")
+ @GatewayTest(v2ExecutionMode = V3)
+ public class Oauth2PolicyV3CnfInvalidPeerCertificateIntegrationTest
+ extends Oauth2PolicyV4EmulationEngineCnfIntegrationTest.AbstractOauth2PolicyCnfInvalidPeerCertificateIntegrationTest {
+
+ /**
+ * This overrides subscription search :
+ * - in jupiter its searched with getByApiAndSecurityToken
+ * - in V3 its searches with api/clientId/plan
+ */
+ protected OngoingStubbing> whenSearchingSubscription(String api, String clientId, String plan) {
+ return when(getBean(SubscriptionService.class).getByApiAndClientIdAndPlan(api, clientId, plan));
+ }
+ }
+}
diff --git a/src/test/resources/certificate/ca-crt.pem b/src/test/resources/certificate/ca-crt.pem
new file mode 100644
index 00000000..65c6030c
--- /dev/null
+++ b/src/test/resources/certificate/ca-crt.pem
@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE-----
+MIICpjCCAY4CCQC3vXoVDq9I4TANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls
+b2NhbGhvc3QwIBcNMjMwODMwMDY0NzU0WhgPMjA1MTAxMTQwNjQ3NTRaMBQxEjAQ
+BgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+AJnhhNZEUwAt1CPo2lgvz0qntq+VeQkj/AtQa0vlHRtTKdGmlJitlOfvnvVCS+Dm
+bBt92mY9ej/yeA+e4G3V6fnpoVs/IeoTzOaw/99VrMSH9qdMHwv9V6JS2mCN7Lx9
+M2uApBc9GBiWy2UpPForL/04MJ0FdBkC/QxSKJu3w5cpyoSzMaFEa1rVrabqEZW6
+Y5ctwq4wtnnXT88hW/ioqRhw9KgcA2GExDAEKKPZlYAetSLRGEz1D/+/5kiRi8EJ
+F9U9bEokMCi2d3S3th/Q+b9KwIQ6AT/yOQfgkivNaNEiWldbtZ47m3vF+lfhG2T1
+mZPGsbzaDEJqTXrgLTT6cIkCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAGZrVurBx
+zV7NRgY+s3snZL5UXaSmrMCVEd238QhGSNbp/bA1Mvzo89kRSnttZ7u4EckmEtXa
+HMEV26DS/LiaRfj+EV3NPAWxhLf6458n2wQbLTz3tUyHMo44lC/I1BGS0myKM4L2
+lS4GH6I+MP94LJt07UHiRndyiO32svYgkCBT0VEPKLsT5E3s7wwyFX/eUrubZut4
+J3P4DgJ37fGZvGWQ8oVAHK3wa+lynYUXkAz9u15ZAU8YtSjShHwJxHQi50h0SD5A
+5To6YsPAvKGGeJtzCBI/J4/6Rkj+6hcyDEHrmzwbjqVVw5KZnRfQOL7UwgfaeFHk
+O996ZP0BXOTg5Q==
+-----END CERTIFICATE-----
diff --git a/src/test/resources/certificate/ca-crt.srl b/src/test/resources/certificate/ca-crt.srl
new file mode 100644
index 00000000..7d623602
--- /dev/null
+++ b/src/test/resources/certificate/ca-crt.srl
@@ -0,0 +1 @@
+C9A98094B1A3B2E4
diff --git a/src/test/resources/certificate/ca-key.pem b/src/test/resources/certificate/ca-key.pem
new file mode 100644
index 00000000..7a91f158
--- /dev/null
+++ b/src/test/resources/certificate/ca-key.pem
@@ -0,0 +1,30 @@
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIFHzBJBgkqhkiG9w0BBQ0wPDAbBgkqhkiG9w0BBQwwDgQIRhlG0vdvRLUCAggA
+MB0GCWCGSAFlAwQBKgQQe3ObWQqFqzsx4fpvLj1LewSCBNA5E4iYa4XgTX4vcfFB
+GFBathkSBkmg/J2G+TWNethFCglFVjVsrwtX4VSOoBnyo3Ab0o9Sog/5zLHvrnWw
+rL0rbRl8AqfqJaiGGaG4nkFIq1HLDeu0W6/6p6eLCg5lSK/r4xzxN2YIPrghwdPo
+BTt/K85Kvd0MnWzIukMCECvHmQAwzqLBKdSnuNco0Hb75XzayolZuT3sLwLApdhO
+chPuI+FX/Kc3NnTB8/5xT3JdYk7lBlJMCBECf+Km7v5kpAgqYnKF44lTBTfm3MyZ
+cdbcgDCq9jRBJHFuI8xNhSkD83fu4xUE4VHaA08lxmZI58xLnPnTh9XpI2jDlnoK
+UxHaB2/7JPZFTrDxcqxWgEFvx/FulQQhteA6bg5Rh8zF6tNz4bvZBUbnIVgH24pY
+psXSDMGMADJateqbA3JQ8gTzAIjS5FOKMw5ISFcMdqCkNORXJEeP11UClrEOx49q
+ww0Tyn1ZZb3LqOrHkkDx186w2lsvwXF6med88Z7CcK+QQK+S9vVIhRINJDotyXv5
+GCmTwLoKqk4Uhn0wLFfrS+4Q4BSAEbBcigvncYK+xoKKnHAmMtx2E3XC6gDkxAnW
+uADQmFEKmJDvR++UYb35fYIWUnUXXycxo0OXL2KapREMPhLu73X3BlmFrfDdvqB8
+yXO8nT4M8RNIi+IpRBN6ORM+V4qxMmzGYfcKcHBuzZQSMf04wh1dsPSM2Q3g08pI
+wRbOknsu3haGpD+CGJ3/iXpHUveWM+kZ1MiGMJ5k+EvGf8ugf05BIduqRiJ6/8Yk
+DhakmXyVSMG2jpLy10ukL/cVVPXYs/n42gJd9JfI2++B6rvgg/gNjBb7O9s51kXI
+r+T8fB1OdlUny9i+YScYAD2ac7Mvs4CmcRHMezuLNtWknzgjUTUOVQlaKCbxenFa
+HgplS4yRSeA7ikaAowID0+1Z3UGtl8jh+/FQk7SGg01ddLmcRvvMpAONDdQ3aGPj
+BXD5BuyW1sjL4c4qcxwVRQoRrlI8Mitd2P4x1P5OMQEcGPlQXqnGBirxxqqqj2Lp
+XCZoLXKQ/7zfxzjXDAGKkQTD4yNiA+u4ZljSm6rsuOTYWzBc6i2/SMPwqa8mb71x
+Je1DEDu79Kx3bwO9Qb8Bzm4d9x3oDbfGn15HAMOnrpdOFe+5asWnksXv7Z3gpU0w
+LhuqjLMVCpROPmXycLoQpHx3c2exc/efZIAvzaEzUzzTeAKUiI62v8ywu+jo7gNp
+OvC/B8i3OdJJGJtAz5/im5h1Zsk9HGtisS7fDN4cLk0EwgTeCQAFt7sOyXgyZ6rj
+kXD0vY4w2sYuecDD/430G4m4EoeQQnlViz2cA5ApunXovbhHGRALOdpXPX77K1ME
+bsCQeqKhqVqvVyy7m53V2jbSXFshraoyIv8d2SKq9ymrLIrqVumkzSkdA+Ciki66
+T5otBP4JwgARZag25bUIL7Zz8zGpKmBAbnw947XS5EU0BrpwWyLxXSA25HETUi3e
+n11hLYQLtlFVoLWmIbyZPsfbJ7veuMxMxK5aXQvHHSyJ92Pa9xcMsdzvxDexHCgs
+ODnwz+adJLIxTRPRJ3g54StckK5TDjBaFQhyisCahSosOZpiDX7fccBiVEStCfD3
+iFHRc/q7BwGyOEHIhvyn2y9xng==
+-----END ENCRYPTED PRIVATE KEY-----
diff --git a/src/test/resources/certificate/client1-crt.pem b/src/test/resources/certificate/client1-crt.pem
new file mode 100644
index 00000000..7cf6209b
--- /dev/null
+++ b/src/test/resources/certificate/client1-crt.pem
@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDpjCCAo4CCQDJqYCUsaOy5DANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls
+b2NhbGhvc3QwIBcNMjMwODMwMDY0ODA5WhgPMjA1MTAxMTQwNjQ4MDlaMBQxEjAQ
+BgNVBAMMCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
+AMt4R19sPi99A7sAYoDlA645j+yiLL7Ope7rOlndVn8yzILpTFPq4uv3j2Kuq46O
+3bbfoEKVIdkpYTzG/wBi1L7uedpA37Fe/qkR/TWYweFl07cHg5rAqiaqaPAmR6wv
+sAG48ZCyp5MsSEVjXwGWyRioJlwVcXunlD74BGV1KtWaqhYbN+r6l0r/wdbkKXBk
+VylPbGTW47Lct0iWt0nxzE4G+WhGsU7TT6yrqbs33vIdpMQRnjqa+6xXoOp4wHu/
+6HXBwTuEhXFqfk3YYO78ZN4tiYBwZDbR4RjQOSCWZgYO6ND1U7RnUcEcNnR7Gyyq
+AoOB1pwGldch7GCEUk5PbrJEINtTHH1aoljQXbOfkStrvM62A2NrEbuRgTPFczi7
+7dUyzXwjOKh2ZYfRGEEGgxeHwboe03ePXxVjEC1lQgCd+VDcqyC/OvQvQRBk74Ew
+V+8NWimDlFdQ3sw0Iqahr5RiCsop5KASNeD8bSExaXQ2gEAHvZiqG2diBjuZn+9x
+HRh/bpRVo70JC7Qyx11YVaiumtgEQyMrDsW8GTsr3PMf9cghcbDsPj5wWRiDWnfn
+KM+PDlW4tFgS6hc87Q8zSDvtEf9qZxShsfVtmA3qOZEQOghRbRauyA3NWeyXJeWn
+iZDjk282Sf/5/L/23sc9GN+k4y3hRUBuHSBP9YyPwvxVAgMBAAEwDQYJKoZIhvcN
+AQELBQADggEBAGm7j6QtYHKMq/o14KBScsdpyjAEkI5YI+PwHjcgydPp50hvqouA
+bSNysjh5ufzhVOm7W9EvG++3cikl6yowKjVmNtOsxAVBdG6MXrT8vkbz5TVfyjbj
+IkwJl1Xt32XesFuxXLmO7XPgS0RrROWPBzYAt8TsbI2SzBI2k860gBRup614RfTF
+lLnPYFOIoUjtz9Nn96prVfjOwDk7jqdEj6Oh0z4DBtBZY2HmZ6QAc5cf/CPpb4WB
+N2gYxrwppTUq8EOzj1aFbacIXwO78XknRDHzgeGxr9LWSBy+vpqjo1BK+DpJwx4o
+LlgM3YAcXb6kyPkxzjwQzJ1P8uCNIocAmEs=
+-----END CERTIFICATE-----
diff --git a/src/test/resources/certificate/client1-csr.pem b/src/test/resources/certificate/client1-csr.pem
new file mode 100644
index 00000000..c6c3de1c
--- /dev/null
+++ b/src/test/resources/certificate/client1-csr.pem
@@ -0,0 +1,26 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIIEWTCCAkECAQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0B
+AQEFAAOCAg8AMIICCgKCAgEAy3hHX2w+L30DuwBigOUDrjmP7KIsvs6l7us6Wd1W
+fzLMgulMU+ri6/ePYq6rjo7dtt+gQpUh2SlhPMb/AGLUvu552kDfsV7+qRH9NZjB
+4WXTtweDmsCqJqpo8CZHrC+wAbjxkLKnkyxIRWNfAZbJGKgmXBVxe6eUPvgEZXUq
+1ZqqFhs36vqXSv/B1uQpcGRXKU9sZNbjsty3SJa3SfHMTgb5aEaxTtNPrKupuzfe
+8h2kxBGeOpr7rFeg6njAe7/odcHBO4SFcWp+Tdhg7vxk3i2JgHBkNtHhGNA5IJZm
+Bg7o0PVTtGdRwRw2dHsbLKoCg4HWnAaV1yHsYIRSTk9uskQg21McfVqiWNBds5+R
+K2u8zrYDY2sRu5GBM8VzOLvt1TLNfCM4qHZlh9EYQQaDF4fBuh7Td49fFWMQLWVC
+AJ35UNyrIL869C9BEGTvgTBX7w1aKYOUV1DezDQipqGvlGIKyinkoBI14PxtITFp
+dDaAQAe9mKobZ2IGO5mf73EdGH9ulFWjvQkLtDLHXVhVqK6a2ARDIysOxbwZOyvc
+8x/1yCFxsOw+PnBZGINad+coz48OVbi0WBLqFzztDzNIO+0R/2pnFKGx9W2YDeo5
+kRA6CFFtFq7IDc1Z7Jcl5aeJkOOTbzZJ//n8v/bexz0Y36TjLeFFQG4dIE/1jI/C
+/FUCAwEAAaAAMA0GCSqGSIb3DQEBCwUAA4ICAQDD6rezlVJfBNk582l3zZiYGdxA
+p0CHe8dmuryVk94OxaVOCSiWl550jPFYjZNb/7Z9/tau/zhJ+JyQT9u8l2XVgLuj
+pLDFWJmz6Gtv0TBrFEa1V6uTu7INBxqWxJDBh9QviwvzetVxT4+PkpNbBjY8E57H
+Nif59nha92a31J/Jytx6/X4GZNJCEJtFK4hqNZNhnsrc91+tqecHMJatvj7YxB38
+VQYOK64+iJ24v6r7bMuE9aAE8M6bJmRPupzL0Fl7yXISqrhYryeRrCXHyRVhUByh
+XiP5ycxKuMByuZz52S7N2qbpMvD7zO8lsNrYDA3Ecz4BmGXJ7uOHA00GnA4eRJJT
+IOKbpGHLo2j5WYLGetR1aBODH5gj6Kkg/oo+o/FazmDiv5toB5ltzemZ7+QtBgN4
+6Xs421zj4NYHN6kLR0O23cDiWbib9PuwJnCYNqdthOw7ykQVBkcE6mdm+dvdO3BF
+0SEajSz/l1jJX9AQWRWopHR074QISP8EIgYXFJgYPVytMStdnfg7Z7/NgMgUQ0jD
+QZyWv/3RLkviYUks3VvsE4qq/4FeFwb9dB4tIo1wMgQXve7e8prJIkQ5X3CiJUuJ
+YT3E+p+/Hg9IY1QjJ5wCsmwlwnX3Q6eptGePa5BPhB3XVTMrMTnmwKxf58xKwXyH
+9yQkb/jT834YKzsFJg==
+-----END CERTIFICATE REQUEST-----
diff --git a/src/test/resources/certificate/client1-key.pem b/src/test/resources/certificate/client1-key.pem
new file mode 100644
index 00000000..f45a17c1
--- /dev/null
+++ b/src/test/resources/certificate/client1-key.pem
@@ -0,0 +1,51 @@
+-----BEGIN RSA PRIVATE KEY-----
+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
+-----END RSA PRIVATE KEY-----
diff --git a/src/test/resources/certificate/keystore.p12 b/src/test/resources/certificate/keystore.p12
new file mode 100644
index 00000000..192fa482
Binary files /dev/null and b/src/test/resources/certificate/keystore.p12 differ