KEYCLOAK-1881 Make SAML descriptor endpoint return all certificates
This commit is contained in:
parent
5d840500af
commit
d5c3bde0af
4 changed files with 68 additions and 30 deletions
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}" />
|
||||||
|
|
Loading…
Reference in a new issue