KEYCLOAK-16456 X509 Auth: add option for OCSP fail-open behavior
This commit is contained in:
parent
b0e5c38775
commit
43a3c676f7
5 changed files with 172 additions and 18 deletions
|
@ -59,6 +59,7 @@ public abstract class AbstractX509ClientCertificateAuthenticator implements Auth
|
|||
public static final String REGULAR_EXPRESSION = "x509-cert-auth.regular-expression";
|
||||
public static final String ENABLE_CRL = "x509-cert-auth.crl-checking-enabled";
|
||||
public static final String ENABLE_OCSP = "x509-cert-auth.ocsp-checking-enabled";
|
||||
public static final String OCSP_FAIL_OPEN = "x509-cert-auth.ocsp-fail-open";
|
||||
public static final String ENABLE_CRLDP = "x509-cert-auth.crldp-checking-enabled";
|
||||
public static final String CANONICAL_DN = "x509-cert-auth.canonical-dn-enabled";
|
||||
public static final String TIMESTAMP_VALIDATION = "x509-cert-auth.timestamp-validation-enabled";
|
||||
|
@ -116,6 +117,7 @@ public abstract class AbstractX509ClientCertificateAuthenticator implements Auth
|
|||
.cRLDPEnabled(config.getCRLDistributionPointEnabled())
|
||||
.cRLrelativePath(config.getCRLRelativePath())
|
||||
.oCSPEnabled(config.getOCSPEnabled())
|
||||
.oCSPFailOpen(config.getOCSPFailOpen())
|
||||
.oCSPResponseCertificate(config.getOCSPResponderCertificate())
|
||||
.oCSPResponderURI(config.getOCSPResponder())
|
||||
.trustValidation()
|
||||
|
|
|
@ -182,6 +182,13 @@ public abstract class AbstractX509ClientCertificateAuthenticatorFactory implemen
|
|||
oCspCheckingEnabled.setHelpText("Enable Certificate Revocation Checking using OCSP");
|
||||
oCspCheckingEnabled.setLabel("OCSP Checking Enabled");
|
||||
|
||||
ProviderConfigProperty ocspFailOpen = new ProviderConfigProperty();
|
||||
ocspFailOpen.setType(BOOLEAN_TYPE);
|
||||
ocspFailOpen.setName(OCSP_FAIL_OPEN);
|
||||
ocspFailOpen.setDefaultValue(Boolean.toString(false));
|
||||
ocspFailOpen.setHelpText("Whether to allow or deny authentication for client certificates that have missing/invalid/inconclusive OCSP endpoints. By default a successful OCSP response is required.");
|
||||
ocspFailOpen.setLabel("OCSP Fail-Open Behavior");
|
||||
|
||||
ProviderConfigProperty ocspResponderUri = new ProviderConfigProperty();
|
||||
ocspResponderUri.setType(STRING_TYPE);
|
||||
ocspResponderUri.setName(OCSPRESPONDER_URI);
|
||||
|
@ -245,6 +252,7 @@ public abstract class AbstractX509ClientCertificateAuthenticatorFactory implemen
|
|||
crlDPEnabled,
|
||||
cRLRelativePath,
|
||||
oCspCheckingEnabled,
|
||||
ocspFailOpen,
|
||||
ocspResponderUri,
|
||||
ocspResponderCert,
|
||||
keyUsage,
|
||||
|
|
|
@ -403,6 +403,7 @@ public class CertificateValidator {
|
|||
boolean _crldpEnabled;
|
||||
CRLLoaderImpl _crlLoader;
|
||||
boolean _ocspEnabled;
|
||||
boolean _ocspFailOpen;
|
||||
OCSPChecker ocspChecker;
|
||||
boolean _timestampValidationEnabled;
|
||||
boolean _trustValidationEnabled;
|
||||
|
@ -417,6 +418,7 @@ public class CertificateValidator {
|
|||
boolean cRLDPCheckingEnabled,
|
||||
CRLLoaderImpl crlLoader,
|
||||
boolean oCSPCheckingEnabled,
|
||||
boolean ocspFailOpen,
|
||||
OCSPChecker ocspChecker,
|
||||
KeycloakSession session,
|
||||
boolean timestampValidationEnabled,
|
||||
|
@ -430,6 +432,7 @@ public class CertificateValidator {
|
|||
_crldpEnabled = cRLDPCheckingEnabled;
|
||||
_crlLoader = crlLoader;
|
||||
_ocspEnabled = oCSPCheckingEnabled;
|
||||
_ocspFailOpen = ocspFailOpen;
|
||||
this.ocspChecker = ocspChecker;
|
||||
this.session = session;
|
||||
_timestampValidationEnabled = timestampValidationEnabled;
|
||||
|
@ -705,25 +708,38 @@ public class CertificateValidator {
|
|||
}
|
||||
}
|
||||
|
||||
OCSPUtils.OCSPRevocationStatus rs = ocspChecker.check(cert, issuer);
|
||||
try {
|
||||
OCSPUtils.OCSPRevocationStatus rs = ocspChecker.check(cert, issuer);
|
||||
|
||||
if (rs == null) {
|
||||
throw new GeneralSecurityException("Unable to check client revocation status using OCSP");
|
||||
}
|
||||
if (rs == null) {
|
||||
if (_ocspFailOpen)
|
||||
logger.warnf("Unable to check client revocation status using OCSP - continuing certificate authentication because of fail-open OCSP configuration setting");
|
||||
else
|
||||
throw new GeneralSecurityException("Unable to check client revocation status using OCSP");
|
||||
}
|
||||
|
||||
if (rs.getRevocationStatus() == OCSPUtils.RevocationStatus.UNKNOWN) {
|
||||
throw new GeneralSecurityException("Unable to determine certificate's revocation status.");
|
||||
}
|
||||
else if (rs.getRevocationStatus() == OCSPUtils.RevocationStatus.REVOKED) {
|
||||
if (rs.getRevocationStatus() == OCSPUtils.RevocationStatus.UNKNOWN) {
|
||||
if (_ocspFailOpen)
|
||||
logger.warnf("Unable to determine certificate's revocation status - continuing certificate authentication because of fail-open OCSP configuration setting");
|
||||
else
|
||||
throw new GeneralSecurityException("Unable to determine certificate's revocation status.");
|
||||
}
|
||||
else if (rs.getRevocationStatus() == OCSPUtils.RevocationStatus.REVOKED) {
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Certificate's been revoked.");
|
||||
sb.append("\n");
|
||||
sb.append(rs.getRevocationReason().toString());
|
||||
sb.append("\n");
|
||||
sb.append(String.format("Revoked on: %s",rs.getRevocationTime().toString()));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Certificate's been revoked.");
|
||||
sb.append("\n");
|
||||
sb.append(rs.getRevocationReason().toString());
|
||||
sb.append("\n");
|
||||
sb.append(String.format("Revoked on: %s",rs.getRevocationTime().toString()));
|
||||
|
||||
throw new GeneralSecurityException(sb.toString());
|
||||
throw new GeneralSecurityException(sb.toString());
|
||||
}
|
||||
} catch (CertPathValidatorException e) {
|
||||
if (_ocspFailOpen)
|
||||
logger.warnf("Unable to check client revocation status using OCSP - continuing certificate authentication because of fail-open OCSP configuration setting");
|
||||
else
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -792,6 +808,7 @@ public class CertificateValidator {
|
|||
boolean _crldpEnabled;
|
||||
CRLLoaderImpl _crlLoader;
|
||||
boolean _ocspEnabled;
|
||||
boolean _ocspFailOpen;
|
||||
String _responderUri;
|
||||
X509Certificate _responderCert;
|
||||
boolean _timestampValidationEnabled;
|
||||
|
@ -967,7 +984,14 @@ public class CertificateValidator {
|
|||
}
|
||||
|
||||
public class GotOCSP {
|
||||
public GotOCSP oCSPResponseCertificate(String responderCert) {
|
||||
public GotOCSPFailOpen oCSPFailOpen(boolean ocspFailOpen) {
|
||||
_ocspFailOpen = ocspFailOpen;
|
||||
return new GotOCSPFailOpen();
|
||||
}
|
||||
}
|
||||
|
||||
public class GotOCSPFailOpen {
|
||||
public GotOCSPFailOpen oCSPResponseCertificate(String responderCert) {
|
||||
if (responderCert != null && !responderCert.isEmpty()) {
|
||||
try {
|
||||
_responderCert = XMLSignatureUtil.getX509CertificateFromKeyInfoString(responderCert);
|
||||
|
@ -979,7 +1003,7 @@ public class CertificateValidator {
|
|||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
return new GotOCSP();
|
||||
return new GotOCSPFailOpen();
|
||||
}
|
||||
|
||||
public CertificateValidatorBuilder oCSPResponderURI(String responderURI) {
|
||||
|
@ -1050,7 +1074,7 @@ public class CertificateValidator {
|
|||
}
|
||||
return new CertificateValidator(certs, _keyUsageBits, _extendedKeyUsage,
|
||||
_certificatePolicy, _certificatePolicyMode,
|
||||
_crlCheckingEnabled, _crldpEnabled, _crlLoader, _ocspEnabled,
|
||||
_crlCheckingEnabled, _crldpEnabled, _crlLoader, _ocspEnabled, _ocspFailOpen,
|
||||
new BouncyCastleOCSPChecker(session, _responderUri, _responderCert), session, _timestampValidationEnabled, _trustValidationEnabled);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -130,6 +130,15 @@ public class X509AuthenticatorConfigModel extends AuthenticatorConfigModel {
|
|||
return this;
|
||||
}
|
||||
|
||||
public boolean getOCSPFailOpen() {
|
||||
return Boolean.parseBoolean(getConfig().getOrDefault(OCSP_FAIL_OPEN, Boolean.toString(false)));
|
||||
}
|
||||
|
||||
public X509AuthenticatorConfigModel setOCSPFailOpen(boolean value) {
|
||||
getConfig().put(OCSP_FAIL_OPEN, Boolean.toString(value));
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean getCRLDistributionPointEnabled() {
|
||||
return Boolean.parseBoolean(getConfig().get(ENABLE_CRLDP));
|
||||
}
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
package org.keycloak.testsuite.x509;
|
||||
|
||||
import com.google.common.base.Charsets;
|
||||
|
||||
import io.undertow.Undertow;
|
||||
import io.undertow.server.handlers.BlockingHandler;
|
||||
|
||||
import java.nio.file.Paths;
|
||||
import java.util.function.Supplier;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.jboss.arquillian.drone.api.annotation.Drone;
|
||||
import org.junit.After;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.testsuite.util.PhantomJSBrowser;
|
||||
import org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel;
|
||||
import org.keycloak.representations.idm.AuthenticatorConfigRepresentation;
|
||||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.IdentityMapperType.USERNAME_EMAIL;
|
||||
import static org.keycloak.authentication.authenticators.x509.X509AuthenticatorConfigModel.MappingSourceType.SUBJECTDN_EMAIL;
|
||||
|
||||
public class X509OCSPResponderFailOpenTest extends AbstractX509AuthenticationTest {
|
||||
|
||||
private static final String OCSP_RESPONDER_HOST = "localhost";
|
||||
|
||||
private static final int OCSP_RESPONDER_PORT = 8888;
|
||||
|
||||
private Undertow ocspResponder;
|
||||
|
||||
@Drone
|
||||
@PhantomJSBrowser
|
||||
private WebDriver phantomJS;
|
||||
|
||||
@Before
|
||||
public void replaceTheDefaultDriver() {
|
||||
replaceDefaultWebDriver(phantomJS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ocspFailCloseLoginFailed() throws Exception {
|
||||
// Test of OCSP failure (invalid OCSP responder host) when OCSP Fail-Open is set to OFF
|
||||
// If test is successful, it should return an auth error
|
||||
|
||||
X509AuthenticatorConfigModel config = new X509AuthenticatorConfigModel()
|
||||
.setOCSPEnabled(true)
|
||||
.setOCSPResponder("http://" + OCSP_RESPONDER_HOST + ".invalid.host:" + OCSP_RESPONDER_PORT + "/oscp")
|
||||
.setOCSPFailOpen(false)
|
||||
.setMappingSourceType(SUBJECTDN_EMAIL)
|
||||
.setUserIdentityMapperType(USERNAME_EMAIL);
|
||||
AuthenticatorConfigRepresentation cfg = newConfig("x509-directgrant-config", config.getConfig());
|
||||
String cfgId = createConfig(directGrantExecution.getId(), cfg);
|
||||
Assert.assertNotNull(cfgId);
|
||||
|
||||
oauth.clientId("resource-owner");
|
||||
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "", "", null);
|
||||
|
||||
assertEquals(Response.Status.UNAUTHORIZED.getStatusCode(), response.getStatusCode());
|
||||
assertEquals("invalid_request", response.getError());
|
||||
|
||||
// Make sure we got the right error
|
||||
Assert.assertThat(response.getErrorDescription(), containsString("OCSP check failed"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ocspFailOpenLoginSuccess() throws Exception {
|
||||
// Test of OCSP failure (invalid OCSP responder host) when OCSP Fail-Open is set to ON
|
||||
// If test is successful, it should continue the login
|
||||
|
||||
X509AuthenticatorConfigModel config =
|
||||
new X509AuthenticatorConfigModel()
|
||||
.setOCSPEnabled(true)
|
||||
.setOCSPFailOpen(true)
|
||||
.setMappingSourceType(SUBJECTDN_EMAIL)
|
||||
.setOCSPResponder("http://" + OCSP_RESPONDER_HOST + ".invalid.host:" + OCSP_RESPONDER_PORT + "/oscp")
|
||||
.setOCSPResponderCertificate(
|
||||
IOUtils.toString(this.getClass().getResourceAsStream(OcspHandler.OCSP_RESPONDER_CERT_PATH), Charsets.UTF_8)
|
||||
.replace("-----BEGIN CERTIFICATE-----", "")
|
||||
.replace("-----END CERTIFICATE-----", ""))
|
||||
.setUserIdentityMapperType(USERNAME_EMAIL);
|
||||
AuthenticatorConfigRepresentation cfg = newConfig("x509-directgrant-config", config.getConfig());
|
||||
String cfgId = createConfig(directGrantExecution.getId(), cfg);
|
||||
Assert.assertNotNull(cfgId);
|
||||
|
||||
String keyStorePath = Paths.get(System.getProperty("client.certificate.keystore"))
|
||||
.getParent().resolve("client-ca.jks").toString();
|
||||
String keyStorePassword = System.getProperty("client.certificate.keystore.passphrase");
|
||||
String trustStorePath = System.getProperty("client.truststore");
|
||||
String trustStorePassword = System.getProperty("client.truststore.passphrase");
|
||||
Supplier<CloseableHttpClient> previous = oauth.getHttpClient();
|
||||
try {
|
||||
oauth.clientId("resource-owner");
|
||||
oauth.httpClient(() -> OAuthClient.newCloseableHttpClientSSL(keyStorePath, keyStorePassword, trustStorePath, trustStorePassword));
|
||||
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "", "", null);
|
||||
|
||||
// Make sure authentication is allowed
|
||||
assertEquals(Response.Status.OK.getStatusCode(), response.getStatusCode());
|
||||
} finally {
|
||||
oauth.httpClient(previous);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue