KEYCLOAK-15485 Add option to enable SAML SP metadata signature

This commit is contained in:
Luca Leonardo Scorcia 2020-09-08 17:33:44 -04:00 committed by Hynek Mlnařík
parent 3723d78e3c
commit 10077b1efe
6 changed files with 117 additions and 0 deletions

View file

@ -36,6 +36,7 @@ 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.common.IDGenerator;
import org.keycloak.saml.processing.core.saml.v2.writers.SAMLMetadataWriter;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
@ -60,6 +61,7 @@ public class SPMetadataDescriptor {
SAMLMetadataWriter metadataWriter = new SAMLMetadataWriter(writer);
EntityDescriptorType entityDescriptor = new EntityDescriptorType(entityId);
entityDescriptor.setID(IDGenerator.create("ID_"));
SPSSODescriptorType spSSODescriptor = new SPSSODescriptorType(Arrays.asList(PROTOCOL_NSURI.get()));
spSSODescriptor.setAuthnRequestsSigned(wantAuthnRequestsSigned);

View file

@ -41,19 +41,25 @@ import org.keycloak.saml.SamlProtocolExtensionsAwareBuilder.NodeGenerator;
import org.keycloak.saml.common.constants.GeneralConstants;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.common.exceptions.ConfigurationException;
import org.keycloak.saml.common.util.DocumentUtil;
import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request;
import org.keycloak.saml.processing.api.saml.v2.sig.SAML2Signature;
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
import org.keycloak.saml.validators.DestinationValidator;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.util.JsonSerialization;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
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 javax.xml.crypto.dsig.CanonicalizationMethod;
import java.net.URI;
import java.security.KeyPair;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
@ -332,6 +338,26 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
wantAuthnRequestsSigned, wantAssertionsSigned, wantAssertionsEncrypted,
entityId, nameIDPolicyFormat, signingKeys, encryptionKeys);
// Metadata signing
if (getConfig().isSignSpMetadata())
{
KeyManager.ActiveRsaKey activeKey = session.keys().getActiveRsaKey(realm);
String keyName = getConfig().getXmlSigKeyInfoKeyNameTransformer().getKeyName(activeKey.getKid(), activeKey.getCertificate());
KeyPair keyPair = new KeyPair(activeKey.getPublicKey(), activeKey.getPrivateKey());
Document metadataDocument = DocumentUtil.getDocument(descriptor);
SAML2Signature signatureHelper = new SAML2Signature();
signatureHelper.setSignatureMethod(getSignatureAlgorithm().getXmlSignatureMethod());
signatureHelper.setDigestMethod(getSignatureAlgorithm().getXmlSignatureDigestMethod());
Node nextSibling = metadataDocument.getDocumentElement().getFirstChild();
signatureHelper.setNextSibling(nextSibling);
signatureHelper.signSAMLDocument(metadataDocument, keyName, keyPair, CanonicalizationMethod.EXCLUSIVE);
descriptor = DocumentUtil.getDocumentAsString(metadataDocument);
}
return Response.ok(descriptor, MediaType.APPLICATION_XML_TYPE).build();
} catch (Exception e) {
logger.warn("Failed to export SAML SP Metadata!", e);

View file

@ -56,6 +56,7 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
public static final String AUTHN_CONTEXT_COMPARISON_TYPE = "authnContextComparisonType";
public static final String AUTHN_CONTEXT_CLASS_REFS = "authnContextClassRefs";
public static final String AUTHN_CONTEXT_DECL_REFS = "authnContextDeclRefs";
public static final String SIGN_SP_METADATA = "signSpMetadata";
public SAMLIdentityProviderConfig() {
}
@ -317,6 +318,14 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
getConfig().put(AUTHN_CONTEXT_DECL_REFS, authnContextDeclRefs);
}
public boolean isSignSpMetadata() {
return Boolean.valueOf(getConfig().get(SIGN_SP_METADATA));
}
public void setSignSpMetadata(boolean signSpMetadata) {
getConfig().put(SIGN_SP_METADATA, String.valueOf(signSpMetadata));
}
@Override
public void validate(RealmModel realm) {
SslRequired sslRequired = realm.getSslRequired();

View file

@ -42,12 +42,17 @@ import org.keycloak.representations.idm.ErrorRepresentation;
import org.keycloak.representations.idm.IdentityProviderMapperRepresentation;
import org.keycloak.representations.idm.IdentityProviderMapperTypeRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.saml.common.exceptions.ConfigurationException;
import org.keycloak.saml.common.exceptions.ParsingException;
import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.common.util.DocumentUtil;
import org.keycloak.saml.processing.core.parsers.saml.SAMLParser;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.broker.OIDCIdentityProviderConfigRep;
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
import org.keycloak.testsuite.util.AdminEventPaths;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import javax.ws.rs.ClientErrorException;
@ -87,6 +92,7 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.XMLDSIG_NSURI;
import static org.keycloak.testsuite.util.ServerURLs.AUTH_SERVER_SSL_REQUIRED;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE;
@ -1007,4 +1013,69 @@ public class IdentityProviderTest extends AbstractAdminTest {
Assert.assertEquals("id", id, info.get("id"));
Assert.assertEquals("name", name, info.get("name"));
}
@Test
public void testSamlExportSignatureOff() throws URISyntaxException, IOException, ConfigurationException, ParsingException, ProcessingException {
// Use import-config to convert IDPSSODescriptor file into key value pairs
// to use when creating a SAML Identity Provider
MultipartFormDataOutput form = new MultipartFormDataOutput();
form.addFormData("providerId", "saml", MediaType.TEXT_PLAIN_TYPE);
URL idpMeta = getClass().getClassLoader().getResource("admin-test/saml-idp-metadata.xml");
byte [] content = Files.readAllBytes(Paths.get(idpMeta.toURI()));
String body = new String(content, Charset.forName("utf-8"));
form.addFormData("file", body, MediaType.APPLICATION_XML_TYPE, "saml-idp-metadata.xml");
Map<String, String> result = realm.identityProviders().importFrom(form);
// Explicitly disable SP Metadata Signature
result.put(SAMLIdentityProviderConfig.SIGN_SP_METADATA, "false");
// Create new SAML identity provider using configuration retrieved from import-config
IdentityProviderRepresentation idpRep = createRep("saml", "saml", true, result);
create(idpRep);
// Perform export, and make sure some of the values are like they're supposed to be
Response response = realm.identityProviders().get("saml").export("xml");
Assert.assertEquals(200, response.getStatus());
body = response.readEntity(String.class);
response.close();
Document document = DocumentUtil.getDocument(body);
Element signatureElement = DocumentUtil.getDirectChildElement(document.getDocumentElement(), XMLDSIG_NSURI.get(), "Signature");
Assert.assertNull(signatureElement);
}
@Test
public void testSamlExportSignatureOn() throws URISyntaxException, IOException, ConfigurationException, ParsingException, ProcessingException {
// Use import-config to convert IDPSSODescriptor file into key value pairs
// to use when creating a SAML Identity Provider
MultipartFormDataOutput form = new MultipartFormDataOutput();
form.addFormData("providerId", "saml", MediaType.TEXT_PLAIN_TYPE);
URL idpMeta = getClass().getClassLoader().getResource("admin-test/saml-idp-metadata.xml");
byte [] content = Files.readAllBytes(Paths.get(idpMeta.toURI()));
String body = new String(content, Charset.forName("utf-8"));
form.addFormData("file", body, MediaType.APPLICATION_XML_TYPE, "saml-idp-metadata.xml");
Map<String, String> result = realm.identityProviders().importFrom(form);
// Explicitly enable SP Metadata Signature
result.put(SAMLIdentityProviderConfig.SIGN_SP_METADATA, "true");
// Create new SAML identity provider using configuration retrieved from import-config
IdentityProviderRepresentation idpRep = createRep("saml", "saml", true, result);
create(idpRep);
// Perform export, and make sure some of the values are like they're supposed to be
Response response = realm.identityProviders().get("saml").export("xml");
Assert.assertEquals(200, response.getStatus());
body = response.readEntity(String.class);
response.close();
Document document = DocumentUtil.getDocument(body);
Element signatureElement = DocumentUtil.getDirectChildElement(document.getDocumentElement(), XMLDSIG_NSURI.get(), "Signature");
Assert.assertNotNull(signatureElement);
}
}

