KEYCLOAK-1881 Update client adapter configuration

Client adapter configuration was updated to support for customization
of HttpClient used for key retrieval similarly to OIDC. Further, it is
now possible to specify several static public keys for signature
verification in saml-client.xml.
This commit is contained in:
Hynek Mlnarik 2016-11-03 14:55:12 +01:00
parent 8ae1b1740d
commit 570d71c07b
14 changed files with 708 additions and 182 deletions

View file

@ -23,9 +23,10 @@ import org.keycloak.saml.SignatureAlgorithm;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.apache.http.client.HttpClient;
import org.keycloak.adapters.HttpClientBuilder;
import org.keycloak.adapters.saml.rotation.SamlDescriptorPublicKeyLocator;
import org.keycloak.rotation.CompositeKeyLocator;
import org.keycloak.rotation.HardcodedKeyLocator;
@ -191,8 +192,9 @@ public class DefaultSamlDeployment implements SamlDeployment {
private final CompositeKeyLocator signatureValidationKeyLocator = new CompositeKeyLocator();
private SingleSignOnService singleSignOnService;
private SingleLogoutService singleLogoutService;
private HardcodedKeyLocator hardcodedKeyLocator;
private final List<PublicKey> signatureValidationKeys = new LinkedList<>();
private int minTimeBetweenDescriptorRequests;
private HttpClient client;
@Override
public String getEntityID() {
@ -227,14 +229,12 @@ public class DefaultSamlDeployment implements SamlDeployment {
this.entityID = entityID;
}
public void setSignatureValidationKey(PublicKey signatureValidationKey) {
this.hardcodedKeyLocator = signatureValidationKey == null ? null : new HardcodedKeyLocator(signatureValidationKey);
refreshKeyLocatorConfiguration();
public void addSignatureValidationKey(PublicKey signatureValidationKey) {
this.signatureValidationKeys.add(signatureValidationKey);
}
public void setSingleSignOnService(SingleSignOnService singleSignOnService) {
this.singleSignOnService = singleSignOnService;
refreshKeyLocatorConfiguration();
}
public void setSingleLogoutService(SingleLogoutService singleLogoutService) {
@ -245,18 +245,26 @@ public class DefaultSamlDeployment implements SamlDeployment {
this.signatureValidationKeyLocator.clear();
// When key is set, use that (and only that), otherwise configure dynamic key locator
if (this.hardcodedKeyLocator != null) {
this.signatureValidationKeyLocator.add(this.hardcodedKeyLocator);
if (! this.signatureValidationKeys.isEmpty()) {
this.signatureValidationKeyLocator.add(new HardcodedKeyLocator(this.signatureValidationKeys));
} else if (this.singleSignOnService != null) {
String samlDescriptorUrl = singleSignOnService.getRequestBindingUrl() + "/descriptor";
// TODO
HttpClient httpClient = new HttpClientBuilder().build();
HttpClient httpClient = getClient();
SamlDescriptorPublicKeyLocator samlDescriptorPublicKeyLocator =
new SamlDescriptorPublicKeyLocator(
samlDescriptorUrl, this.minTimeBetweenDescriptorRequests, DEFAULT_CACHE_TTL, httpClient);
this.signatureValidationKeyLocator.add(samlDescriptorPublicKeyLocator);
}
}
@Override
public HttpClient getClient() {
return this.client;
}
public void setClient(HttpClient client) {
this.client = client;
}
}
private IDP idp;

View file

@ -23,6 +23,7 @@ import org.keycloak.saml.SignatureAlgorithm;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.util.Set;
import org.apache.http.client.HttpClient;
import org.keycloak.rotation.KeyLocator;
/**
@ -77,6 +78,12 @@ public interface SamlDeployment {
*/
int getMinTimeBetweenDescriptorRequests();
/**
* Returns {@link HttpClient} instance that will be used for http communication with this IdP.
* @return see description
*/
HttpClient getClient();
public interface SingleSignOnService {
/**
* Returns {@code true} if the requests to IdP need to be signed by SP key.

View file

@ -19,6 +19,7 @@ package org.keycloak.adapters.saml.config;
import java.io.Serializable;
import java.util.List;
import org.keycloak.adapters.cloned.AdapterHttpClientConfig;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -157,12 +158,97 @@ public class IDP implements Serializable {
}
}
public static class HttpClientConfig implements AdapterHttpClientConfig {
private String truststore;
private String truststorePassword;
private String clientKeystore;
private String clientKeystorePassword;
private boolean allowAnyHostname;
private boolean disableTrustManager;
private int connectionPoolSize;
private String proxyUrl;
@Override
public String getTruststore() {
return truststore;
}
public void setTruststore(String truststore) {
this.truststore = truststore;
}
@Override
public String getTruststorePassword() {
return truststorePassword;
}
public void setTruststorePassword(String truststorePassword) {
this.truststorePassword = truststorePassword;
}
@Override
public String getClientKeystore() {
return clientKeystore;
}
public void setClientKeystore(String clientKeystore) {
this.clientKeystore = clientKeystore;
}
@Override
public String getClientKeystorePassword() {
return clientKeystorePassword;
}
public void setClientKeystorePassword(String clientKeystorePassword) {
this.clientKeystorePassword = clientKeystorePassword;
}
@Override
public boolean isAllowAnyHostname() {
return allowAnyHostname;
}
public void setAllowAnyHostname(boolean allowAnyHostname) {
this.allowAnyHostname = allowAnyHostname;
}
@Override
public boolean isDisableTrustManager() {
return disableTrustManager;
}
public void setDisableTrustManager(boolean disableTrustManager) {
this.disableTrustManager = disableTrustManager;
}
@Override
public int getConnectionPoolSize() {
return connectionPoolSize;
}
public void setConnectionPoolSize(int connectionPoolSize) {
this.connectionPoolSize = connectionPoolSize;
}
@Override
public String getProxyUrl() {
return proxyUrl;
}
public void setProxyUrl(String proxyUrl) {
this.proxyUrl = proxyUrl;
}
}
private String entityID;
private String signatureAlgorithm;
private String signatureCanonicalizationMethod;
private SingleSignOnService singleSignOnService;
private SingleLogoutService singleLogoutService;
private List<Key> keys;
private AdapterHttpClientConfig httpClientConfig = new HttpClientConfig();
public String getEntityID() {
return entityID;
@ -212,4 +298,12 @@ public class IDP implements Serializable {
this.signatureCanonicalizationMethod = signatureCanonicalizationMethod;
}
public AdapterHttpClientConfig getHttpClientConfig() {
return httpClientConfig;
}
public void setHttpClientConfig(AdapterHttpClientConfig httpClientConfig) {
this.httpClientConfig = httpClientConfig;
}
}

View file

@ -72,4 +72,15 @@ public class ConfigXmlConstants {
public static final String VALIDATE_REQUEST_SIGNATURE_ATTR = "validateRequestSignature";
public static final String POST_BINDING_URL_ATTR = "postBindingUrl";
public static final String REDIRECT_BINDING_URL_ATTR = "redirectBindingUrl";
public static final String HTTP_CLIENT_ELEMENT = "HttpClient";
public static final String ALLOW_ANY_HOSTNAME_ATTR = "allowAnyHostname";
public static final String CLIENT_KEYSTORE_ATTR = "clientKeystore";
public static final String CLIENT_KEYSTORE_PASSWORD_ATTR = "clientKeystorePassword";
public static final String CONNECTION_POOL_SIZE_ATTR = "connectionPoolSize";
public static final String DISABLE_TRUST_MANAGER_ATTR = "disableTrustManager";
public static final String PROXY_URL_ATTR = "proxyUrl";
public static final String TRUSTSTORE_ATTR = "truststore";
public static final String TRUSTSTORE_PASSWORD_ATTR = "truststorePassword";
}

View file

@ -40,6 +40,7 @@ import java.security.PublicKey;
import java.security.cert.Certificate;
import java.util.HashSet;
import java.util.Set;
import org.keycloak.adapters.cloned.HttpClientBuilder;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -178,36 +179,39 @@ public class DeploymentBuilder {
if (sp.getIdp().getKeys() != null) {
for (Key key : sp.getIdp().getKeys()) {
if (key.isSigning()) {
if (key.getKeystore() != null) {
KeyStore keyStore = loadKeystore(resourceLoader, key);
Certificate cert = null;
try {
cert = keyStore.getCertificate(key.getKeystore().getCertificateAlias());
} catch (KeyStoreException e) {
throw new RuntimeException(e);
}
idp.setSignatureValidationKey(cert.getPublicKey());
} else {
if (key.getPublicKeyPem() == null && key.getCertificatePem() == null) {
throw new RuntimeException("IDP signing key must have a PublicKey or Certificate defined");
}
try {
PublicKey publicKey = getPublicKeyFromPem(key);
idp.setSignatureValidationKey(publicKey);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
processSigningKey(idp, key, resourceLoader);
}
}
}
idp.setClient(new HttpClientBuilder().build(sp.getIdp().getHttpClientConfig()));
idp.refreshKeyLocatorConfiguration();
return deployment;
}
protected static PublicKey getPublicKeyFromPem(Key key) throws Exception {
private void processSigningKey(DefaultSamlDeployment.DefaultIDP idp, Key key, ResourceLoader resourceLoader) throws RuntimeException {
PublicKey publicKey;
if (key.getKeystore() != null) {
KeyStore keyStore = loadKeystore(resourceLoader, key);
Certificate cert = null;
try {
cert = keyStore.getCertificate(key.getKeystore().getCertificateAlias());
} catch (KeyStoreException e) {
throw new RuntimeException(e);
}
publicKey = cert.getPublicKey();
} else {
if (key.getPublicKeyPem() == null && key.getCertificatePem() == null) {
throw new RuntimeException("IDP signing key must have a PublicKey or Certificate defined");
}
publicKey = getPublicKeyFromPem(key);
}
idp.addSignatureValidationKey(publicKey);
}
protected static PublicKey getPublicKeyFromPem(Key key) {
PublicKey publicKey;
if (key.getPublicKeyPem() != null) {
publicKey = PemUtils.decodePublicKey(key.getPublicKeyPem().trim());

View file

@ -29,6 +29,10 @@ import javax.xml.stream.events.EndElement;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;
import java.util.List;
import org.keycloak.adapters.saml.config.IDP.HttpClientConfig;
import static org.keycloak.adapters.saml.config.parsers.SPXmlParser.getAttributeValue;
import static org.keycloak.adapters.saml.config.parsers.SPXmlParser.getBooleanAttributeValue;
import static org.keycloak.adapters.saml.config.parsers.SPXmlParser.getIntegerAttributeValue;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -41,16 +45,16 @@ public class IDPXmlParser extends AbstractParser {
StartElement startElement = StaxParserUtil.getNextStartElement(xmlEventReader);
StaxParserUtil.validate(startElement, ConfigXmlConstants.IDP_ELEMENT);
IDP idp = new IDP();
String entityID = SPXmlParser.getAttributeValue(startElement, ConfigXmlConstants.ENTITY_ID_ATTR);
String entityID = getAttributeValue(startElement, ConfigXmlConstants.ENTITY_ID_ATTR);
if (entityID == null) {
throw new ParsingException("entityID must be set on IDP");
}
idp.setEntityID(entityID);
boolean signaturesRequired = SPXmlParser.getBooleanAttributeValue(startElement, ConfigXmlConstants.SIGNATURES_REQUIRED_ATTR);
idp.setSignatureCanonicalizationMethod(SPXmlParser.getAttributeValue(startElement, ConfigXmlConstants.SIGNATURE_CANONICALIZATION_METHOD_ATTR));
idp.setSignatureAlgorithm(SPXmlParser.getAttributeValue(startElement, ConfigXmlConstants.SIGNATURE_ALGORITHM_ATTR));
boolean signaturesRequired = getBooleanAttributeValue(startElement, ConfigXmlConstants.SIGNATURES_REQUIRED_ATTR);
idp.setSignatureCanonicalizationMethod(getAttributeValue(startElement, ConfigXmlConstants.SIGNATURE_CANONICALIZATION_METHOD_ATTR));
idp.setSignatureAlgorithm(getAttributeValue(startElement, ConfigXmlConstants.SIGNATURE_ALGORITHM_ATTR));
while (xmlEventReader.hasNext()) {
XMLEvent xmlEvent = StaxParserUtil.peek(xmlEventReader);
if (xmlEvent == null)
@ -75,6 +79,10 @@ public class IDPXmlParser extends AbstractParser {
IDP.SingleLogoutService slo = parseSingleLogoutService(xmlEventReader, signaturesRequired);
idp.setSingleLogoutService(slo);
} else if (tag.equals(ConfigXmlConstants.HTTP_CLIENT_ELEMENT)) {
HttpClientConfig config = parseHttpClientElement(xmlEventReader);
idp.setHttpClientConfig(config);
} else if (tag.equals(ConfigXmlConstants.KEYS_ELEMENT)) {
KeysXmlParser parser = new KeysXmlParser();
List<Key> keys = (List<Key>)parser.parse(xmlEventReader);
@ -90,29 +98,63 @@ public class IDPXmlParser extends AbstractParser {
protected IDP.SingleLogoutService parseSingleLogoutService(XMLEventReader xmlEventReader, boolean signaturesRequired) throws ParsingException {
IDP.SingleLogoutService slo = new IDP.SingleLogoutService();
StartElement element = StaxParserUtil.getNextStartElement(xmlEventReader);
slo.setSignRequest(SPXmlParser.getBooleanAttributeValue(element, ConfigXmlConstants.SIGN_REQUEST_ATTR, signaturesRequired));
slo.setValidateResponseSignature(SPXmlParser.getBooleanAttributeValue(element, ConfigXmlConstants.VALIDATE_RESPONSE_SIGNATURE_ATTR, signaturesRequired));
slo.setValidateRequestSignature(SPXmlParser.getBooleanAttributeValue(element, ConfigXmlConstants.VALIDATE_REQUEST_SIGNATURE_ATTR, signaturesRequired));
slo.setRequestBinding(SPXmlParser.getAttributeValue(element, ConfigXmlConstants.REQUEST_BINDING_ATTR));
slo.setResponseBinding(SPXmlParser.getAttributeValue(element, ConfigXmlConstants.RESPONSE_BINDING_ATTR));
slo.setSignResponse(SPXmlParser.getBooleanAttributeValue(element, ConfigXmlConstants.SIGN_RESPONSE_ATTR, signaturesRequired));
slo.setPostBindingUrl(SPXmlParser.getAttributeValue(element, ConfigXmlConstants.POST_BINDING_URL_ATTR));
slo.setRedirectBindingUrl(SPXmlParser.getAttributeValue(element, ConfigXmlConstants.REDIRECT_BINDING_URL_ATTR));
slo.setSignRequest(getBooleanAttributeValue(element, ConfigXmlConstants.SIGN_REQUEST_ATTR, signaturesRequired));
slo.setValidateResponseSignature(getBooleanAttributeValue(element, ConfigXmlConstants.VALIDATE_RESPONSE_SIGNATURE_ATTR, signaturesRequired));
slo.setValidateRequestSignature(getBooleanAttributeValue(element, ConfigXmlConstants.VALIDATE_REQUEST_SIGNATURE_ATTR, signaturesRequired));
slo.setRequestBinding(getAttributeValue(element, ConfigXmlConstants.REQUEST_BINDING_ATTR));
slo.setResponseBinding(getAttributeValue(element, ConfigXmlConstants.RESPONSE_BINDING_ATTR));
slo.setSignResponse(getBooleanAttributeValue(element, ConfigXmlConstants.SIGN_RESPONSE_ATTR, signaturesRequired));
slo.setPostBindingUrl(getAttributeValue(element, ConfigXmlConstants.POST_BINDING_URL_ATTR));
slo.setRedirectBindingUrl(getAttributeValue(element, ConfigXmlConstants.REDIRECT_BINDING_URL_ATTR));
return slo;
}
protected IDP.SingleSignOnService parseSingleSignOnService(XMLEventReader xmlEventReader, boolean signaturesRequired) throws ParsingException {
IDP.SingleSignOnService sso = new IDP.SingleSignOnService();
StartElement element = StaxParserUtil.getNextStartElement(xmlEventReader);
sso.setSignRequest(SPXmlParser.getBooleanAttributeValue(element, ConfigXmlConstants.SIGN_REQUEST_ATTR, signaturesRequired));
sso.setValidateResponseSignature(SPXmlParser.getBooleanAttributeValue(element, ConfigXmlConstants.VALIDATE_RESPONSE_SIGNATURE_ATTR, signaturesRequired));
sso.setValidateAssertionSignature(SPXmlParser.getBooleanAttributeValue(element, ConfigXmlConstants.VALIDATE_ASSERTION_SIGNATURE_ATTR));
sso.setRequestBinding(SPXmlParser.getAttributeValue(element, ConfigXmlConstants.REQUEST_BINDING_ATTR));
sso.setResponseBinding(SPXmlParser.getAttributeValue(element, ConfigXmlConstants.RESPONSE_BINDING_ATTR));
sso.setBindingUrl(SPXmlParser.getAttributeValue(element, ConfigXmlConstants.BINDING_URL_ATTR));
sso.setSignRequest(getBooleanAttributeValue(element, ConfigXmlConstants.SIGN_REQUEST_ATTR, signaturesRequired));
sso.setValidateResponseSignature(getBooleanAttributeValue(element, ConfigXmlConstants.VALIDATE_RESPONSE_SIGNATURE_ATTR, signaturesRequired));
sso.setValidateAssertionSignature(getBooleanAttributeValue(element, ConfigXmlConstants.VALIDATE_ASSERTION_SIGNATURE_ATTR));
sso.setRequestBinding(getAttributeValue(element, ConfigXmlConstants.REQUEST_BINDING_ATTR));
sso.setResponseBinding(getAttributeValue(element, ConfigXmlConstants.RESPONSE_BINDING_ATTR));
sso.setBindingUrl(getAttributeValue(element, ConfigXmlConstants.BINDING_URL_ATTR));
return sso;
}
private HttpClientConfig parseHttpClientElement(XMLEventReader xmlEventReader) throws ParsingException {
StartElement startElement = StaxParserUtil.getNextStartElement(xmlEventReader);
StaxParserUtil.validate(startElement, ConfigXmlConstants.HTTP_CLIENT_ELEMENT);
HttpClientConfig config = new HttpClientConfig();
config.setAllowAnyHostname(getBooleanAttributeValue(startElement, ConfigXmlConstants.ALLOW_ANY_HOSTNAME_ATTR, false));
config.setClientKeystore(getAttributeValue(startElement, ConfigXmlConstants.CLIENT_KEYSTORE_ATTR));
config.setClientKeystorePassword(getAttributeValue(startElement, ConfigXmlConstants.CLIENT_KEYSTORE_PASSWORD_ATTR));
config.setConnectionPoolSize(getIntegerAttributeValue(startElement, ConfigXmlConstants.CONNECTION_POOL_SIZE_ATTR, 0));
config.setDisableTrustManager(getBooleanAttributeValue(startElement, ConfigXmlConstants.ALLOW_ANY_HOSTNAME_ATTR, false));
config.setProxyUrl(getAttributeValue(startElement, ConfigXmlConstants.PROXY_URL_ATTR));
config.setTruststore(getAttributeValue(startElement, ConfigXmlConstants.TRUSTSTORE_ATTR));
config.setTruststorePassword(getAttributeValue(startElement, ConfigXmlConstants.TRUSTSTORE_PASSWORD_ATTR));
while (xmlEventReader.hasNext()) {
XMLEvent xmlEvent = StaxParserUtil.peek(xmlEventReader);
if (xmlEvent == null)
break;
if (xmlEvent instanceof EndElement) {
EndElement endElement = (EndElement) StaxParserUtil.getNextEvent(xmlEventReader);
String endElementName = StaxParserUtil.getEndElementName(endElement);
if (endElementName.equals(ConfigXmlConstants.ROLE_IDENTIFIERS_ELEMENT))
break;
else
continue;
}
String tag = StaxParserUtil.getStartElementName(startElement);
StaxParserUtil.bypassElementBlock(xmlEventReader, tag);
}
return config;
}
@Override
public boolean supports(QName qname) {
return false;

View file

@ -48,6 +48,13 @@ public class SPXmlParser extends AbstractParser {
return str;
}
public static int getIntegerAttributeValue(StartElement startElement, String tag, int defaultValue) {
String result = getAttributeValue(startElement, tag);
if (result == null)
return defaultValue;
return Integer.valueOf(result);
}
public static boolean getBooleanAttributeValue(StartElement startElement, String tag, boolean defaultValue) {
String result = getAttributeValue(startElement, tag);
if (result == null)

View file

@ -0,0 +1,144 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
~ and other contributors as indicated by the @author tags.
~
~ 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.
-->
<xs:schema version="1.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns="urn:keycloak:saml:adapter"
targetNamespace="urn:keycloak:saml:adapter"
elementFormDefault="qualified"
attributeFormDefault="unqualified">
<xs:element name="keycloak-saml-adapter" type="adapter-type"/>
<xs:complexType name="adapter-type">
<xs:annotation>
<xs:documentation>
<![CDATA[
The Keycloak SAML Adapter keycloak-saml.xml config file
]]>
</xs:documentation>
</xs:annotation>
<xs:all>
<xs:element name="SP" maxOccurs="1" minOccurs="0" type="sp-type"/>
</xs:all>
</xs:complexType>
<xs:complexType name="sp-type">
<xs:all>
<xs:element name="Keys" type="keys-type" minOccurs="0" maxOccurs="1"/>
<xs:element name="PrincipalNameMapping" type="principal-name-mapping-type" minOccurs="0" maxOccurs="1"/>
<xs:element name="RoleIdentifiers" type="role-identifiers-type" minOccurs="0" maxOccurs="1"/>
<xs:element name="IDP" type="idp-type" minOccurs="1" maxOccurs="1"/>
</xs:all>
<xs:attribute name="entityID" type="xs:string" use="required"/>
<xs:attribute name="sslPolicy" type="xs:string" use="optional"/>
<xs:attribute name="nameIDPolicyFormat" type="xs:string" use="optional"/>
<xs:attribute name="logoutPage" type="xs:string" use="optional"/>
<xs:attribute name="forceAuthentication" type="xs:boolean" use="optional"/>
<xs:attribute name="isPassive" type="xs:boolean" use="optional"/>
<xs:attribute name="turnOffChangeSessionIdOnLogin" type="xs:boolean" use="optional"/>
</xs:complexType>
<xs:complexType name="keys-type">
<xs:sequence>
<xs:element name="Key" type="key-type" minOccurs="1" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="key-type">
<xs:all>
<xs:element name="KeyStore" maxOccurs="1" minOccurs="0" type="key-store-type"/>
<xs:element name="PrivateKeyPem" type="xs:string" minOccurs="0" maxOccurs="1"/>
<xs:element name="PublicKeyPem" type="xs:string" minOccurs="0" maxOccurs="1"/>
<xs:element name="CertificatePem" type="xs:string" minOccurs="0" maxOccurs="1"/>
</xs:all>
<xs:attribute name="signing" type="xs:boolean" use="optional"/>
<xs:attribute name="encryption" type="xs:boolean" use="optional"/>
</xs:complexType>
<xs:complexType name="key-store-type">
<xs:all>
<xs:element name="PrivateKey" maxOccurs="1" minOccurs="0" type="private-key-type"/>
<xs:element name="Certificate" type="certificate-type" minOccurs="0" maxOccurs="1"/>
</xs:all>
<xs:attribute name="file" type="xs:string" use="optional"/>
<xs:attribute name="resource" type="xs:string" use="optional"/>
<xs:attribute name="password" type="xs:string" use="required"/>
</xs:complexType>
<xs:complexType name="private-key-type">
<xs:attribute name="alias" type="xs:string" use="required"/>
<xs:attribute name="password" type="xs:string" use="required"/>
</xs:complexType>
<xs:complexType name="certificate-type">
<xs:attribute name="alias" type="xs:string" use="required"/>
</xs:complexType>
<xs:complexType name="principal-name-mapping-type">
<xs:attribute name="policy" type="xs:string" use="required"/>
<xs:attribute name="attribute" type="xs:string" use="optional"/>
</xs:complexType>
<xs:complexType name="role-identifiers-type">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="Attribute" maxOccurs="unbounded" minOccurs="0" type="attribute-type"/>
</xs:choice>
</xs:complexType>
<xs:complexType name="attribute-type">
<xs:attribute name="name" type="xs:string" use="required"/>
</xs:complexType>
<xs:complexType name="idp-type">
<xs:sequence minOccurs="0" maxOccurs="unbounded">
<xs:element name="SingleSignOnService" maxOccurs="1" minOccurs="1" type="sign-on-type"/>
<xs:element name="SingleLogoutService" type="logout-type" minOccurs="0" maxOccurs="1"/>
<xs:element name="Keys" type="keys-type" minOccurs="0" maxOccurs="1"/>
<xs:element name="HttpClient" type="http-client-type" minOccurs="0" maxOccurs="1"/>
</xs:sequence>
<xs:attribute name="entityID" type="xs:string" use="required"/>
<xs:attribute name="signaturesRequired" type="xs:boolean" use="required"/>
<xs:attribute name="signatureAlgorithm" type="xs:string" use="optional"/>
<xs:attribute name="signatureCanonicalizationMethod" type="xs:string" use="optional"/>
<xs:attribute name="encryption" type="xs:boolean" use="optional"/>
</xs:complexType>
<xs:complexType name="sign-on-type">
<xs:attribute name="signRequest" type="xs:boolean" use="optional"/>
<xs:attribute name="validateResponseSignature" type="xs:boolean" use="optional"/>
<xs:attribute name="validateAssertionSignature" type="xs:boolean" use="optional"/>
<xs:attribute name="requestBinding" type="xs:string" use="optional"/>
<xs:attribute name="responseBinding" type="xs:string" use="optional"/>
<xs:attribute name="bindingUrl" type="xs:string" use="optional"/>
</xs:complexType>
<xs:complexType name="logout-type">
<xs:attribute name="signRequest" type="xs:boolean" use="optional"/>
<xs:attribute name="signResponse" type="xs:boolean" use="optional"/>
<xs:attribute name="validateRequestSignature" type="xs:boolean" use="optional"/>
<xs:attribute name="validateResponseSignature" type="xs:boolean" use="optional"/>
<xs:attribute name="requestBinding" type="xs:string" use="optional"/>
<xs:attribute name="responseBinding" type="xs:string" use="optional"/>
<xs:attribute name="postBindingUrl" type="xs:string" use="optional"/>
<xs:attribute name="redirectBindingUrl" type="xs:string" use="optional"/>
</xs:complexType>
<xs:complexType name="http-client-type">
<xs:attribute name="allowAnyHostname" type="xs:boolean" use="optional"/>
<xs:attribute name="clientKeystore" type="xs:string" use="optional"/>
<xs:attribute name="clientKeystorePassword" type="xs:string" use="optional"/>
<xs:attribute name="connectionPoolSize" type="xs:int" use="optional"/>
<xs:attribute name="disableTrustManager" type="xs:boolean" use="optional"/>
<xs:attribute name="proxyUrl" type="xs:string" use="optional"/>
<xs:attribute name="truststore" type="xs:string" use="optional"/>
<xs:attribute name="truststorePassword" type="xs:string" use="optional"/>
</xs:complexType>
</xs:schema>

View file

@ -0,0 +1,180 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.adapters.saml.config.parsers;
import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;
import org.junit.Test;
import org.keycloak.adapters.saml.config.IDP;
import org.keycloak.adapters.saml.config.Key;
import org.keycloak.adapters.saml.config.KeycloakSamlAdapter;
import org.keycloak.adapters.saml.config.SP;
import org.keycloak.saml.common.util.StaxParserUtil;
import java.io.InputStream;
import org.junit.Rule;
import org.junit.rules.ExpectedException;
import org.keycloak.saml.common.exceptions.ParsingException;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class KeycloakSamlAdapterXMLParserTest {
private static final String CURRENT_XSD_LOCATION = "/schema/keycloak_saml_adapter_1_7.xsd";
@Rule
public ExpectedException expectedException = ExpectedException.none();
private void testValidationValid(String fileName) throws Exception {
InputStream schema = getClass().getResourceAsStream(CURRENT_XSD_LOCATION);
InputStream is = getClass().getResourceAsStream(fileName);
assertNotNull(is);
assertNotNull(schema);
StaxParserUtil.validate(is, schema);
}
@Test
public void testValidationSimpleFile() throws Exception {
testValidationValid("keycloak-saml.xml");
}
@Test
public void testValidationMultipleKeys() throws Exception {
testValidationValid("keycloak-saml-multiple-signing-keys.xml");
}
@Test
public void testValidationWithHttpClient() throws Exception {
testValidationValid("keycloak-saml-wth-http-client-settings.xml");
}
@Test
public void testValidationKeyInvalid() throws Exception {
InputStream schemaIs = KeycloakSamlAdapterXMLParser.class.getResourceAsStream(CURRENT_XSD_LOCATION);
InputStream is = getClass().getResourceAsStream("keycloak-saml-invalid.xml");
assertNotNull(is);
assertNotNull(schemaIs);
expectedException.expect(ParsingException.class);
StaxParserUtil.validate(is, schemaIs);
}
@Test
public void testXmlParser() throws Exception {
InputStream is = getClass().getResourceAsStream("keycloak-saml.xml");
assertNotNull(is);
KeycloakSamlAdapterXMLParser parser = new KeycloakSamlAdapterXMLParser();
KeycloakSamlAdapter config = (KeycloakSamlAdapter)parser.parse(is);
assertNotNull(config);
assertEquals(1, config.getSps().size());
SP sp = config.getSps().get(0);
assertEquals("sp", sp.getEntityID());
assertEquals("ssl", sp.getSslPolicy());
assertEquals("format", sp.getNameIDPolicyFormat());
assertTrue(sp.isForceAuthentication());
assertTrue(sp.isIsPassive());
assertEquals(2, sp.getKeys().size());
Key signing = sp.getKeys().get(0);
assertTrue(signing.isSigning());
Key.KeyStoreConfig keystore = signing.getKeystore();
assertNotNull(keystore);
assertEquals("file", keystore.getFile());
assertEquals("cp", keystore.getResource());
assertEquals("pw", keystore.getPassword());
assertEquals("private alias", keystore.getPrivateKeyAlias());
assertEquals("private pw", keystore.getPrivateKeyPassword());
assertEquals("cert alias", keystore.getCertificateAlias());
Key encryption = sp.getKeys().get(1);
assertTrue(encryption.isEncryption());
assertEquals("private pem", encryption.getPrivateKeyPem());
assertEquals("public pem", encryption.getPublicKeyPem());
assertEquals("policy", sp.getPrincipalNameMapping().getPolicy());
assertEquals("attribute", sp.getPrincipalNameMapping().getAttributeName());
assertTrue(sp.getRoleAttributes().size() == 1);
assertTrue(sp.getRoleAttributes().contains("member"));
IDP idp = sp.getIdp();
assertEquals("idp", idp.getEntityID());
assertEquals("RSA", idp.getSignatureAlgorithm());
assertEquals("canon", idp.getSignatureCanonicalizationMethod());
assertTrue(idp.getSingleSignOnService().isSignRequest());
assertTrue(idp.getSingleSignOnService().isValidateResponseSignature());
assertEquals("post", idp.getSingleSignOnService().getRequestBinding());
assertEquals("url", idp.getSingleSignOnService().getBindingUrl());
assertTrue(idp.getSingleLogoutService().isSignRequest());
assertTrue(idp.getSingleLogoutService().isSignResponse());
assertTrue(idp.getSingleLogoutService().isValidateRequestSignature());
assertTrue(idp.getSingleLogoutService().isValidateResponseSignature());
assertEquals("redirect", idp.getSingleLogoutService().getRequestBinding());
assertEquals("post", idp.getSingleLogoutService().getResponseBinding());
assertEquals("posturl", idp.getSingleLogoutService().getPostBindingUrl());
assertEquals("redirecturl", idp.getSingleLogoutService().getRedirectBindingUrl());
assertTrue(idp.getKeys().size() == 1);
assertTrue(idp.getKeys().get(0).isSigning());
assertEquals("cert pem", idp.getKeys().get(0).getCertificatePem());
}
@Test
public void testXmlParserMultipleSigningKeys() throws Exception {
InputStream is = getClass().getResourceAsStream("keycloak-saml-multiple-signing-keys.xml");
assertNotNull(is);
KeycloakSamlAdapterXMLParser parser = new KeycloakSamlAdapterXMLParser();
KeycloakSamlAdapter config = (KeycloakSamlAdapter) parser.parse(is);
assertNotNull(config);
assertEquals(1, config.getSps().size());
SP sp = config.getSps().get(0);
IDP idp = sp.getIdp();
assertTrue(idp.getKeys().size() == 4);
for (int i = 0; i < 4; i ++) {
Key key = idp.getKeys().get(i);
assertTrue(key.isSigning());
assertEquals("cert pem " + i, idp.getKeys().get(i).getCertificatePem());
}
}
@Test
public void testXmlParserHttpClientSettings() throws Exception {
InputStream is = getClass().getResourceAsStream("keycloak-saml-wth-http-client-settings.xml");
assertNotNull(is);
KeycloakSamlAdapterXMLParser parser = new KeycloakSamlAdapterXMLParser();
KeycloakSamlAdapter config = (KeycloakSamlAdapter) parser.parse(is);
assertNotNull(config);
assertEquals(1, config.getSps().size());
SP sp = config.getSps().get(0);
IDP idp = sp.getIdp();
assertThat(idp.getHttpClientConfig(), notNullValue());
assertThat(idp.getHttpClientConfig().getClientKeystore(), is("ks"));
assertThat(idp.getHttpClientConfig().getClientKeystorePassword(), is("ks-pwd"));
assertThat(idp.getHttpClientConfig().getProxyUrl(), is("pu"));
assertThat(idp.getHttpClientConfig().getTruststore(), is("ts"));
assertThat(idp.getHttpClientConfig().getTruststorePassword(), is("tsp"));
assertThat(idp.getHttpClientConfig().getConnectionPoolSize(), is(42));
assertThat(idp.getHttpClientConfig().isAllowAnyHostname(), is(true));
assertThat(idp.getHttpClientConfig().isDisableTrustManager(), is(true));
}
}

View file

@ -1,133 +0,0 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.test.adapters.saml;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.adapters.saml.config.IDP;
import org.keycloak.adapters.saml.config.Key;
import org.keycloak.adapters.saml.config.KeycloakSamlAdapter;
import org.keycloak.adapters.saml.config.SP;
import org.keycloak.adapters.saml.config.parsers.KeycloakSamlAdapterXMLParser;
import org.keycloak.saml.common.util.StaxParserUtil;
import javax.xml.XMLConstants;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import java.io.InputStream;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class XmlParserTest {
@Test
public void testValidation() throws Exception {
{
InputStream schema = KeycloakSamlAdapterXMLParser.class.getResourceAsStream("/schema/keycloak_saml_adapter_1_6.xsd");
InputStream is = getClass().getResourceAsStream("/keycloak-saml.xml");
Assert.assertNotNull(is);
Assert.assertNotNull(schema);
StaxParserUtil.validate(is, schema);
}
{
InputStream sch = KeycloakSamlAdapterXMLParser.class.getResourceAsStream("/schema/keycloak_saml_adapter_1_6.xsd");
InputStream doc = getClass().getResourceAsStream("/keycloak-saml2.xml");
Assert.assertNotNull(doc);
Assert.assertNotNull(sch);
try {
SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
Schema schema = factory.newSchema(new StreamSource(sch));
Validator validator = schema.newValidator();
StreamSource source = new StreamSource(doc);
source.setSystemId("/keycloak-saml2.xml");
validator.validate(source);
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Test
public void testXmlParser() throws Exception {
InputStream is = getClass().getResourceAsStream("/keycloak-saml.xml");
Assert.assertNotNull(is);
KeycloakSamlAdapterXMLParser parser = new KeycloakSamlAdapterXMLParser();
KeycloakSamlAdapter config = (KeycloakSamlAdapter)parser.parse(is);
Assert.assertNotNull(config);
Assert.assertEquals(1, config.getSps().size());
SP sp = config.getSps().get(0);
Assert.assertEquals("sp", sp.getEntityID());
Assert.assertEquals("ssl", sp.getSslPolicy());
Assert.assertEquals("format", sp.getNameIDPolicyFormat());
Assert.assertTrue(sp.isForceAuthentication());
Assert.assertTrue(sp.isIsPassive());
Assert.assertEquals(2, sp.getKeys().size());
Key signing = sp.getKeys().get(0);
Assert.assertTrue(signing.isSigning());
Key.KeyStoreConfig keystore = signing.getKeystore();
Assert.assertNotNull(keystore);
Assert.assertEquals("file", keystore.getFile());
Assert.assertEquals("cp", keystore.getResource());
Assert.assertEquals("pw", keystore.getPassword());
Assert.assertEquals("private alias", keystore.getPrivateKeyAlias());
Assert.assertEquals("private pw", keystore.getPrivateKeyPassword());
Assert.assertEquals("cert alias", keystore.getCertificateAlias());
Key encryption = sp.getKeys().get(1);
Assert.assertTrue(encryption.isEncryption());
Assert.assertEquals("private pem", encryption.getPrivateKeyPem());
Assert.assertEquals("public pem", encryption.getPublicKeyPem());
Assert.assertEquals("policy", sp.getPrincipalNameMapping().getPolicy());
Assert.assertEquals("attribute", sp.getPrincipalNameMapping().getAttributeName());
Assert.assertTrue(sp.getRoleAttributes().size() == 1);
Assert.assertTrue(sp.getRoleAttributes().contains("member"));
IDP idp = sp.getIdp();
Assert.assertEquals("idp", idp.getEntityID());
Assert.assertEquals("RSA", idp.getSignatureAlgorithm());
Assert.assertEquals("canon", idp.getSignatureCanonicalizationMethod());
Assert.assertTrue(idp.getSingleSignOnService().isSignRequest());
Assert.assertTrue(idp.getSingleSignOnService().isValidateResponseSignature());
Assert.assertEquals("post", idp.getSingleSignOnService().getRequestBinding());
Assert.assertEquals("url", idp.getSingleSignOnService().getBindingUrl());
Assert.assertTrue(idp.getSingleLogoutService().isSignRequest());
Assert.assertTrue(idp.getSingleLogoutService().isSignResponse());
Assert.assertTrue(idp.getSingleLogoutService().isValidateRequestSignature());
Assert.assertTrue(idp.getSingleLogoutService().isValidateResponseSignature());
Assert.assertEquals("redirect", idp.getSingleLogoutService().getRequestBinding());
Assert.assertEquals("post", idp.getSingleLogoutService().getResponseBinding());
Assert.assertEquals("posturl", idp.getSingleLogoutService().getPostBindingUrl());
Assert.assertEquals("redirecturl", idp.getSingleLogoutService().getRedirectBindingUrl());
Assert.assertTrue(idp.getKeys().size() == 1);
Assert.assertTrue(idp.getKeys().get(0).isSigning());
Assert.assertEquals("cert pem", idp.getKeys().get(0).getCertificatePem());
}
}

View file

@ -0,0 +1,81 @@
<!--
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
~ and other contributors as indicated by the @author tags.
~
~ 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.
-->
<keycloak-saml-adapter xmlns="urn:keycloak:saml:adapter">
<SP entityID="sp"
sslPolicy="ssl"
nameIDPolicyFormat="format"
forceAuthentication="true"
isPassive="true">
<Keys>
<Key signing="true" >
<KeyStore file="file" resource="cp" password="pw">
<PrivateKey alias="private alias" password="private pw"/>
<Certificate alias="cert alias"/>
</KeyStore>
</Key>
<Key encryption="true">
<PrivateKeyPem>
private pem
</PrivateKeyPem>
<PublicKeyPem>
public pem
</PublicKeyPem>
</Key>
</Keys>
<PrincipalNameMapping policy="policy" attribute="attribute"/>
<RoleIdentifiers>
<Attribute name="member"/>
</RoleIdentifiers>
<IDP entityID="idp"
signatureAlgorithm="RSA"
signatureCanonicalizationMethod="canon"
signaturesRequired="true"
>
<SingleSignOnService signRequest="true"
validateResponseSignature="true"
requestBinding="post"
bindingUrl="url"
/>
<SingleLogoutService
validateRequestSignature="true"
validateResponseSignature="true"
signRequest="true"
signResponse="true"
requestBinding="redirect"
responseBinding="post"
postBindingUrl="posturl"
redirectBindingUrl="redirecturl"
/>
<Keys>
<Key signing="true">
<CertificatePem>cert pem 0</CertificatePem>
</Key>
<Key signing="true">
<CertificatePem>cert pem 1</CertificatePem>
</Key>
<Key signing="true">
<CertificatePem>cert pem 2</CertificatePem>
</Key>
<Key signing="true">
<CertificatePem>cert pem 3</CertificatePem>
</Key>
</Keys>
</IDP>
</SP>
</keycloak-saml-adapter>

View file

@ -0,0 +1,81 @@
<!--
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
~ and other contributors as indicated by the @author tags.
~
~ 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.
-->
<keycloak-saml-adapter xmlns="urn:keycloak:saml:adapter">
<SP entityID="sp"
sslPolicy="ssl"
nameIDPolicyFormat="format"
forceAuthentication="true"
isPassive="true">
<Keys>
<Key signing="true" >
<KeyStore file="file" resource="cp" password="pw">
<PrivateKey alias="private alias" password="private pw"/>
<Certificate alias="cert alias"/>
</KeyStore>
</Key>
<Key encryption="true">
<PrivateKeyPem>
private pem
</PrivateKeyPem>
<PublicKeyPem>
public pem
</PublicKeyPem>
</Key>
</Keys>
<PrincipalNameMapping policy="policy" attribute="attribute"/>
<RoleIdentifiers>
<Attribute name="member"/>
</RoleIdentifiers>
<IDP entityID="idp"
signatureAlgorithm="RSA"
signatureCanonicalizationMethod="canon"
signaturesRequired="true"
>
<SingleSignOnService signRequest="true"
validateResponseSignature="true"
requestBinding="post"
bindingUrl="url"
/>
<SingleLogoutService
validateRequestSignature="true"
validateResponseSignature="true"
signRequest="true"
signResponse="true"
requestBinding="redirect"
responseBinding="post"
postBindingUrl="posturl"
redirectBindingUrl="redirecturl"
/>
<Keys>
<Key signing="true">
<CertificatePem>
cert pem
</CertificatePem>
</Key>
</Keys>
<HttpClient allowAnyHostname="true"
clientKeystore="ks" clientKeystorePassword="ks-pwd"
connectionPoolSize="42"
disableTrustManager="true"
proxyUrl="pu"
truststore="ts" truststorePassword="tsp"
/>
</IDP>
</SP>
</keycloak-saml-adapter>