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)
|
||||
{
|
||||
final char[] chars = string.toCharArray();
|
||||
StringBuffer buffer = new StringBuffer();
|
||||
StringBuilder buffer = new StringBuilder();
|
||||
boolean properties = false;
|
||||
int state = NORMAL;
|
||||
int start = 0;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}" />
|
||||
|
|
Loading…
Reference in a new issue