View file

@ -699,6 +699,8 @@ validating-x509-certificate.tooltip=The certificate in PEM format that must be u
saml.loginHint=Pass subject
saml.loginHint.tooltip=During login phase, forward an optional login_hint query parameter to SAML AuthnRequest's Subject.
saml.import-from-url.tooltip=Import metadata from a remote IDP SAML entity descriptor.
identity-provider.saml.sign-sp-metadata=Sign Service Provider Metadata
identity-provider.saml.sign-sp-metadata.tooltip=Enable/disable signature of the provider SAML metadata
identity-provider.saml.requested-authncontext=Requested AuthnContext Constraints
identity-provider.saml.requested-authncontext.tooltip=Allows the SP to specify the authentication context requirements of authentication statements returned.
identity-provider.saml.authncontext-comparison-type=Comparison

View file

@ -267,6 +267,13 @@
</div>
<kc-tooltip>{{:: 'validating-x509-certificate.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="signSpMetadata">{{:: 'identity-provider.saml.sign-sp-metadata' | translate}}</label>
<div class="col-md-6">
<input ng-model="identityProvider.config.signSpMetadata" id="signSpMetadata" onoffswitchvalue on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
</div>
<kc-tooltip>{{:: 'identity-provider.saml.sign-sp-metadata.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="loginHint">{{:: 'saml.loginHint' | translate}}</label>
<div class="col-sm-4">