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 820717b1ac..86cab91762 100755 --- a/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java +++ b/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java @@ -444,6 +444,17 @@ public class SAMLEndpoint { assertionElement = DocumentUtil.getElement(holder.getSamlDocument(), new QName(JBossSAMLConstants.ASSERTION.get())); } + // Validate the response Issuer + final String responseIssuer = responseType.getIssuer() != null ? responseType.getIssuer().getValue(): null; + final boolean responseIssuerValidationSuccess = config.getIdpEntityId() == null || + (responseIssuer != null && responseIssuer.equals(config.getIdpEntityId())); + if (!responseIssuerValidationSuccess) { + logger.errorf("Response Issuer validation failed: expected %s, actual %s", config.getIdpEntityId(), responseIssuer); + event.event(EventType.IDENTITY_PROVIDER_RESPONSE); + event.error(Errors.INVALID_SAML_RESPONSE); + return ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, Messages.INVALID_REQUESTER); + } + // Validate InResponseTo attribute: must match the generated request ID String expectedRequestId = authSession.getClientNote(SamlProtocol.SAML_REQUEST_ID_BROKER); final boolean inResponseToValidationSuccess = validateInResponseToAttribute(responseType, expectedRequestId); @@ -470,7 +481,20 @@ public class SAMLEndpoint { // This methods writes the parsed and decrypted id back on the responseType parameter: AssertionUtil.decryptId(responseType, keys.getPrivateKey()); } + AssertionType assertion = responseType.getAssertions().get(0).getAssertion(); + + // Validate the assertion Issuer + final String assertionIssuer = assertion.getIssuer() != null ? assertion.getIssuer().getValue(): null; + final boolean assertionIssuerValidationSuccess = config.getIdpEntityId() == null || + (assertionIssuer != null && assertionIssuer.equals(config.getIdpEntityId())); + if (!assertionIssuerValidationSuccess) { + logger.errorf("Assertion Issuer validation failed: expected %s, actual %s", config.getIdpEntityId(), assertionIssuer); + event.event(EventType.IDENTITY_PROVIDER_RESPONSE); + event.error(Errors.INVALID_SAML_RESPONSE); + return ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, Messages.INVALID_REQUESTER); + } + NameIDType subjectNameID = getSubjectNameID(assertion); String principal = getPrincipal(assertion); 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 19cfd07d09..65f89552a6 100755 --- a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java +++ b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java @@ -35,6 +35,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 IDP_ENTITY_ID = "idpEntityId"; 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"; @@ -78,6 +79,14 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel { getConfig().put(ENTITY_ID, entityId); } + public String getIdpEntityId() { + return getConfig().get(IDP_ENTITY_ID); + } + + public void setIdpEntityId(String idpEntityId) { + getConfig().put(IDP_ENTITY_ID, idpEntityId); + } + public String getSingleSignOnServiceUrl() { return getConfig().get(SINGLE_SIGN_ON_SERVICE_URL); } diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java index af4df91757..57699444cf 100755 --- a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java +++ b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java @@ -123,6 +123,7 @@ public class SAMLIdentityProviderFactory extends AbstractIdentityProviderFactory } } + samlIdentityProviderConfig.setIdpEntityId(entityType.getEntityID()); samlIdentityProviderConfig.setSingleLogoutServiceUrl(singleLogoutServiceUrl); samlIdentityProviderConfig.setSingleSignOnServiceUrl(singleSignOnServiceUrl); samlIdentityProviderConfig.setWantAuthnRequestsSigned(idpDescriptor.isWantAuthnRequestsSigned()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IdentityProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IdentityProviderTest.java index 5d57e70729..b1bf2e16de 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IdentityProviderTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IdentityProviderTest.java @@ -924,7 +924,8 @@ public class IdentityProviderTest extends AbstractAdminTest { "signingCertificate", "addExtensionsElementWithKeyInfo", "loginHint", - "hideOnLoginPage" + "hideOnLoginPage", + "idpEntityId" )); assertThat(config, hasEntry("validateSignature", "true")); assertThat(config, hasEntry("singleLogoutServiceUrl", "http://localhost:8080/auth/realms/master/protocol/saml")); @@ -935,6 +936,7 @@ public class IdentityProviderTest extends AbstractAdminTest { assertThat(config, hasEntry("addExtensionsElementWithKeyInfo", "false")); assertThat(config, hasEntry("nameIDPolicyFormat", "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent")); assertThat(config, hasEntry("hideOnLoginPage", "true")); + assertThat(config, hasEntry("idpEntityId", "http://localhost:8080/auth/realms/master")); assertThat(config, hasEntry(is("signingCertificate"), notNullValue())); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerTest.java index 8ed39e8961..29646e39ce 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerTest.java @@ -4,6 +4,7 @@ import org.keycloak.admin.client.resource.IdentityProviderResource; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.UserResource; import com.google.common.collect.ImmutableMap; +import org.keycloak.broker.saml.SAMLIdentityProviderConfig; import org.keycloak.broker.saml.mappers.AttributeToRoleMapper; import org.keycloak.broker.saml.mappers.UserAttributeMapper; import org.keycloak.dom.saml.v2.assertion.AssertionType; @@ -25,9 +26,12 @@ import org.keycloak.saml.processing.api.saml.v2.request.SAML2Request; import org.keycloak.saml.processing.core.parsers.saml.protocol.SAMLProtocolQNames; import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder; 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 java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -342,6 +346,70 @@ public final class KcSamlBrokerTest extends AbstractAdvancedBrokerTest { } + // Issue #10982 + @Test + public void loginWithIdpEntityIdCorrect() throws Exception { + // Set the expected IDP Entity ID to the correct value + try (Closeable idpUpdater = new IdentityProviderAttributeUpdater(identityProviderResource) + .setAttribute(SAMLIdentityProviderConfig.IDP_ENTITY_ID, "https://localhost:8543/auth/realms/provider") + .update()) + { + AuthnRequestType loginRep = SamlClient.createLoginRequestDocument(AbstractSamlTest.SAML_CLIENT_ID_SALES_POST + ".dot/ted", getConsumerRoot() + "/sales-post/saml", null); + + Document doc = SAML2Request.convert(loginRep); + + SAMLDocumentHolder samlResponse = 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() + .build() + + .login().user(bc.getUserLogin(), bc.getUserPassword()).build() + + .processSamlResponse(Binding.POST) // Response from producer IdP + .build() + + // first-broker flow + .updateProfile().firstName("a").lastName("b").email(bc.getUserEmail()).username(bc.getUserLogin()).build() + .followOneRedirect() + + .getSamlResponse(Binding.POST); // Response from consumer IdP + + Assert.assertThat(samlResponse, Matchers.notNullValue()); + Assert.assertThat(samlResponse.getSamlObject(), isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS)); + } + } + + // Issue #10982 + @Test + public void loginWithIdpEntityIdMismatchResponse() throws Exception { + // Set the expected IDP Entity ID to a wrong value + try (Closeable idpUpdater = new IdentityProviderAttributeUpdater(identityProviderResource) + .setAttribute(SAMLIdentityProviderConfig.IDP_ENTITY_ID, "http://my.custom.idp.entity.id") + .update()) + { + 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() + .build() + + .login().user(bc.getUserLogin(), bc.getUserPassword()).build() + + .processSamlResponse(Binding.POST) // Response from producer IdP + .build() + .execute(hr -> assertThat(hr, statusCodeIsHC(Response.Status.BAD_REQUEST))); // Response from consumer IdP + } + } + // KEYCLOAK-17935 @Test public void loginInResponseToMismatch() throws Exception { 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 b31c4aaaee..d20231e264 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 @@ -772,6 +772,8 @@ 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.idp-entity-id=Identity Provider Entity ID +identity-provider.saml.idp-entity-id.tooltip=The Entity ID used to validate the Issuer for received SAML assertions. If empty, no Issuer validation is performed. 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 identity-provider.saml.attribute-consuming-service-index=Attribute Consuming Service Index 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 01421798fd..5b2fdb93cf 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 @@ -140,6 +140,13 @@ {{:: 'identity-provider.saml.entity-id.tooltip' | translate}} +
+ +
+ +
+ {{:: 'identity-provider.saml.idp-entity-id.tooltip' | translate}} +