KEYCLOAK-1881 Make SAML descriptor endpoint return all certificates

This commit is contained in:
Hynek Mlnarik 2016-11-02 08:19:14 +01:00
parent 5d840500af
commit d5c3bde0af
4 changed files with 68 additions and 30 deletions

View file

@ -98,7 +98,7 @@ public final class StringPropertyReplacer
public static String replaceProperties(final String string, final Properties props) public static String replaceProperties(final String string, final Properties props)
{ {
final char[] chars = string.toCharArray(); final char[] chars = string.toCharArray();
StringBuffer buffer = new StringBuffer(); StringBuilder buffer = new StringBuilder();
boolean properties = false; boolean properties = false;
int state = NORMAL; int state = NORMAL;
int start = 0; int start = 0;

View file

@ -17,26 +17,21 @@
package org.keycloak.saml; package org.keycloak.saml;
import org.keycloak.common.util.PemUtils;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class SPMetadataDescriptor { public class SPMetadataDescriptor {
public static String getSPDescriptor(String binding, String assertionEndpoint, String logoutEndpoint, boolean wantAuthnRequestsSigned, String entityId, String nameIDPolicyFormat, String certificatePem) { public static String getSPDescriptor(String binding, String assertionEndpoint, String logoutEndpoint, boolean wantAuthnRequestsSigned, String entityId, String nameIDPolicyFormat, String certificatePem) {
String descriptor = String descriptor =
"<EntityDescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" entityID=\"" + entityId + "\">\n" + "<EntityDescriptor xmlns=\"urn:oasis:names:tc:SAML:2.0:metadata\" entityID=\"" + entityId + "\">\n" +
" <SPSSODescriptor AuthnRequestsSigned=\"" + wantAuthnRequestsSigned + "\"\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"; " 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) { if (wantAuthnRequestsSigned) {
descriptor += descriptor += xmlKeyInfo(null, certificatePem, "signing", true);
" <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 += descriptor +=
" <SingleLogoutService Binding=\"" + binding + "\" Location=\"" + logoutEndpoint + "\"/>\n" + " <SingleLogoutService Binding=\"" + binding + "\" Location=\"" + logoutEndpoint + "\"/>\n" +
@ -44,10 +39,34 @@ public class SPMetadataDescriptor {
" </NameIDFormat>\n" + " </NameIDFormat>\n" +
" <AssertionConsumerService\n" + " <AssertionConsumerService\n" +
" Binding=\"" + binding + "\" Location=\"" + assertionEndpoint + "\"\n" + " Binding=\"" + binding + "\" Location=\"" + assertionEndpoint + "\"\n" +
" index=\"1\" isDefault=\"true\" />\n"; " index=\"1\" isDefault=\"true\" />\n" +
descriptor +=
" </SPSSODescriptor>\n" + " </SPSSODescriptor>\n" +
"</EntityDescriptor>\n"; "</EntityDescriptor>\n";
return descriptor; 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();
}
} }

View file

@ -74,6 +74,17 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URI; import java.net.URI;
import java.security.PublicKey; 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 * 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 { public static String getIDPMetadataDescriptor(UriInfo uriInfo, KeycloakSession session, RealmModel realm) throws IOException {
InputStream is = SamlService.class.getResourceAsStream("/idp-metadata-template.xml"); InputStream is = SamlService.class.getResourceAsStream("/idp-metadata-template.xml");
String template = StreamUtil.readString(is); String template = StreamUtil.readString(is);
template = template.replace("${idp.entityID}", RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString()); Properties props = new Properties();
template = template.replace("${idp.sso.HTTP-POST}", RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL).toString()); props.put("idp.entityID", RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString());
template = template.replace("${idp.sso.HTTP-Redirect}", RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL).toString()); props.put("idp.sso.HTTP-POST", 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()); props.put("idp.sso.HTTP-Redirect", RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL).toString());
template = template.replace("${idp.signing.certificate}", PemUtils.encodeCertificate(session.keys().getActiveKey(realm).getCertificate())); props.put("idp.sls.HTTP-POST", RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL).toString());
return template; 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 @GET

View file

@ -16,22 +16,12 @@
~ limitations under the License. ~ limitations under the License.
--> -->
<EntitiesDescriptor Name="urn:keycloak" <EntitiesDescriptor Name="urn:keycloak" xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
xmlns:dsig="http://www.w3.org/2000/09/xmldsig#"> xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
<EntityDescriptor entityID="${idp.entityID}"> <EntityDescriptor entityID="${idp.entityID}">
<IDPSSODescriptor WantAuthnRequestsSigned="true" <IDPSSODescriptor WantAuthnRequestsSigned="true"
protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"> protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
${idp.signing.certificates}
<KeyDescriptor use="signing">
<dsig:KeyInfo>
<dsig:X509Data>
<dsig:X509Certificate>
${idp.signing.certificate}
</dsig:X509Certificate>
</dsig:X509Data>
</dsig:KeyInfo>
</KeyDescriptor>
<SingleLogoutService <SingleLogoutService
Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="${idp.sls.HTTP-POST}" /> Location="${idp.sls.HTTP-POST}" />