KEYCLOAK-15697 Make the Service Provider Entity ID user configurable

This commit is contained in:
Luca Leonardo Scorcia 2020-09-23 18:08:49 -04:00 committed by Hynek Mlnařík
parent 59ef7d258f
commit f274ec447b
7 changed files with 130 additions and 2 deletions

View file

@ -370,7 +370,12 @@ public class SAMLEndpoint {
}
private String getEntityId(UriInfo uriInfo, RealmModel realm) {
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) {

View file

@ -165,7 +165,12 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
}
private String getEntityId(UriInfo uriInfo, RealmModel realm) {
String configEntityId = getConfig().getEntityId();
if (configEntityId == null || configEntityId.isEmpty())
return UriBuilder.fromUri(uriInfo.getBaseUri()).path("realms").path(realm.getName()).build().toString();
else
return configEntityId;
}
private List<String> getAuthnContextClassRefUris() {

View file

@ -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);
}

View file

@ -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();
}
}
}

View file

@ -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

View file

@ -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;

View file

@ -133,6 +133,13 @@
<fieldset>
<legend uncollapsed><span class="text">{{:: 'saml-config' | translate}}</span> <kc-tooltip>{{:: 'identity-provider.saml-config.tooltip' | translate}}</kc-tooltip></legend>
<div class="form-group clearfix">
<label class="col-md-2 control-label" for="entityId"><span class="required">*</span> {{:: 'identity-provider.saml.entity-id' | translate}}</label>
<div class="col-md-6">
<input kc-no-reserved-chars class="form-control" id="entityId" type="text" ng-model="identityProvider.config.entityId" required>
</div>
<kc-tooltip>{{:: 'identityProvider.config.entity-id.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix">
<label class="col-md-2 control-label" for="singleSignOnServiceUrl"><span class="required">*</span> {{:: 'single-signon-service-url' | translate}}</label>
<div class="col-md-6">