KEYCLOAK-15485 Add option to enable SAML SP metadata signature
This commit is contained in:
parent
3723d78e3c
commit
10077b1efe
6 changed files with 117 additions and 0 deletions
|
@ -36,6 +36,7 @@ import javax.xml.stream.XMLStreamException;
|
||||||
import javax.xml.stream.XMLStreamWriter;
|
import javax.xml.stream.XMLStreamWriter;
|
||||||
import org.keycloak.saml.common.util.StaxUtil;
|
import org.keycloak.saml.common.util.StaxUtil;
|
||||||
import org.keycloak.saml.common.exceptions.ProcessingException;
|
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.keycloak.saml.processing.core.saml.v2.writers.SAMLMetadataWriter;
|
||||||
import org.w3c.dom.Document;
|
import org.w3c.dom.Document;
|
||||||
import org.w3c.dom.Element;
|
import org.w3c.dom.Element;
|
||||||
|
@ -60,6 +61,7 @@ public class SPMetadataDescriptor {
|
||||||
SAMLMetadataWriter metadataWriter = new SAMLMetadataWriter(writer);
|
SAMLMetadataWriter metadataWriter = new SAMLMetadataWriter(writer);
|
||||||
|
|
||||||
EntityDescriptorType entityDescriptor = new EntityDescriptorType(entityId);
|
EntityDescriptorType entityDescriptor = new EntityDescriptorType(entityId);
|
||||||
|
entityDescriptor.setID(IDGenerator.create("ID_"));
|
||||||
|
|
||||||
SPSSODescriptorType spSSODescriptor = new SPSSODescriptorType(Arrays.asList(PROTOCOL_NSURI.get()));
|
SPSSODescriptorType spSSODescriptor = new SPSSODescriptorType(Arrays.asList(PROTOCOL_NSURI.get()));
|
||||||
spSSODescriptor.setAuthnRequestsSigned(wantAuthnRequestsSigned);
|
spSSODescriptor.setAuthnRequestsSigned(wantAuthnRequestsSigned);
|
||||||
|
|
|
@ -41,19 +41,25 @@ import org.keycloak.saml.SamlProtocolExtensionsAwareBuilder.NodeGenerator;
|
||||||
import org.keycloak.saml.common.constants.GeneralConstants;
|
import org.keycloak.saml.common.constants.GeneralConstants;
|
||||||
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
|
||||||
import org.keycloak.saml.common.exceptions.ConfigurationException;
|
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.request.SAML2Request;
|
||||||
|
import org.keycloak.saml.processing.api.saml.v2.sig.SAML2Signature;
|
||||||
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
|
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
|
||||||
import org.keycloak.saml.validators.DestinationValidator;
|
import org.keycloak.saml.validators.DestinationValidator;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
import org.keycloak.util.JsonSerialization;
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
import org.w3c.dom.Document;
|
||||||
import org.w3c.dom.Element;
|
import org.w3c.dom.Element;
|
||||||
|
import org.w3c.dom.Node;
|
||||||
|
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import javax.ws.rs.core.UriBuilder;
|
import javax.ws.rs.core.UriBuilder;
|
||||||
import javax.ws.rs.core.UriInfo;
|
import javax.ws.rs.core.UriInfo;
|
||||||
|
import javax.xml.crypto.dsig.CanonicalizationMethod;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.security.KeyPair;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
|
@ -332,6 +338,26 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
|
||||||
wantAuthnRequestsSigned, wantAssertionsSigned, wantAssertionsEncrypted,
|
wantAuthnRequestsSigned, wantAssertionsSigned, wantAssertionsEncrypted,
|
||||||
entityId, nameIDPolicyFormat, signingKeys, encryptionKeys);
|
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();
|
return Response.ok(descriptor, MediaType.APPLICATION_XML_TYPE).build();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.warn("Failed to export SAML SP Metadata!", e);
|
logger.warn("Failed to export SAML SP Metadata!", e);
|
||||||
|
|
|
@ -56,6 +56,7 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
|
||||||
public static final String AUTHN_CONTEXT_COMPARISON_TYPE = "authnContextComparisonType";
|
public static final String AUTHN_CONTEXT_COMPARISON_TYPE = "authnContextComparisonType";
|
||||||
public static final String AUTHN_CONTEXT_CLASS_REFS = "authnContextClassRefs";
|
public static final String AUTHN_CONTEXT_CLASS_REFS = "authnContextClassRefs";
|
||||||
public static final String AUTHN_CONTEXT_DECL_REFS = "authnContextDeclRefs";
|
public static final String AUTHN_CONTEXT_DECL_REFS = "authnContextDeclRefs";
|
||||||
|
public static final String SIGN_SP_METADATA = "signSpMetadata";
|
||||||
|
|
||||||
public SAMLIdentityProviderConfig() {
|
public SAMLIdentityProviderConfig() {
|
||||||
}
|
}
|
||||||
|
@ -317,6 +318,14 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
|
||||||
getConfig().put(AUTHN_CONTEXT_DECL_REFS, authnContextDeclRefs);
|
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
|
@Override
|
||||||
public void validate(RealmModel realm) {
|
public void validate(RealmModel realm) {
|
||||||
SslRequired sslRequired = realm.getSslRequired();
|
SslRequired sslRequired = realm.getSslRequired();
|
||||||
|
|
|
@ -42,12 +42,17 @@ import org.keycloak.representations.idm.ErrorRepresentation;
|
||||||
import org.keycloak.representations.idm.IdentityProviderMapperRepresentation;
|
import org.keycloak.representations.idm.IdentityProviderMapperRepresentation;
|
||||||
import org.keycloak.representations.idm.IdentityProviderMapperTypeRepresentation;
|
import org.keycloak.representations.idm.IdentityProviderMapperTypeRepresentation;
|
||||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
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.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.saml.processing.core.parsers.saml.SAMLParser;
|
||||||
import org.keycloak.testsuite.Assert;
|
import org.keycloak.testsuite.Assert;
|
||||||
import org.keycloak.testsuite.broker.OIDCIdentityProviderConfigRep;
|
import org.keycloak.testsuite.broker.OIDCIdentityProviderConfigRep;
|
||||||
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
|
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
|
||||||
import org.keycloak.testsuite.util.AdminEventPaths;
|
import org.keycloak.testsuite.util.AdminEventPaths;
|
||||||
|
import org.w3c.dom.Document;
|
||||||
|
import org.w3c.dom.Element;
|
||||||
import org.w3c.dom.NodeList;
|
import org.w3c.dom.NodeList;
|
||||||
|
|
||||||
import javax.ws.rs.ClientErrorException;
|
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.assertThat;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.junit.Assert.fail;
|
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 static org.keycloak.testsuite.util.ServerURLs.AUTH_SERVER_SSL_REQUIRED;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
||||||
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE;
|
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("id", id, info.get("id"));
|
||||||
Assert.assertEquals("name", name, info.get("name"));
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -699,6 +699,8 @@ validating-x509-certificate.tooltip=The certificate in PEM format that must be u
|
||||||
saml.loginHint=Pass subject
|
saml.loginHint=Pass subject
|
||||||
saml.loginHint.tooltip=During login phase, forward an optional login_hint query parameter to SAML AuthnRequest's 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.
|
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=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.requested-authncontext.tooltip=Allows the SP to specify the authentication context requirements of authentication statements returned.
|
||||||
identity-provider.saml.authncontext-comparison-type=Comparison
|
identity-provider.saml.authncontext-comparison-type=Comparison
|
||||||
|
|
|
@ -267,6 +267,13 @@
|
||||||
</div>
|
</div>
|
||||||
<kc-tooltip>{{:: 'validating-x509-certificate.tooltip' | translate}}</kc-tooltip>
|
<kc-tooltip>{{:: 'validating-x509-certificate.tooltip' | translate}}</kc-tooltip>
|
||||||
</div>
|
</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">
|
<div class="form-group">
|
||||||
<label class="col-sm-2 control-label" for="loginHint">{{:: 'saml.loginHint' | translate}}</label>
|
<label class="col-sm-2 control-label" for="loginHint">{{:: 'saml.loginHint' | translate}}</label>
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-4">
|
||||||
|
|
Loading…
Reference in a new issue