Skip to content

Commit

Permalink
Add MTLS certificate validator
Browse files Browse the repository at this point in the history
  • Loading branch information
RivinduM committed Mar 4, 2024
1 parent acc05a4 commit 210d88f
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). All Rights Reserved.
*
* This software is the property of WSO2 LLC. and its suppliers, if any.
* Dissemination of any information or reproduction of any material contained
* herein in any form is strictly forbidden, unless permitted by WSO2 expressly.
* You may not alter or remove any copyright or other notice from copies of this content.
*/

package com.wso2.openbanking.accelerator.identity.token.validators;

import com.wso2.openbanking.accelerator.common.exception.OpenBankingException;
import com.wso2.openbanking.accelerator.common.util.CertificateUtils;
import com.wso2.openbanking.accelerator.identity.token.util.TokenFilterException;
import com.wso2.openbanking.accelerator.identity.util.IdentityCommonConstants;
import com.wso2.openbanking.accelerator.identity.util.IdentityCommonUtil;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.security.cert.X509Certificate;

import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* MTLS Certificate Validator.
* Validates the expiry status of the certificate.
*/
public class MTLSCertificateValidator implements OBIdentityFilterValidator {

private static final Log log = LogFactory.getLog(MTLSCertificateValidator.class);

@Override
public void validate(ServletRequest request, String clientId) throws TokenFilterException, ServletException {

HttpServletRequest servletRequest = (HttpServletRequest) request;
String mtlsCertificate = servletRequest.getHeader(IdentityCommonUtil.getMTLSAuthHeader());
// MTLSEnforcementValidator validates the presence of the certificate.
if (mtlsCertificate != null) {
try {
X509Certificate x509Certificate = CertificateUtils.parseCertificate(mtlsCertificate);

if (CertificateUtils.isExpired(x509Certificate)) {
log.error("Certificate with the serial number " +
x509Certificate.getSerialNumber() + " issued by the CA " +
x509Certificate.getIssuerDN().toString() + " is expired");
throw new TokenFilterException(HttpServletResponse.SC_UNAUTHORIZED,
"Invalid mutual TLS request. Client certificate is expired",
"Certificate with the serial number " + x509Certificate.getSerialNumber() +
" issued by the CA " + x509Certificate.getIssuerDN().toString() + " is expired");
}
log.debug("Client certificate expiry validation completed successfully");
} catch (OpenBankingException e) {
log.error("Invalid mutual TLS request. Client certificate is invalid", e);
throw new TokenFilterException(HttpServletResponse.SC_UNAUTHORIZED, IdentityCommonConstants
.OAUTH2_INVALID_CLIENT_MESSAGE, e.getMessage());
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ public void testHandleCustomClaims() throws OpenBankingException, IdentityOAuth2


assertEquals("123", jwtClaimsSet.getClaim("consent_id"));
assertEquals("{x5t#S256=GA370hkNKyI1C060VmxL4xZtKyjD6aQUjrGKYWoeZX8}", jwtClaimsSet.getClaim(
assertEquals("{x5t#S256=k0p--ML7nfkE2pULKryszJRBx2ThBMaxHgJOePosits}", jwtClaimsSet.getClaim(
"cnf").toString());
assertEquals("[email protected]", jwtClaimsSet.getClaim("sub"));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
public class TestConstants {
public static final String TARGET_STREAM = "targetStream";
public static final String CERTIFICATE_HEADER = "x-wso2-mutual-auth-cert";
public static final String CERTIFICATE_CONTENT = "-----BEGIN CERTIFICATE-----" +
public static final String EXPIRED_CERTIFICATE_CONTENT = "-----BEGIN CERTIFICATE-----" +
"MIIFODCCBCCgAwIBAgIEWcWGxDANBgkqhkiG9w0BAQsFADBTMQswCQYDVQQGEwJH" +
"QjEUMBIGA1UEChMLT3BlbkJhbmtpbmcxLjAsBgNVBAMTJU9wZW5CYW5raW5nIFBy" +
"ZS1Qcm9kdWN0aW9uIElzc3VpbmcgQ0EwHhcNMTkwNTE2MDg0NDQ2WhcNMjAwNjE2" +
Expand Down Expand Up @@ -54,6 +54,37 @@ public class TestConstants {
"wtXOy27LE4exJRuZsF1CA78ObaRytuE3DJcnIRdhOcjWieS/MxZD7bzuuAPu5ySX" +
"i2/qxT3AlWtHtxrz0mKSC3rlgYAHCzCAHoASWKpf5tnB3TodPVZ6DYOu7oI=" +
"-----END CERTIFICATE-----";

public static final String CERTIFICATE_CONTENT = "-----BEGIN CERTIFICATE-----" +
"MIIFODCCBCCgAwIBAgIEWca5LzANBgkqhkiG9w0BAQsFADBTMQswCQYDVQQGEwJH" +
"QjEUMBIGA1UEChMLT3BlbkJhbmtpbmcxLjAsBgNVBAMTJU9wZW5CYW5raW5nIFBy" +
"ZS1Qcm9kdWN0aW9uIElzc3VpbmcgQ0EwHhcNMjMwNDE3MDQ1ODE2WhcNMjQwNTE3" +
"MDUyODE2WjBhMQswCQYDVQQGEwJHQjEUMBIGA1UEChMLT3BlbkJhbmtpbmcxGzAZ" +
"BgNVBAsTEjAwMTU4MDAwMDFIUVFyWkFBWDEfMB0GA1UEAxMWb1E0S29hYXZwT3Vv" +
"RTdydlFzWkVPVjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJeMaWpz" +
"mwYZ25cDHLSEWhcwaa/JJXgwupZJifByhaao8m/Hhx8PZpXOXz7GcfiNVmz3w1cA" +
"FXvfrNh4A7rc2vjp9shNQ6bQnbOKVjoN+rNxskYjxpvLOllCUaii5kjdRF5r0YE9" +
"7t3hH7GdATT56Js9aomykbeYodG1vN4eDcgArn1fO7q+6+0Ew2Mla5X+T/fsfu+1" +
"4tXMLx7AAQSCzGfsYnJp6fCJQ4uk1d5mlYWd+cM2gWf1eQ5sHeL1K9B+czos57NF" +
"hsVUBvPCPLmratanj78tN8O6zOxAs1UEckf+z1rLK3D2NCqv9FnfB7saLKhp58vQ" +
"qoRnOiW+lr1Z4bsCAwEAAaOCAgQwggIAMA4GA1UdDwEB/wQEAwIHgDAgBgNVHSUB" +
"Af8EFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwgeAGA1UdIASB2DCB1TCB0gYLKwYB" +
"BAGodYEGAWQwgcIwKgYIKwYBBQUHAgEWHmh0dHA6Ly9vYi50cnVzdGlzLmNvbS9w" +
"b2xpY2llczCBkwYIKwYBBQUHAgIwgYYMgYNVc2Ugb2YgdGhpcyBDZXJ0aWZpY2F0" +
"ZSBjb25zdGl0dXRlcyBhY2NlcHRhbmNlIG9mIHRoZSBPcGVuQmFua2luZyBSb290" +
"IENBIENlcnRpZmljYXRpb24gUG9saWNpZXMgYW5kIENlcnRpZmljYXRlIFByYWN0" +
"aWNlIFN0YXRlbWVudDBtBggrBgEFBQcBAQRhMF8wJgYIKwYBBQUHMAGGGmh0dHA6" +
"Ly9vYi50cnVzdGlzLmNvbS9vY3NwMDUGCCsGAQUFBzAChilodHRwOi8vb2IudHJ1" +
"c3Rpcy5jb20vb2JfcHBfaXNzdWluZ2NhLmNydDA6BgNVHR8EMzAxMC+gLaArhilo" +
"dHRwOi8vb2IudHJ1c3Rpcy5jb20vb2JfcHBfaXNzdWluZ2NhLmNybDAfBgNVHSME" +
"GDAWgBRQc5HGIXLTd/T+ABIGgVx5eW4/UDAdBgNVHQ4EFgQUSoZfmnXGAPddPqfH" +
"WVOvkxD89MgwDQYJKoZIhvcNAQELBQADggEBABHzHOJzn4DPHay8xGzlWJIxxe+X" +
"sNtupR5V/ouEGCzJMUPmegYeK2Kiv+Z9nJKnbspgqLil52yauKWRmiXif4FWoOPR" +
"wspR9ijnynCgIp6z3EAOawbe28HkaGEfAi8PMqdNAYLKpXg35TUnbP+p2Q55Grq9" +
"EpSR2APQfJ4TjgLgKjqLRf/RjJAY9hJbQJIUl07esYf8hH7mX6uHDCywzic+UEQ3" +
"tUfo7PgWmnmtyUdFcW1qAl4P80a5fb8Wq0gNu6gN5tK2bg5TfSo3Gp2It8NVu/dY" +
"7q3ur7CAYTXrThjg4GXUQgVqYgV3pHbr1LTAiRtac7RBhMNPCklZA78RpTM=" +
"-----END CERTIFICATE-----";
public static final String CLIENT_ASSERTION = "eyJraWQiOiJqeVJVY3l0MWtWQ2xjSXZsVWxjRHVrVlozdFUiLCJhbGciOiJQUzI1" +
"NiJ9.eyJzdWIiOiJpWXBSbTY0YjJ2bXZtS0RoZEw2S1pEOXo2ZmNhIiwiYXVkIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6OTQ0My9vYXV0a" +
"DIvdG9rZW4iLCJpc3MiOiJpWXBSbTY0YjJ2bXZtS0RoZEw2S1pEOXo2ZmNhIiwiZXhwIjoxNjEwNjMxNDEyLCJpYXQiOjE2MTA2MDE" +
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). All Rights Reserved.
*
* This software is the property of WSO2 LLC. and its suppliers, if any.
* Dissemination of any information or reproduction of any material contained
* herein in any form is strictly forbidden, unless permitted by WSO2 expressly.
* You may not alter or remove any copyright or other notice from copies of this content.
*/

package com.wso2.openbanking.accelerator.identity.token.validators;

import com.wso2.openbanking.accelerator.common.exception.OpenBankingException;
import com.wso2.openbanking.accelerator.identity.internal.IdentityExtensionsDataHolder;
import com.wso2.openbanking.accelerator.identity.token.DefaultTokenFilter;
import com.wso2.openbanking.accelerator.identity.token.TokenFilter;
import com.wso2.openbanking.accelerator.identity.token.util.TestConstants;
import com.wso2.openbanking.accelerator.identity.token.util.TestUtil;
import com.wso2.openbanking.accelerator.identity.util.IdentityCommonConstants;
import com.wso2.openbanking.accelerator.identity.util.IdentityCommonUtil;
import org.apache.http.HttpStatus;
import org.mockito.Mockito;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.testng.PowerMockTestCase;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;

import static org.testng.Assert.assertEquals;

@PrepareForTest({IdentityCommonUtil.class})
@PowerMockIgnore({"jdk.internal.reflect.*"})
public class MTLSCertificateValidatorTest extends PowerMockTestCase {

MockHttpServletResponse response;
MockHttpServletRequest request;
FilterChain filterChain;
TokenFilter filter;

@BeforeMethod
public void beforeMethod() throws ReflectiveOperationException, IOException, OpenBankingException {

request = new MockHttpServletRequest();
response = new MockHttpServletResponse();
filterChain = Mockito.spy(FilterChain.class);

List<OBIdentityFilterValidator> validators = new ArrayList<>();
MTLSCertificateValidator mtlsCertificateValidator = Mockito.spy(MTLSCertificateValidator.class);
validators.add(mtlsCertificateValidator);

filter = Mockito.spy(TokenFilter.class);
Mockito.doReturn(new DefaultTokenFilter()).when(filter).getDefaultTokenFilter();
Mockito.doReturn(validators).when(filter).getValidators();
PowerMockito.mockStatic(IdentityCommonUtil.class);
PowerMockito.when(IdentityCommonUtil.getMTLSAuthHeader()).thenReturn(TestConstants.CERTIFICATE_HEADER);
PowerMockito.when(IdentityCommonUtil.getRegulatoryFromSPMetaData("test")).thenReturn(true);
Map<String, Object> configMap = new HashMap<>();
configMap.put(IdentityCommonConstants.ENABLE_TRANSPORT_CERT_AS_HEADER, true);
configMap.put(IdentityCommonConstants.CLIENT_CERTIFICATE_ENCODE, false);
IdentityExtensionsDataHolder.getInstance().setConfigurationMap(configMap);

request.setParameter(IdentityCommonConstants.CLIENT_ID, "test");
request.setAttribute(IdentityCommonConstants.JAVAX_SERVLET_REQUEST_CERTIFICATE, null);

}

@Test(description = "Test whether the expired certificate fails")
public void testMTLSCertValidationWithExpiredCertificate() throws IOException, ServletException {

request.addHeader(TestConstants.CERTIFICATE_HEADER, TestConstants.EXPIRED_CERTIFICATE_CONTENT);

filter.doFilter(request, response, filterChain);
Map<String, String> responseMap = TestUtil.getResponse(response.getOutputStream());
assertEquals(response.getStatus(), HttpStatus.SC_UNAUTHORIZED);
assertEquals(responseMap.get(IdentityCommonConstants.OAUTH_ERROR), "invalid_client");
assertEquals(responseMap.get(IdentityCommonConstants.OAUTH_ERROR_DESCRIPTION),
"Invalid mutual TLS request. Client certificate is expired");

}

@Test(description = "Test whether the expired certificate fails")
public void testMTLSCertValidationWithValidCertificate() throws IOException, ServletException {

request.addHeader(TestConstants.CERTIFICATE_HEADER, TestConstants.CERTIFICATE_CONTENT);

filter.doFilter(request, response, filterChain);
assertEquals(response.getStatus(), HttpStatus.SC_OK);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@
<class name="com.wso2.openbanking.accelerator.identity.token.validators.ClientAuthenticatorValidatorTest"/>
</classes>
</test>
<test name="mtls-certificate-validator">
<classes>
<class name="com.wso2.openbanking.accelerator.identity.token.validators.MTLSCertificateValidatorTest"/>
</classes>
</test>
<test name="client-mutual-tls-enforcement-validator">
<classes>
<class name="com.wso2.openbanking.accelerator.identity.token.validators.MTLSEnforcementValidatorTest"/>
Expand Down

0 comments on commit 210d88f

Please sign in to comment.