Refactor SAML metadata generation to use the SAMLMetadataWriter class

This commit is contained in:
Luca Leonardo Scorcia 2020-04-22 07:38:39 -04:00 committed by Hynek Mlnařík
parent 90cf478f13
commit d6934c64fd
12 changed files with 406 additions and 247 deletions

View file

@ -17,59 +17,111 @@
package org.keycloak.saml;
import org.keycloak.dom.saml.v2.metadata.EndpointType;
import org.keycloak.dom.saml.v2.metadata.EntityDescriptorType;
import org.keycloak.dom.saml.v2.metadata.IndexedEndpointType;
import org.keycloak.dom.saml.v2.metadata.KeyDescriptorType;
import org.keycloak.dom.saml.v2.metadata.KeyTypes;
import org.keycloak.dom.saml.v2.metadata.SPSSODescriptorType;
import java.io.StringWriter;
import java.net.URI;
import java.util.Arrays;
import java.util.List;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import org.keycloak.saml.common.util.StaxUtil;
import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.processing.core.saml.v2.writers.SAMLMetadataWriter;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.XMLDSIG_NSURI;
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.PROTOCOL_NSURI;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class SPMetadataDescriptor {
public static String getSPDescriptor(String binding, String assertionEndpoint, String logoutEndpoint,
public static String getSPDescriptor(URI binding, URI assertionEndpoint, URI logoutEndpoint,
boolean wantAuthnRequestsSigned, boolean wantAssertionsSigned, boolean wantAssertionsEncrypted,
String entityId, String nameIDPolicyFormat, String signingCerts, String encryptionCerts) {
String descriptor =
"<EntityDescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" entityID=\"" + entityId + "\">\n" +
" <SPSSODescriptor AuthnRequestsSigned=\"" + wantAuthnRequestsSigned + "\" WantAssertionsSigned=\"" + wantAssertionsSigned + "\"\n" +
" protocolSupportEnumeration=\"urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.1:protocol http://schemas.xmlsoap.org/ws/2003/07/secext\">\n";
String entityId, String nameIDPolicyFormat, List<Element> signingCerts, List<Element> encryptionCerts)
throws XMLStreamException, ProcessingException, ParserConfigurationException
{
StringWriter sw = new StringWriter();
XMLStreamWriter writer = StaxUtil.getXMLStreamWriter(sw);
SAMLMetadataWriter metadataWriter = new SAMLMetadataWriter(writer);
EntityDescriptorType entityDescriptor = new EntityDescriptorType(entityId);
SPSSODescriptorType spSSODescriptor = new SPSSODescriptorType(Arrays.asList(PROTOCOL_NSURI.get()));
spSSODescriptor.setAuthnRequestsSigned(wantAuthnRequestsSigned);
spSSODescriptor.setWantAssertionsSigned(wantAssertionsSigned);
spSSODescriptor.addNameIDFormat(nameIDPolicyFormat);
spSSODescriptor.addSingleLogoutService(new EndpointType(binding, logoutEndpoint));
if (wantAuthnRequestsSigned && signingCerts != null) {
descriptor += signingCerts;
for (Element key: signingCerts)
{
KeyDescriptorType keyDescriptor = new KeyDescriptorType();
keyDescriptor.setUse(KeyTypes.SIGNING);
keyDescriptor.setKeyInfo(key);
spSSODescriptor.addKeyDescriptor(keyDescriptor);
}
}
if (wantAssertionsEncrypted && encryptionCerts != null) {
descriptor += encryptionCerts;
for (Element key: encryptionCerts)
{
KeyDescriptorType keyDescriptor = new KeyDescriptorType();
keyDescriptor.setUse(KeyTypes.ENCRYPTION);
keyDescriptor.setKeyInfo(key);
spSSODescriptor.addKeyDescriptor(keyDescriptor);
}
descriptor +=
" <SingleLogoutService Binding=\"" + binding + "\" Location=\"" + logoutEndpoint + "\"/>\n" +
" <NameIDFormat>" + nameIDPolicyFormat + "\n" +
" </NameIDFormat>\n" +
" <AssertionConsumerService\n" +
" Binding=\"" + binding + "\" Location=\"" + assertionEndpoint + "\"\n" +
" index=\"1\" isDefault=\"true\" />\n" +
" </SPSSODescriptor>\n" +
"</EntityDescriptor>\n";
return descriptor;
}
public static String xmlKeyInfo(String indentation, String keyId, String pemEncodedCertificate, String purpose, boolean declareDSigNamespace) {
if (pemEncodedCertificate == null) {
return "";
IndexedEndpointType assertionConsumerEndpoint = new IndexedEndpointType(binding, assertionEndpoint);
assertionConsumerEndpoint.setIsDefault(true);
assertionConsumerEndpoint.setIndex(1);
spSSODescriptor.addAssertionConsumerService(assertionConsumerEndpoint);
entityDescriptor.addChoiceType(new EntityDescriptorType.EDTChoiceType(Arrays.asList(new EntityDescriptorType.EDTDescriptorChoiceType(spSSODescriptor))));
metadataWriter.writeEntityDescriptor(entityDescriptor);
return sw.toString();
}
StringBuilder target = new StringBuilder()
.append(indentation).append("<KeyDescriptor use=\"").append(purpose).append("\">\n")
.append(indentation).append(" <dsig:KeyInfo").append(declareDSigNamespace ? " xmlns:dsig=\"http://www.w3.org/2000/09/xmldsig#\">\n" : ">\n");
public static Element buildKeyInfoElement(String keyName, String pemEncodedCertificate)
throws javax.xml.parsers.ParserConfigurationException
{
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.newDocument();
if (keyId != null) {
target.append(indentation).append(" <dsig:KeyName>").append(keyId).append("</dsig:KeyName>\n");
Element keyInfo = doc.createElementNS(XMLDSIG_NSURI.get(), "ds:KeyInfo");
if (keyName != null) {
Element keyNameElement = doc.createElementNS(XMLDSIG_NSURI.get(), "ds:KeyName");
keyNameElement.setTextContent(keyName);
keyInfo.appendChild(keyNameElement);
}
target
.append(indentation).append(" <dsig:X509Data>\n")
.append(indentation).append(" <dsig:X509Certificate>").append(pemEncodedCertificate).append("</dsig:X509Certificate>\n")
.append(indentation).append(" </dsig:X509Data>\n")
.append(indentation).append(" </dsig:KeyInfo>\n")
.append(indentation).append("</KeyDescriptor>\n")
;
Element x509Data = doc.createElementNS(XMLDSIG_NSURI.get(), "ds:X509Data");
return target.toString();
}
Element x509Certificate = doc.createElementNS(XMLDSIG_NSURI.get(), "ds:X509Certificate");
x509Certificate.setTextContent(pemEncodedCertificate);
x509Data.appendChild(x509Certificate);
keyInfo.appendChild(x509Data);
return keyInfo;
}
}

View file

@ -187,8 +187,7 @@ public class SAMLMetadataWriter extends BaseWriter {
public void write(SPSSODescriptorType spSSODescriptor) throws ProcessingException {
StaxUtil.writeStartElement(writer, METADATA_PREFIX, JBossSAMLConstants.SP_SSO_DESCRIPTOR.get(), JBossSAMLURIConstants.METADATA_NSURI.get());
StaxUtil.writeAttribute(writer, new QName(JBossSAMLConstants.PROTOCOL_SUPPORT_ENUMERATION.get()), spSSODescriptor
.getProtocolSupportEnumeration().get(0));
writeProtocolSupportEnumeration(spSSODescriptor.getProtocolSupportEnumeration());
// Write the attributes
Boolean authnSigned = spSSODescriptor.isAuthnRequestsSigned();
@ -250,6 +249,12 @@ public class SAMLMetadataWriter extends BaseWriter {
}
writeProtocolSupportEnumeration(idpSSODescriptor.getProtocolSupportEnumeration());
// Get the key descriptors
List<KeyDescriptorType> keyDescriptors = idpSSODescriptor.getKeyDescriptor();
for (KeyDescriptorType keyDescriptor : keyDescriptors) {
writeKeyDescriptor(keyDescriptor);
}
List<IndexedEndpointType> artifactResolutionServices = idpSSODescriptor.getArtifactResolutionService();
for (IndexedEndpointType indexedEndpoint : artifactResolutionServices) {
writeArtifactResolutionService(indexedEndpoint);
@ -260,16 +265,16 @@ public class SAMLMetadataWriter extends BaseWriter {
writeSingleLogoutService(endpoint);
}
List<EndpointType> ssoServices = idpSSODescriptor.getSingleSignOnService();
for (EndpointType endpoint : ssoServices) {
writeSingleSignOnService(endpoint);
}
List<String> nameIDFormats = idpSSODescriptor.getNameIDFormat();
for (String nameIDFormat : nameIDFormats) {
writeNameIDFormat(nameIDFormat);
}
List<EndpointType> ssoServices = idpSSODescriptor.getSingleSignOnService();
for (EndpointType endpoint : ssoServices) {
writeSingleSignOnService(endpoint);
}
List<AttributeType> attributes = idpSSODescriptor.getAttribute();
for (AttributeType attribType : attributes) {
write(attribType);
@ -550,7 +555,10 @@ public class SAMLMetadataWriter extends BaseWriter {
private void writeNameIDFormat(String nameIDFormat) throws ProcessingException {
StaxUtil.writeStartElement(writer, METADATA_PREFIX, JBossSAMLConstants.NAMEID_FORMAT.get(), JBossSAMLURIConstants.METADATA_NSURI.get());
if (nameIDFormat != null) {
StaxUtil.writeCharacters(writer, nameIDFormat);
}
StaxUtil.writeEndElement(writer);
}
}

View file

@ -45,12 +45,17 @@ import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
import org.keycloak.saml.validators.DestinationValidator;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.w3c.dom.Element;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
import java.security.KeyPair;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
@ -230,19 +235,20 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
@Override
public Response export(UriInfo uriInfo, RealmModel realm, String format) {
String authnBinding = JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get();
try
{
URI authnBinding = JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.getUri();
if (getConfig().isPostBindingAuthnRequest()) {
authnBinding = JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get();
authnBinding = JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.getUri();
}
String endpoint = uriInfo.getBaseUriBuilder()
URI endpoint = uriInfo.getBaseUriBuilder()
.path("realms").path(realm.getName())
.path("broker")
.path(getConfig().getAlias())
.path("endpoint")
.build().toString();
.build();
boolean wantAuthnRequestsSigned = getConfig().isWantAuthnRequestsSigned();
@ -251,32 +257,30 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
String entityId = getEntityId(uriInfo, realm);
String nameIDPolicyFormat = getConfig().getNameIDPolicyFormat();
StringBuilder signingKeysString = new StringBuilder();
StringBuilder encryptionKeysString = new StringBuilder();
List<Element> signingKeys = new ArrayList<Element>();
List<Element> encryptionKeys = new ArrayList<Element>();
Set<RsaKeyMetadata> keys = new TreeSet<>((o1, o2) -> o1.getStatus() == o2.getStatus() // Status can be only PASSIVE OR ACTIVE, push PASSIVE to end of list
? (int) (o2.getProviderPriority() - o1.getProviderPriority())
: (o1.getStatus() == KeyStatus.PASSIVE ? 1 : -1));
keys.addAll(session.keys().getRsaKeys(realm));
for (RsaKeyMetadata key : keys) {
addKeyInfo(signingKeysString, key, KeyTypes.SIGNING.value());
if (key == null || key.getCertificate() == null) continue;
if (key.getStatus() == KeyStatus.ACTIVE) {
addKeyInfo(encryptionKeysString, key, KeyTypes.ENCRYPTION.value());
}
signingKeys.add(SPMetadataDescriptor.buildKeyInfoElement(key.getKid(), PemUtils.encodeCertificate(key.getCertificate())));
if (key.getStatus() == KeyStatus.ACTIVE)
encryptionKeys.add(SPMetadataDescriptor.buildKeyInfoElement(key.getKid(), PemUtils.encodeCertificate(key.getCertificate())));
}
String descriptor = SPMetadataDescriptor.getSPDescriptor(authnBinding, endpoint, endpoint,
wantAuthnRequestsSigned, wantAssertionsSigned, wantAssertionsEncrypted,
entityId, nameIDPolicyFormat, signingKeysString.toString(), encryptionKeysString.toString());
entityId, nameIDPolicyFormat, signingKeys, encryptionKeys);
return Response.ok(descriptor, MediaType.APPLICATION_XML_TYPE).build();
} catch (Exception e) {
logger.warn("Failed to export SAML SP Metadata!", e);
throw new RuntimeException(e);
}
private static void addKeyInfo(StringBuilder target, RsaKeyMetadata key, String purpose) {
if (key == null) {
return;
}
target.append(SPMetadataDescriptor.xmlKeyInfo(" ", key.getKid(), PemUtils.encodeCertificate(key.getCertificate()), purpose, true));
}
public SignatureAlgorithm getSignatureAlgorithm() {

View file

@ -0,0 +1,130 @@
/*
* 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.protocol.saml;
import org.keycloak.dom.saml.v2.metadata.EndpointType;
import org.keycloak.dom.saml.v2.metadata.EntitiesDescriptorType;
import org.keycloak.dom.saml.v2.metadata.EntityDescriptorType;
import org.keycloak.dom.saml.v2.metadata.IDPSSODescriptorType;
import org.keycloak.dom.saml.v2.metadata.KeyDescriptorType;
import org.keycloak.dom.saml.v2.metadata.KeyTypes;
import java.io.StringWriter;
import java.net.URI;
import java.util.Arrays;
import java.util.List;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.processing.core.saml.v2.writers.SAMLMetadataWriter;
import org.keycloak.saml.common.util.StaxUtil;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.SAML_HTTP_POST_BINDING;
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING;
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.SAML_SOAP_BINDING;
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT;
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT;
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED;
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL;
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.XMLDSIG_NSURI;
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.PROTOCOL_NSURI;
/**
* @version $Revision: 1 $
*/
public class IDPMetadataDescriptor {
public static String getIDPDescriptor(URI loginPostEndpoint, URI loginRedirectEndpoint, URI logoutEndpoint,
String entityId, boolean wantAuthnRequestsSigned, List<Element> signingCerts, List<Element> encryptionCerts)
throws XMLStreamException, ProcessingException, ParserConfigurationException
{
StringWriter sw = new StringWriter();
XMLStreamWriter writer = StaxUtil.getXMLStreamWriter(sw);
SAMLMetadataWriter metadataWriter = new SAMLMetadataWriter(writer);
EntitiesDescriptorType entitiesDescriptor = new EntitiesDescriptorType();
entitiesDescriptor.setName("urn:keycloak");
EntityDescriptorType entityDescriptor = new EntityDescriptorType(entityId);
IDPSSODescriptorType spIDPDescriptor = new IDPSSODescriptorType(Arrays.asList(PROTOCOL_NSURI.get()));
spIDPDescriptor.setWantAuthnRequestsSigned(wantAuthnRequestsSigned);
spIDPDescriptor.addNameIDFormat(NAMEID_FORMAT_PERSISTENT.get());
spIDPDescriptor.addNameIDFormat(NAMEID_FORMAT_TRANSIENT.get());
spIDPDescriptor.addNameIDFormat(NAMEID_FORMAT_UNSPECIFIED.get());
spIDPDescriptor.addNameIDFormat(NAMEID_FORMAT_EMAIL.get());
spIDPDescriptor.addSingleLogoutService(new EndpointType(SAML_HTTP_POST_BINDING.getUri(), logoutEndpoint));
spIDPDescriptor.addSingleLogoutService(new EndpointType(SAML_HTTP_REDIRECT_BINDING.getUri(), logoutEndpoint));
spIDPDescriptor.addSingleSignOnService(new EndpointType(SAML_HTTP_POST_BINDING.getUri(), loginPostEndpoint));
spIDPDescriptor.addSingleSignOnService(new EndpointType(SAML_HTTP_REDIRECT_BINDING.getUri(), loginRedirectEndpoint));
spIDPDescriptor.addSingleSignOnService(new EndpointType(SAML_SOAP_BINDING.getUri(), loginPostEndpoint));
if (wantAuthnRequestsSigned && signingCerts != null) {
for (Element key: signingCerts)
{
KeyDescriptorType keyDescriptor = new KeyDescriptorType();
keyDescriptor.setUse(KeyTypes.SIGNING);
keyDescriptor.setKeyInfo(key);
spIDPDescriptor.addKeyDescriptor(keyDescriptor);
}
}
entityDescriptor.addChoiceType(new EntityDescriptorType.EDTChoiceType(Arrays.asList(new EntityDescriptorType.EDTDescriptorChoiceType(spIDPDescriptor))));
entitiesDescriptor.addEntityDescriptor(entityDescriptor);
metadataWriter.writeEntitiesDescriptor(entitiesDescriptor);
return sw.toString();
}
public static Element buildKeyInfoElement(String keyName, String pemEncodedCertificate)
throws javax.xml.parsers.ParserConfigurationException
{
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.newDocument();
Element keyInfo = doc.createElementNS(XMLDSIG_NSURI.get(), "ds:KeyInfo");
if (keyName != null) {
Element keyNameElement = doc.createElementNS(XMLDSIG_NSURI.get(), "ds:KeyName");
keyNameElement.setTextContent(keyName);
keyInfo.appendChild(keyNameElement);
}
Element x509Data = doc.createElementNS(XMLDSIG_NSURI.get(), "ds:X509Data");
Element x509Certificate = doc.createElementNS(XMLDSIG_NSURI.get(), "ds:X509Certificate");
x509Certificate.setTextContent(pemEncodedCertificate);
x509Data.appendChild(x509Certificate);
keyInfo.appendChild(x509Data);
return keyInfo;
}
}

View file

@ -63,6 +63,8 @@ import org.keycloak.services.resources.RealmsResource;
import org.keycloak.services.util.CacheControlUtil;
import org.keycloak.utils.MediaType;
import org.w3c.dom.Element;
import javax.ws.rs.Consumes;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
@ -77,7 +79,9 @@ import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
@ -656,38 +660,28 @@ public class SamlService extends AuthorizationEndpointBase {
}
public static String getIDPMetadataDescriptor(UriInfo uriInfo, KeycloakSession session, RealmModel realm) {
InputStream is = SamlService.class.getResourceAsStream("/idp-metadata-template.xml");
String template;
try {
template = StreamUtil.readString(is, StandardCharsets.UTF_8);
} catch (IOException ex) {
logger.error("Cannot generate IdP metadata", ex);
return "";
}
Properties props = new Properties();
props.put("idp.entityID", RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString());
props.put("idp.sso.HTTP-POST", RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL).toString());
props.put("idp.sso.HTTP-Redirect", RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL).toString());
props.put("idp.sls.HTTP-POST", RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL).toString());
StringBuilder keysString = new StringBuilder();
Set<KeyWrapper> keys = new TreeSet<>((o1, o2) -> o1.getStatus() == o2.getStatus() // Status can be only PASSIVE OR ACTIVE, push PASSIVE to end of list
? (int) (o2.getProviderPriority() - o1.getProviderPriority())
: (o1.getStatus() == KeyStatus.PASSIVE ? 1 : -1));
keys.addAll(session.keys().getKeys(realm, KeyUse.SIG, Algorithm.RS256));
try {
List<Element> signingKeys = new ArrayList<Element>();
for (KeyWrapper key : keys) {
addKeyInfo(keysString, key, KeyTypes.SIGNING.value());
}
props.put("idp.signing.certificates", keysString.toString());
return StringPropertyReplacer.replaceProperties(template, props);
signingKeys.add(IDPMetadataDescriptor.buildKeyInfoElement(key.getKid(), PemUtils.encodeCertificate(key.getCertificate())));
}
private static void addKeyInfo(StringBuilder target, KeyWrapper key, String purpose) {
if (key == null) {
return;
return IDPMetadataDescriptor.getIDPDescriptor(
RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL),
RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL),
RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL),
RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString(),
true,
signingKeys, null);
} catch (Exception ex) {
logger.error("Cannot generate IdP metadata", ex);
return "";
}
target.append(SPMetadataDescriptor.xmlKeyInfo(" ",
key.getKid(), PemUtils.encodeCertificate(key.getCertificate()), purpose, false));
}
private boolean isClientProtocolCorrect(ClientModel clientModel) {

View file

@ -17,7 +17,9 @@
package org.keycloak.protocol.saml.installation;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.dom.saml.v2.metadata.KeyTypes;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
@ -28,10 +30,13 @@ import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.saml.SPMetadataDescriptor;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.w3c.dom.Element;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.net.URI;
import org.keycloak.dom.saml.v2.metadata.KeyTypes;
import java.util.Arrays;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -39,22 +44,25 @@ import org.keycloak.dom.saml.v2.metadata.KeyTypes;
*/
public class SamlSPDescriptorClientInstallation implements ClientInstallationProvider {
protected static final Logger logger = Logger.getLogger(SamlSPDescriptorClientInstallation.class);
public static final String SAML_CLIENT_INSTALATION_SP_DESCRIPTOR = "saml-sp-descriptor";
private static final String FALLBACK_ERROR_URL_STRING = "ERROR:ENDPOINT NOT SET";
private static final String FALLBACK_ERROR_URL_STRING = "ERROR:ENDPOINT_NOT_SET";
public static String getSPDescriptorForClient(ClientModel client) {
try {
SamlClient samlClient = new SamlClient(client);
String assertionUrl;
String logoutUrl;
String binding;
URI binding;
if (samlClient.forcePostBinding()) {
assertionUrl = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE);
logoutUrl = client.getAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE);
binding = JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get();
binding = JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.getUri();
} else { //redirect binding
assertionUrl = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE);
logoutUrl = client.getAttribute(SamlProtocol.SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE);
binding = JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get();
binding = JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.getUri();
}
if (assertionUrl == null || assertionUrl.trim().isEmpty()) assertionUrl = client.getManagementUrl();
if (assertionUrl == null || assertionUrl.trim().isEmpty()) assertionUrl = FALLBACK_ERROR_URL_STRING;
@ -62,11 +70,15 @@ public class SamlSPDescriptorClientInstallation implements ClientInstallationPro
if (logoutUrl == null || logoutUrl.trim().isEmpty()) logoutUrl = FALLBACK_ERROR_URL_STRING;
String nameIdFormat = samlClient.getNameIDFormat();
if (nameIdFormat == null) nameIdFormat = SamlProtocol.SAML_DEFAULT_NAMEID_FORMAT;
String spCertificate = SPMetadataDescriptor.xmlKeyInfo(" ", null, samlClient.getClientSigningCertificate(), KeyTypes.SIGNING.value(), true);
String encCertificate = SPMetadataDescriptor.xmlKeyInfo(" ", null, samlClient.getClientEncryptingCertificate(), KeyTypes.ENCRYPTION.value(), true);
return SPMetadataDescriptor.getSPDescriptor(binding, assertionUrl, logoutUrl, samlClient.requiresClientSignature(),
Element spCertificate = SPMetadataDescriptor.buildKeyInfoElement(null, samlClient.getClientSigningCertificate());
Element encCertificate = SPMetadataDescriptor.buildKeyInfoElement(null, samlClient.getClientEncryptingCertificate());
return SPMetadataDescriptor.getSPDescriptor(binding, new URI(assertionUrl), new URI(logoutUrl), samlClient.requiresClientSignature(),
samlClient.requiresAssertionSignature(), samlClient.requiresEncryption(),
client.getClientId(), nameIdFormat, spCertificate, encCertificate);
client.getClientId(), nameIdFormat, Arrays.asList(spCertificate), Arrays.asList(encCertificate));
} catch (Exception ex) {
logger.error("Cannot generate SP metadata", ex);
return "";
}
}
@Override

View file

@ -1,45 +0,0 @@
<?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.
-->
<EntitiesDescriptor Name="urn:keycloak" xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
<EntityDescriptor entityID="${idp.entityID}">
<IDPSSODescriptor WantAuthnRequestsSigned="true"
protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
${idp.signing.certificates}
<SingleLogoutService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="${idp.sls.HTTP-POST}" />
<SingleLogoutService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location="${idp.sso.HTTP-Redirect}" />
<NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</NameIDFormat>
<NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
<NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</NameIDFormat>
<NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat>
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="${idp.sso.HTTP-POST}" />
<SingleSignOnService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location="${idp.sso.HTTP-Redirect}" />
<SingleSignOnService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
Location="${idp.sso.HTTP-POST}" />
</IDPSSODescriptor>
</EntityDescriptor>
</EntitiesDescriptor>

View file

@ -933,9 +933,7 @@ public class IdentityProviderTest extends AbstractAdminTest {
Assert.assertTrue("AuthnRequestsSigned", desc.isAuthnRequestsSigned());
Set<String> expected = new HashSet<>(Arrays.asList(
"urn:oasis:names:tc:SAML:2.0:protocol",
"urn:oasis:names:tc:SAML:1.1:protocol",
"http://schemas.xmlsoap.org/ws/2003/07/secext"));
"urn:oasis:names:tc:SAML:2.0:protocol"));
Set<String> actual = new HashSet<>(desc.getProtocolSupportEnumeration());

View file

@ -48,6 +48,8 @@ import static org.junit.Assert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.keycloak.testsuite.util.ServerURLs.getAuthServerContextRoot;
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.METADATA_NSURI;
/**
* Test getting the installation/configuration files for OIDC and SAML.
*
@ -189,10 +191,11 @@ public class InstallationTest extends AbstractClientTest {
}
@Test
public void testSamlMetadataSpDescriptor() {
public void testSamlMetadataSpDescriptor() throws Exception {
String xml = samlClient.getInstallationProvider(SamlSPDescriptorClientInstallation.SAML_CLIENT_INSTALATION_SP_DESCRIPTOR);
assertThat(xml, containsString("<EntityDescriptor"));
assertThat(xml, containsString("<SPSSODescriptor"));
Document doc = getDocumentFromXmlString(xml);
assertElements(doc, METADATA_NSURI.get(), "EntityDescriptor", null);
assertElements(doc, METADATA_NSURI.get(), "SPSSODescriptor", null);
assertThat(xml, containsString(SAML_NAME));
}
@ -215,9 +218,9 @@ public class InstallationTest extends AbstractClientTest {
Document doc = getDocumentFromXmlString(updater.getResource().getInstallationProvider(SamlSPDescriptorClientInstallation.SAML_CLIENT_INSTALATION_SP_DESCRIPTOR));
Map<String, String> attrNamesAndValues = new HashMap<>();
attrNamesAndValues.put("Binding", JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get());
attrNamesAndValues.put("Location", "ERROR:ENDPOINT NOT SET");
assertElements(doc, "SingleLogoutService", attrNamesAndValues);
assertElements(doc, "AssertionConsumerService", attrNamesAndValues);
attrNamesAndValues.put("Location", "ERROR:ENDPOINT_NOT_SET");
assertElements(doc, METADATA_NSURI.get(), "SingleLogoutService", attrNamesAndValues);
assertElements(doc, METADATA_NSURI.get(), "AssertionConsumerService", attrNamesAndValues);
attrNamesAndValues.clear();
//fallback to adminUrl
@ -226,8 +229,8 @@ public class InstallationTest extends AbstractClientTest {
doc = getDocumentFromXmlString(updater.getResource().getInstallationProvider(SamlSPDescriptorClientInstallation.SAML_CLIENT_INSTALATION_SP_DESCRIPTOR));
attrNamesAndValues.put("Binding", JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get());
attrNamesAndValues.put("Location", "admin-url");
assertElements(doc, "SingleLogoutService", attrNamesAndValues);
assertElements(doc, "AssertionConsumerService", attrNamesAndValues);
assertElements(doc, METADATA_NSURI.get(), "SingleLogoutService", attrNamesAndValues);
assertElements(doc, METADATA_NSURI.get(), "AssertionConsumerService", attrNamesAndValues);
attrNamesAndValues.clear();
//fine grained
@ -241,11 +244,11 @@ public class InstallationTest extends AbstractClientTest {
doc = getDocumentFromXmlString(updater.getResource().getInstallationProvider(SamlSPDescriptorClientInstallation.SAML_CLIENT_INSTALATION_SP_DESCRIPTOR));
attrNamesAndValues.put("Binding", JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get());
attrNamesAndValues.put("Location", "saml-logout-post-url");
assertElements(doc, "SingleLogoutService", attrNamesAndValues);
assertElements(doc, METADATA_NSURI.get(), "SingleLogoutService", attrNamesAndValues);
attrNamesAndValues.clear();
attrNamesAndValues.put("Binding", JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get());
attrNamesAndValues.put("Location", "saml-assertion-post-url");
assertElements(doc, "AssertionConsumerService", attrNamesAndValues);
assertElements(doc, METADATA_NSURI.get(), "AssertionConsumerService", attrNamesAndValues);
}
assertAdminEvents.assertEvent(getRealmId(), OperationType.UPDATE, AdminEventPaths.clientResourcePath(samlClientId), ResourceType.CLIENT);
}
@ -263,9 +266,9 @@ public class InstallationTest extends AbstractClientTest {
Document doc = getDocumentFromXmlString(updater.getResource().getInstallationProvider(SamlSPDescriptorClientInstallation.SAML_CLIENT_INSTALATION_SP_DESCRIPTOR));
Map<String, String> attrNamesAndValues = new HashMap<>();
attrNamesAndValues.put("Binding", JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get());
attrNamesAndValues.put("Location", "ERROR:ENDPOINT NOT SET");
assertElements(doc, "SingleLogoutService", attrNamesAndValues);
assertElements(doc, "AssertionConsumerService", attrNamesAndValues);
attrNamesAndValues.put("Location", "ERROR:ENDPOINT_NOT_SET");
assertElements(doc, METADATA_NSURI.get(), "SingleLogoutService", attrNamesAndValues);
assertElements(doc, METADATA_NSURI.get(), "AssertionConsumerService", attrNamesAndValues);
attrNamesAndValues.clear();
//fallback to adminUrl
@ -274,8 +277,8 @@ public class InstallationTest extends AbstractClientTest {
doc = getDocumentFromXmlString(updater.getResource().getInstallationProvider(SamlSPDescriptorClientInstallation.SAML_CLIENT_INSTALATION_SP_DESCRIPTOR));
attrNamesAndValues.put("Binding", JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get());
attrNamesAndValues.put("Location", "admin-url");
assertElements(doc, "SingleLogoutService", attrNamesAndValues);
assertElements(doc, "AssertionConsumerService", attrNamesAndValues);
assertElements(doc, METADATA_NSURI.get(), "SingleLogoutService", attrNamesAndValues);
assertElements(doc, METADATA_NSURI.get(), "AssertionConsumerService", attrNamesAndValues);
attrNamesAndValues.clear();
//fine grained
@ -288,29 +291,33 @@ public class InstallationTest extends AbstractClientTest {
doc = getDocumentFromXmlString(updater.getResource().getInstallationProvider(SamlSPDescriptorClientInstallation.SAML_CLIENT_INSTALATION_SP_DESCRIPTOR));
attrNamesAndValues.put("Binding", JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get());
attrNamesAndValues.put("Location", "saml-logout-redirect-url");
assertElements(doc, "SingleLogoutService", attrNamesAndValues);
assertElements(doc, METADATA_NSURI.get(), "SingleLogoutService", attrNamesAndValues);
attrNamesAndValues.clear();
attrNamesAndValues.put("Binding", JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get());
attrNamesAndValues.put("Location", "saml-assertion-redirect-url");
assertElements(doc, "AssertionConsumerService", attrNamesAndValues);
assertElements(doc, METADATA_NSURI.get(), "AssertionConsumerService", attrNamesAndValues);
}
assertAdminEvents.assertEvent(getRealmId(), OperationType.UPDATE, AdminEventPaths.clientResourcePath(samlClientId), ResourceType.CLIENT);
}
private Document getDocumentFromXmlString(String xml) throws SAXException, ParserConfigurationException, IOException {
DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
DocumentBuilder db = dbf.newDocumentBuilder();
InputSource is = new InputSource();
is.setCharacterStream(new StringReader(xml));
return db.parse(is);
}
private void assertElements(Document doc, String tagName, Map<String, String> attrNamesAndValues) {
NodeList elementsByTagName = doc.getElementsByTagName(tagName);
private void assertElements(Document doc, String tagNamespace, String tagName, Map<String, String> attrNamesAndValues) {
NodeList elementsByTagName = doc.getElementsByTagNameNS(tagNamespace, tagName);
assertThat("Expected exactly one " + tagName + " element!", elementsByTagName.getLength(), is(equalTo(1)));
Node element = elementsByTagName.item(0);
if (attrNamesAndValues != null) {
for (String attrName : attrNamesAndValues.keySet()) {
assertThat(element.getAttributes().getNamedItem(attrName).getNodeValue(), containsString(attrNamesAndValues.get(attrName)));
}
}
}
}

View file

@ -5,16 +5,7 @@
>
<IDPSSODescriptor WantAuthnRequestsSigned="true"
protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</NameIDFormat>
<NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
<NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</NameIDFormat>
<NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat>
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="http://localhost:8080/auth/realms/master/protocol/saml" />
<SingleLogoutService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="http://localhost:8080/auth/realms/master/protocol/saml" />
<KeyDescriptor use="signing">
<dsig:KeyInfo>
<dsig:KeyName>hAoy_sBtpu6FdRVCk7ykihF6Ug-o0pKPK3LN9RYkeqs</dsig:KeyName>
@ -35,5 +26,14 @@
</dsig:X509Data>
</dsig:KeyInfo>
</KeyDescriptor>
<SingleLogoutService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="http://localhost:8080/auth/realms/master/protocol/saml" />
<NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</NameIDFormat>
<NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
<NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</NameIDFormat>
<NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat>
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="http://localhost:8080/auth/realms/master/protocol/saml" />
</IDPSSODescriptor>
</EntityDescriptor>

View file

@ -5,16 +5,6 @@
>
<IDPSSODescriptor WantAuthnRequestsSigned="true"
protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</NameIDFormat>
<NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
<NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</NameIDFormat>
<NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat>
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="http://localhost:8080/auth/realms/master/protocol/saml" />
<SingleLogoutService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="http://localhost:8080/auth/realms/master/protocol/saml" />
<KeyDescriptor use="signing">
<dsig:KeyInfo xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
<dsig:X509Data>
@ -24,5 +14,14 @@
</dsig:X509Data>
</dsig:KeyInfo>
</KeyDescriptor>
<SingleLogoutService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="http://localhost:8080/auth/realms/master/protocol/saml" />
<NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</NameIDFormat>
<NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
<NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</NameIDFormat>
<NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat>
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="http://localhost:8080/auth/realms/master/protocol/saml" />
</IDPSSODescriptor>
</EntityDescriptor>