diff --git a/common/src/main/java/org/keycloak/common/util/StringPropertyReplacer.java b/common/src/main/java/org/keycloak/common/util/StringPropertyReplacer.java
index 3e4839a..283eb3e 100755
--- a/common/src/main/java/org/keycloak/common/util/StringPropertyReplacer.java
+++ b/common/src/main/java/org/keycloak/common/util/StringPropertyReplacer.java
@@ -98,7 +98,7 @@ public final class StringPropertyReplacer
public static String replaceProperties(final String string, final Properties props)
{
final char[] chars = string.toCharArray();
- StringBuffer buffer = new StringBuffer();
+ StringBuilder buffer = new StringBuilder();
boolean properties = false;
int state = NORMAL;
int start = 0;
diff --git a/saml-core/src/main/java/org/keycloak/saml/SPMetadataDescriptor.java b/saml-core/src/main/java/org/keycloak/saml/SPMetadataDescriptor.java
index e6c10af..cc94b8a 100755
--- a/saml-core/src/main/java/org/keycloak/saml/SPMetadataDescriptor.java
+++ b/saml-core/src/main/java/org/keycloak/saml/SPMetadataDescriptor.java
@@ -17,26 +17,21 @@
package org.keycloak.saml;
+import org.keycloak.common.util.PemUtils;
+
/**
* @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, boolean wantAuthnRequestsSigned, String entityId, String nameIDPolicyFormat, String certificatePem) {
String descriptor =
"<EntityDescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" entityID=\"" + entityId + "\">\n" +
" <SPSSODescriptor AuthnRequestsSigned=\"" + wantAuthnRequestsSigned + "\"\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";
if (wantAuthnRequestsSigned) {
- descriptor +=
- " <KeyDescriptor use=\"signing\">\n" +
- " <dsig:KeyInfo xmlns:dsig=\"http://www.w3.org/2000/09/xmldsig#\">\n" +
- " <dsig:X509Data>\n" +
- " <dsig:X509Certificate>\n" + certificatePem + "\n" +
- " </dsig:X509Certificate>\n" +
- " </dsig:X509Data>\n" +
- " </dsig:KeyInfo>\n" +
- " </KeyDescriptor>\n";
+ descriptor += xmlKeyInfo(null, certificatePem, "signing", true);
}
descriptor +=
" <SingleLogoutService Binding=\"" + binding + "\" Location=\"" + logoutEndpoint + "\"/>\n" +
@@ -44,10 +39,34 @@ public class SPMetadataDescriptor {
" </NameIDFormat>\n" +
" <AssertionConsumerService\n" +
" Binding=\"" + binding + "\" Location=\"" + assertionEndpoint + "\"\n" +
- " index=\"1\" isDefault=\"true\" />\n";
- descriptor +=
+ " 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 "";
+ }
+
+ 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");
+
+ if (keyId != null) {
+ target.append(indentation).append(" <dsig:KeyName>").append(keyId).append("</dsig:KeyName>\n");
+ }
+
+ 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")
+ ;
+
+ return target.toString();
+ }
+
}
diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlService.java b/services/src/main/java/org/keycloak/protocol/saml/SamlService.java
index e02093c..40f615e 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/SamlService.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/SamlService.java
@@ -74,6 +74,17 @@ import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.security.PublicKey;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.Set;
+import java.util.TreeSet;
+import org.keycloak.common.util.StringPropertyReplacer;
+import org.keycloak.dom.saml.v2.metadata.KeyTypes;
+import org.keycloak.keys.KeyMetadata;
+import org.keycloak.rotation.HardcodedKeyLocator;
+import org.keycloak.rotation.KeyLocator;
+import org.keycloak.saml.SPMetadataDescriptor;
+import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
/**
* Resource class for the oauth/openid connect token service
@@ -541,12 +552,30 @@ public class SamlService extends AuthorizationEndpointBase {
public static String getIDPMetadataDescriptor(UriInfo uriInfo, KeycloakSession session, RealmModel realm) throws IOException {
InputStream is = SamlService.class.getResourceAsStream("/idp-metadata-template.xml");
String template = StreamUtil.readString(is);
- template = template.replace("${idp.entityID}", RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString());
- template = template.replace("${idp.sso.HTTP-POST}", RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL).toString());
- template = template.replace("${idp.sso.HTTP-Redirect}", RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL).toString());
- template = template.replace("${idp.sls.HTTP-POST}", RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL).toString());
- template = template.replace("${idp.signing.certificate}", PemUtils.encodeCertificate(session.keys().getActiveKey(realm).getCertificate()));
- return template;
+ 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<KeyMetadata> 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() == KeyMetadata.Status.PASSIVE ? 1 : -1));
+ keys.addAll(session.keys().getKeys(realm, false));
+ for (KeyMetadata key : keys) {
+ addKeyInfo(keysString, key, KeyTypes.SIGNING.value());
+ }
+ props.put("idp.signing.certificates", keysString.toString());
+ return StringPropertyReplacer.replaceProperties(template, props);
+ }
+
+ private static void addKeyInfo(StringBuilder target, KeyMetadata key, String purpose) {
+ if (key == null) {
+ return;
+ }
+
+ target.append(SPMetadataDescriptor.xmlKeyInfo(" ",
+ key.getKid(), PemUtils.encodeCertificate(key.getCertificate()), purpose, false));
}
@GET
diff --git a/services/src/main/resources/idp-metadata-template.xml b/services/src/main/resources/idp-metadata-template.xml
index 0a53647..a4416cd 100755
--- a/services/src/main/resources/idp-metadata-template.xml
+++ b/services/src/main/resources/idp-metadata-template.xml
@@ -16,22 +16,12 @@
~ limitations under the License.
-->
-<EntitiesDescriptor Name="urn:keycloak"
- xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
+<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">
-
- <KeyDescriptor use="signing">
- <dsig:KeyInfo>
- <dsig:X509Data>
- <dsig:X509Certificate>
- ${idp.signing.certificate}
- </dsig:X509Certificate>
- </dsig:X509Data>
- </dsig:KeyInfo>
- </KeyDescriptor>
+${idp.signing.certificates}
<SingleLogoutService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="${idp.sls.HTTP-POST}" />