diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java b/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java index a4943a0e7f..de3870c981 100755 --- a/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java +++ b/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java @@ -370,7 +370,12 @@ public class SAMLEndpoint { } private String getEntityId(UriInfo uriInfo, RealmModel realm) { - return UriBuilder.fromUri(uriInfo.getBaseUri()).path("realms").path(realm.getName()).build().toString(); + String configEntityId = config.getEntityId(); + + if (configEntityId == null || configEntityId.isEmpty()) + return UriBuilder.fromUri(uriInfo.getBaseUri()).path("realms").path(realm.getName()).build().toString(); + else + return configEntityId; } protected Response handleLoginResponse(String samlResponse, SAMLDocumentHolder holder, ResponseType responseType, String relayState, String clientId) { diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java index c590e31fc6..012d2e63e2 100755 --- a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java +++ b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java @@ -165,7 +165,12 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider getAuthnContextClassRefUris() { diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java index 7ec28997f9..daa075ab31 100755 --- a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java +++ b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java @@ -33,6 +33,7 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel { public static final XmlKeyInfoKeyNameTransformer DEFAULT_XML_KEY_INFO_KEY_NAME_TRANSFORMER = XmlKeyInfoKeyNameTransformer.NONE; + public static final String ENTITY_ID = "entityId"; public static final String ADD_EXTENSIONS_ELEMENT_WITH_KEY_INFO = "addExtensionsElementWithKeyInfo"; public static final String BACKCHANNEL_SUPPORTED = "backchannelSupported"; public static final String ENCRYPTION_PUBLIC_KEY = "encryptionPublicKey"; @@ -65,6 +66,14 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel { super(identityProviderModel); } + public String getEntityId() { + return getConfig().get(ENTITY_ID); + } + + public void setEntityId(String entityId) { + getConfig().put(ENTITY_ID, entityId); + } + public String getSingleSignOnServiceUrl() { return getConfig().get(SINGLE_SIGN_ON_SERVICE_URL); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlCustomEntityIdBrokerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlCustomEntityIdBrokerTest.java new file mode 100644 index 0000000000..ccee8bc567 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlCustomEntityIdBrokerTest.java @@ -0,0 +1,99 @@ +package org.keycloak.testsuite.broker; + +import org.keycloak.broker.saml.SAMLIdentityProviderConfig; +import org.keycloak.dom.saml.v2.protocol.AuthnRequestType; +import org.keycloak.saml.common.util.DocumentUtil; +import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request; +import org.keycloak.testsuite.saml.AbstractSamlTest; +import org.keycloak.testsuite.updaters.IdentityProviderAttributeUpdater; +import org.keycloak.testsuite.util.SamlClient; +import org.keycloak.testsuite.util.SamlClient.Binding; +import org.keycloak.testsuite.util.SamlClientBuilder; +import java.io.Closeable; + +import org.junit.Assert; +import org.junit.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import static org.keycloak.saml.common.constants.JBossSAMLURIConstants.ASSERTION_NSURI; +import static org.keycloak.testsuite.broker.BrokerTestTools.getConsumerRoot; + +/** + * Final class as it's not intended to be overriden. + */ +public final class KcSamlCustomEntityIdBrokerTest extends AbstractBrokerTest { + + @Override + protected BrokerConfiguration getBrokerConfiguration() { + return KcSamlBrokerConfiguration.INSTANCE; + } + + @Test + public void testCustomEntityNotSet() throws Exception { + // No comparison type, no classrefs, no declrefs -> No RequestedAuthnContext + try (Closeable idpUpdater = new IdentityProviderAttributeUpdater(identityProviderResource) + .update()) + { + // Build the login request document + AuthnRequestType loginRep = SamlClient.createLoginRequestDocument(AbstractSamlTest.SAML_CLIENT_ID_SALES_POST + ".dot/ted", getConsumerRoot() + "/sales-post/saml", null); + Document doc = SAML2Request.convert(loginRep); + new SamlClientBuilder() + .authnRequest(getConsumerSamlEndpoint(bc.consumerRealmName()), doc, Binding.POST) + .build() // Request to consumer IdP + .login().idp(bc.getIDPAlias()).build() + .processSamlResponse(Binding.POST) // AuthnRequest to producer IdP + .targetAttributeSamlRequest() + .transformDocument((document) -> { + try + { + log.infof("Document: %s", DocumentUtil.asString(document)); + + // Find the Issuer element + Element issuerElement = DocumentUtil.getDirectChildElement(document.getDocumentElement(), ASSERTION_NSURI.get(), "Issuer"); + Assert.assertEquals("Unexpected Issuer element value", "https://localhost:8543/auth/realms/consumer", issuerElement.getTextContent()); + } + catch (Exception ex) + { + throw new RuntimeException(ex); + } + }) + .build() + .execute(); + } + } + + @Test + public void testCustomEntityIdSet() throws Exception { + // Comparison type set, no classrefs, no declrefs -> No RequestedAuthnContext + try (Closeable idpUpdater = new IdentityProviderAttributeUpdater(identityProviderResource) + .setAttribute(SAMLIdentityProviderConfig.ENTITY_ID, "http://my.custom.entity.id") + .update()) + { + // Build the login request document + AuthnRequestType loginRep = SamlClient.createLoginRequestDocument(AbstractSamlTest.SAML_CLIENT_ID_SALES_POST + ".dot/ted", getConsumerRoot() + "/sales-post/saml", null); + Document doc = SAML2Request.convert(loginRep); + new SamlClientBuilder() + .authnRequest(getConsumerSamlEndpoint(bc.consumerRealmName()), doc, Binding.POST) + .build() // Request to consumer IdP + .login().idp(bc.getIDPAlias()).build() + .processSamlResponse(Binding.POST) // AuthnRequest to producer IdP + .targetAttributeSamlRequest() + .transformDocument((document) -> { + try + { + log.infof("Document: %s", DocumentUtil.asString(document)); + + // Find the Issuer element + Element issuerElement = DocumentUtil.getDirectChildElement(document.getDocumentElement(), ASSERTION_NSURI.get(), "Issuer"); + Assert.assertEquals("Unexpected Issuer element value", "http://my.custom.entity.id", issuerElement.getTextContent()); + } + catch (Exception ex) + { + throw new RuntimeException(ex); + } + }) + .build() + .execute(); + } + } +} diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties index 8d0d42c969..390920edad 100644 --- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties +++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties @@ -664,6 +664,8 @@ import-from-url=Import from URL identity-provider.import-from-url.tooltip=Import metadata from a remote IDP discovery descriptor. import-from-file=Import from file identity-provider.import-from-file.tooltip=Import metadata from a downloaded IDP discovery descriptor. +identity-provider.saml.entity-id=Service Provider Entity ID +identity-provider.saml.entity-id.tooltip=The Entity ID that will be used to uniquely identify this SAML Service Provider identity-provider.saml.protocol-endpoints.saml=SAML 2.0 Service Provider Metadata identity-provider.saml.protocol-endpoints.saml.tooltip=Shows the configuration of the Service Provider endpoint saml-config=SAML Config diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js index 1c59452558..25a71df6f5 100644 --- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js @@ -873,6 +873,7 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload $scope.identityProvider.config.signatureAlgorithm = $scope.signatureAlgorithms[1]; $scope.identityProvider.config.xmlSigKeyInfoKeyNameTransformer = $scope.xmlKeyNameTranformers[1]; } + $scope.identityProvider.config.entityId = $scope.identityProvider.config.entityId || (authUrl + '/realms/' + realm.realm); } $scope.hidePassword = true; diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html index c88e09d7a2..f4165645c9 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html @@ -133,6 +133,13 @@
{{:: 'saml-config' | translate}} {{:: 'identity-provider.saml-config.tooltip' | translate}} +
+ +
+ +
+ {{:: 'identityProvider.config.entity-id.tooltip' | translate}} +