keycloak-aplcache

Merge pull request #3553 from hmlnarik/KEYCLOAK-3731-alternative KEYCLOAK-3731

11/28/2016 2:23:26 PM

Details

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 0ef5276..951d03a 100755
--- a/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
+++ b/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
@@ -66,6 +66,7 @@ import javax.ws.rs.GET;
 import javax.ws.rs.POST;
 import javax.ws.rs.Path;
 import javax.ws.rs.QueryParam;
+import javax.ws.rs.PathParam;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.MediaType;
@@ -92,6 +93,7 @@ public class SAMLEndpoint {
     public static final String SAML_FEDERATED_SUBJECT_NAMEFORMAT = "SAML_FEDERATED_SUBJECT_NAMEFORMAT";
     public static final String SAML_LOGIN_RESPONSE = "SAML_LOGIN_RESPONSE";
     public static final String SAML_ASSERTION = "SAML_ASSERTION";
+    public static final String SAML_IDP_INITIATED_CLIENT_ID = "SAML_IDP_INITIATED_CLIENT_ID";
     public static final String SAML_AUTHN_STATEMENT = "SAML_AUTHN_STATEMENT";
     protected RealmModel realm;
     protected EventBuilder event;
@@ -130,7 +132,7 @@ public class SAMLEndpoint {
     public Response redirectBinding(@QueryParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest,
                                     @QueryParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse,
                                     @QueryParam(GeneralConstants.RELAY_STATE) String relayState)  {
-        return new RedirectBinding().execute(samlRequest, samlResponse, relayState);
+        return new RedirectBinding().execute(samlRequest, samlResponse, relayState, null);
     }
 
 
@@ -141,7 +143,29 @@ public class SAMLEndpoint {
     public Response postBinding(@FormParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest,
                                 @FormParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse,
                                 @FormParam(GeneralConstants.RELAY_STATE) String relayState) {
-        return new PostBinding().execute(samlRequest, samlResponse, relayState);
+        return new PostBinding().execute(samlRequest, samlResponse, relayState, null);
+    }
+
+    @Path("clients/{client_id}")
+    @GET
+    public Response redirectBinding(@QueryParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest,
+                                    @QueryParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse,
+                                    @QueryParam(GeneralConstants.RELAY_STATE) String relayState,
+                                    @PathParam("client_id") String clientId)  {
+        return new RedirectBinding().execute(samlRequest, samlResponse, relayState, clientId);
+    }
+
+
+    /**
+     */
+    @Path("clients/{client_id}")
+    @POST
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    public Response postBinding(@FormParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest,
+                                @FormParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse,
+                                @FormParam(GeneralConstants.RELAY_STATE) String relayState,
+                                @PathParam("client_id") String clientId) {
+        return new PostBinding().execute(samlRequest, samlResponse, relayState, clientId);
     }
 
     protected abstract class Binding {
@@ -194,12 +218,12 @@ public class SAMLEndpoint {
             return new HardcodedKeyLocator(keys);
         }
 
-        public Response execute(String samlRequest, String samlResponse, String relayState) {
+        public Response execute(String samlRequest, String samlResponse, String relayState, String clientId) {
             event = new EventBuilder(realm, session, clientConnection);
             Response response = basicChecks(samlRequest, samlResponse);
             if (response != null) return response;
             if (samlRequest != null) return handleSamlRequest(samlRequest, relayState);
-            else return handleSamlResponse(samlResponse, relayState);
+            else return handleSamlResponse(samlResponse, relayState, clientId);
         }
 
         protected Response handleSamlRequest(String samlRequest, String relayState) {
@@ -304,7 +328,7 @@ public class SAMLEndpoint {
         private String getEntityId(UriInfo uriInfo, RealmModel realm) {
             return UriBuilder.fromUri(uriInfo.getBaseUri()).path("realms").path(realm.getName()).build().toString();
         }
-        protected Response handleLoginResponse(String samlResponse, SAMLDocumentHolder holder, ResponseType responseType, String relayState) {
+        protected Response handleLoginResponse(String samlResponse, SAMLDocumentHolder holder, ResponseType responseType, String relayState, String clientId) {
 
             try {
                 KeyManager.ActiveKey keys = session.keys().getActiveKey(realm);
@@ -316,6 +340,9 @@ public class SAMLEndpoint {
                 BrokeredIdentityContext identity = new BrokeredIdentityContext(subjectNameID.getValue());
                 identity.getContextData().put(SAML_LOGIN_RESPONSE, responseType);
                 identity.getContextData().put(SAML_ASSERTION, assertion);
+                if (clientId != null && ! clientId.trim().isEmpty()) {
+                    identity.getContextData().put(SAML_IDP_INITIATED_CLIENT_ID, clientId);
+                }
 
                 identity.setUsername(subjectNameID.getValue());
 
@@ -369,7 +396,7 @@ public class SAMLEndpoint {
 
 
 
-        public Response handleSamlResponse(String samlResponse, String relayState) {
+        public Response handleSamlResponse(String samlResponse, String relayState, String clientId) {
             SAMLDocumentHolder holder = extractResponseDocument(samlResponse);
             StatusResponseType statusResponse = (StatusResponseType)holder.getSamlObject();
             // validate destination
@@ -390,7 +417,7 @@ public class SAMLEndpoint {
                 }
             }
             if (statusResponse instanceof ResponseType) {
-                return handleLoginResponse(samlResponse, holder, (ResponseType)statusResponse, relayState);
+                return handleLoginResponse(samlResponse, holder, (ResponseType)statusResponse, relayState, clientId);
 
             } else {
                 // todo need to check that it is actually a LogoutResponse
diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlService.java b/services/src/main/java/org/keycloak/protocol/saml/SamlService.java
index 14c5503..c404ef8 100755
--- a/services/src/main/java/org/keycloak/protocol/saml/SamlService.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/SamlService.java
@@ -611,12 +611,29 @@ public class SamlService extends AuthorizationEndpointBase {
             return ErrorPage.error(session, Messages.INVALID_REDIRECT_URI);
         }
 
+        ClientSessionModel clientSession = createClientSessionForIdpInitiatedSso(this.session, this.realm, client, relayState);
+
+        return newBrowserAuthentication(clientSession, false, false);
+    }
+
+    /**
+     * Creates a client session object for SAML IdP-initiated SSO session.
+     * The session takes the parameters from from client definition,
+     * namely binding type and redirect URL.
+     *
+     * @param session KC session
+     * @param realm Realm to create client session in
+     * @param client Client to create client session for
+     * @param relayState Optional relay state - free field as per SAML specification
+     * @return
+     */
+    public static ClientSessionModel createClientSessionForIdpInitiatedSso(KeycloakSession session, RealmModel realm, ClientModel client, String relayState) {
         String bindingType = SamlProtocol.SAML_POST_BINDING;
         if (client.getManagementUrl() == null && client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE) == null && client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE) != null) {
             bindingType = SamlProtocol.SAML_REDIRECT_BINDING;
         }
 
-        String redirect = null;
+        String redirect;
         if (bindingType.equals(SamlProtocol.SAML_REDIRECT_BINDING)) {
             redirect = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE);
         } else {
@@ -640,8 +657,7 @@ public class SamlService extends AuthorizationEndpointBase {
             clientSession.setNote(GeneralConstants.RELAY_STATE, relayState);
         }
 
-        return newBrowserAuthentication(clientSession, false, false);
-
+        return clientSession;
     }
 
     @POST
diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
index a8c4cc4..b1f4587 100755
--- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
+++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
@@ -19,6 +19,7 @@ package org.keycloak.services.resources;
 import org.jboss.logging.Logger;
 import org.jboss.resteasy.spi.HttpRequest;
 import org.jboss.resteasy.spi.ResteasyProviderFactory;
+
 import org.keycloak.OAuth2Constants;
 import org.keycloak.authentication.AuthenticationProcessor;
 import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
@@ -30,6 +31,7 @@ import org.keycloak.broker.provider.IdentityBrokerException;
 import org.keycloak.broker.provider.IdentityProvider;
 import org.keycloak.broker.provider.IdentityProviderFactory;
 import org.keycloak.broker.provider.IdentityProviderMapper;
+import org.keycloak.broker.saml.SAMLEndpoint;
 import org.keycloak.broker.social.SocialIdentityProvider;
 import org.keycloak.common.ClientConnection;
 import org.keycloak.common.util.ObjectUtil;
@@ -54,8 +56,11 @@ import org.keycloak.models.UserModel;
 import org.keycloak.models.UserSessionModel;
 import org.keycloak.models.utils.FormMessage;
 import org.keycloak.protocol.oidc.TokenManager;
+import org.keycloak.protocol.saml.SamlProtocol;
+import org.keycloak.protocol.saml.SamlService;
 import org.keycloak.provider.ProviderFactory;
 import org.keycloak.representations.AccessToken;
+import org.keycloak.saml.common.constants.GeneralConstants;
 import org.keycloak.services.ErrorPage;
 import org.keycloak.services.ErrorResponse;
 import org.keycloak.services.ServicesLogger;
@@ -87,6 +92,8 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 
 import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT;
@@ -255,7 +262,12 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
     public Response authenticated(BrokeredIdentityContext context) {
         IdentityProviderModel identityProviderConfig = context.getIdpConfig();
 
-        ParsedCodeContext parsedCode = parseClientSessionCode(context.getCode());
+        final ParsedCodeContext parsedCode;
+        if (context.getContextData().get(SAMLEndpoint.SAML_IDP_INITIATED_CLIENT_ID) != null) {
+            parsedCode = samlIdpInitiatedSSO((String) context.getContextData().get(SAMLEndpoint.SAML_IDP_INITIATED_CLIENT_ID));
+        } else {
+            parsedCode = parseClientSessionCode(context.getCode());
+        }
         if (parsedCode.response != null) {
             return parsedCode.response;
         }
@@ -696,6 +708,53 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
         return ParsedCodeContext.response(staleCodeError);
     }
 
+    /**
+     * If there is a client whose SAML IDP-initiated SSO URL name is set to the
+     * given {@code clientUrlName}, creates a fresh client session for that
+     * client and returns a {@link ParsedCodeContext} object with that session.
+     * Otherwise returns "client not found" response.
+     *
+     * @param clientUrlName
+     * @return see description
+     */
+    private ParsedCodeContext samlIdpInitiatedSSO(final String clientUrlName) {
+        event.event(EventType.LOGIN);
+        CacheControlUtil.noBackButtonCacheControlHeader();
+        Optional<ClientModel> oClient = this.realmModel.getClients().stream()
+          .filter(c -> Objects.equals(c.getAttribute(SamlProtocol.SAML_IDP_INITIATED_SSO_URL_NAME), clientUrlName))
+          .findFirst();
+
+        if (! oClient.isPresent()) {
+            event.error(Errors.CLIENT_NOT_FOUND);
+            return ParsedCodeContext.response(redirectToErrorPage(Messages.CLIENT_NOT_FOUND));
+        }
+
+        ClientSessionModel clientSession = SamlService.createClientSessionForIdpInitiatedSso(session, realmModel, oClient.get(), null);
+
+        return ParsedCodeContext.clientSessionCode(new ClientSessionCode(session, this.realmModel, clientSession));
+    }
+
+    /**
+     * Returns {@code true} if the client session is defined for the given code
+     * in the current session and for the current realm.
+     * Does <b>not</b> check the session validity. To obtain client session if
+     * and only if it exists and is valid, use {@link ClientSessionCode#parse}.
+     *
+     * @param code
+     * @return
+     */
+    protected boolean isClientSessionRegistered(String code) {
+        if (code == null) {
+            return false;
+        }
+
+        try {
+            return ClientSessionCode.getClientSession(code, this.session, this.realmModel) != null;
+        } catch (RuntimeException e) {
+            return false;
+        }
+    }
+
     private Response checkAccountManagementFailedLinking(ClientSessionModel clientSession, String error, Object... parameters) {
         if (clientSession.getUserSession() != null && clientSession.getClient() != null && clientSession.getClient().getClientId().equals(ACCOUNT_MANAGEMENT_CLIENT_ID)) {
 
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlIdPInitiatedSsoTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlIdPInitiatedSsoTest.java
new file mode 100644
index 0000000..4bb367f
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlIdPInitiatedSsoTest.java
@@ -0,0 +1,132 @@
+/*
+ * To change this license header, choose License Headers in Project Properties.
+ * To change this template file, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package org.keycloak.testsuite.broker;
+
+import org.keycloak.admin.client.resource.UsersResource;
+import org.keycloak.common.util.StreamUtil;
+import org.keycloak.common.util.StringPropertyReplacer;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.IdentityProviderRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest;
+import org.keycloak.testsuite.adapter.page.SalesPostServlet;
+import org.keycloak.testsuite.adapter.servlet.SendUsernameServlet;
+import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.pages.UpdateAccountInformationPage;
+import org.keycloak.testsuite.util.IOUtil;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.graphene.page.Page;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.Test;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.support.ui.ExpectedCondition;
+import org.openqa.selenium.support.ui.WebDriverWait;
+
+import static org.keycloak.testsuite.broker.BrokerTestConstants.*;
+import static org.hamcrest.Matchers.*;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class KcSamlIdPInitiatedSsoTest extends AbstractKeycloakTest {
+
+    private static final String PROVIDER_REALM_USER_NAME = "test";
+    private static final String PROVIDER_REALM_USER_PASSWORD = "test";
+
+    @Page
+    protected LoginPage accountLoginPage;
+
+    @Page
+    protected UpdateAccountInformationPage updateAccountInformationPage;
+
+    protected String getAuthRoot() {
+        return suiteContext.getAuthServerInfo().getContextRoot().toString();
+    }
+
+    private RealmRepresentation loadFromClasspath(String fileName, Properties properties) {
+        InputStream is = KcSamlIdPInitiatedSsoTest.class.getResourceAsStream(fileName);
+        try {
+            String template = StreamUtil.readString(is);
+            String realmString = StringPropertyReplacer.replaceProperties(template, properties);
+            return IOUtil.loadRealm(new ByteArrayInputStream(realmString.getBytes("UTF-8")));
+        } catch (IOException ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
+    @Override
+    public void addTestRealms(List<RealmRepresentation> testRealms) {
+        Properties p = new Properties();
+        p.put("name.realm.provider", REALM_PROV_NAME);
+        p.put("name.realm.consumer", REALM_CONS_NAME);
+        p.put("url.realm.provider", getAuthRoot() + "/auth/realms/" + REALM_PROV_NAME);
+        p.put("url.realm.consumer", getAuthRoot() + "/auth/realms/" + REALM_CONS_NAME);
+        
+        testRealms.add(loadFromClasspath("kc3731-provider-realm.json", p));
+        testRealms.add(loadFromClasspath("kc3731-broker-realm.json", p));
+    }
+
+    @Test
+    public void testProviderIdpInitiatedLogin() {
+        driver.navigate().to(getSamlIdpInitiatedUrl(REALM_PROV_NAME, "samlbroker"));
+
+        waitForPage("log in to");
+
+        Assert.assertThat("Driver should be on the provider realm page right now",
+                driver.getCurrentUrl(), containsString("/auth/realms/" + REALM_PROV_NAME + "/"));
+
+        log.debug("Logging in");
+        accountLoginPage.login(PROVIDER_REALM_USER_NAME, PROVIDER_REALM_USER_PASSWORD);
+
+        waitForPage("update account information");
+
+        Assert.assertTrue(updateAccountInformationPage.isCurrent());
+        Assert.assertThat("We must be on consumer realm right now",
+                driver.getCurrentUrl(), containsString("/auth/realms/" + REALM_CONS_NAME + "/"));
+
+        log.debug("Updating info on updateAccount page");
+        updateAccountInformationPage.updateAccountInformation("mytest", "test@localhost", "Firstname", "Lastname");
+
+        UsersResource consumerUsers = adminClient.realm(REALM_CONS_NAME).users();
+
+        int userCount = consumerUsers.count();
+        Assert.assertTrue("There must be at least one user", userCount > 0);
+
+        List<UserRepresentation> users = consumerUsers.search("", 0, userCount);
+
+        boolean isUserFound = users.stream().anyMatch(user -> user.getUsername().equals("mytest") && user.getEmail().equals("test@localhost"));
+        Assert.assertTrue("There must be user " + "mytest" + " in realm " + REALM_CONS_NAME, isUserFound);
+
+        Assert.assertThat(driver.findElement(org.openqa.selenium.By.tagName("form")).getAttribute("action"), containsString("http://localhost:18080/sales-post-enc/"));
+    }
+
+    private String getSamlIdpInitiatedUrl(String realmName, String samlIdpInitiatedSsoUrlName) {
+        return getAuthRoot() + "/auth/realms/" + realmName + "/protocol/saml/clients/" + samlIdpInitiatedSsoUrlName;
+    }
+
+    private void waitForPage(final String title) {
+        WebDriverWait wait = new WebDriverWait(driver, 5);
+
+        ExpectedCondition<Boolean> condition = (WebDriver input) -> input.getTitle().toLowerCase().contains(title);
+
+        wait.until(condition);
+    }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/org/keycloak/testsuite/broker/kc3731-broker-realm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/org/keycloak/testsuite/broker/kc3731-broker-realm.json
new file mode 100644
index 0000000..6e5c7e0
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/org/keycloak/testsuite/broker/kc3731-broker-realm.json
@@ -0,0 +1,64 @@
+{
+  "id" : "${name.realm.consumer}",
+  "realm" : "${name.realm.consumer}",
+  "enabled" : true,
+  "sslRequired" : "external",
+  "roles" : {
+    "client" : {
+      "http://localhost:18080/sales-post-enc/" : [ {
+        "name" : "manager"
+      } ]
+    }
+  },
+  "clients" : [ {
+    "clientId": "http://localhost:18080/sales-post-enc/",
+    "enabled": true,
+    "protocol": "saml",
+    "fullScopeAllowed": true,
+    "redirectUris": [
+      "http://localhost:18080/sales-post-enc/*"
+    ],
+    "attributes": {
+      "saml.authnstatement": "true",
+      "saml.client.signature": "true",
+      "saml.encrypt": "false",
+      "saml.server.signature": "true",
+      "saml.signature.algorithm": "RSA_SHA512",
+      "saml.signing.certificate": "MIIB1DCCAT0CBgFJGVacCDANBgkqhkiG9w0BAQsFADAwMS4wLAYDVQQDEyVodHRwOi8vbG9jYWxob3N0OjgwODAvc2FsZXMtcG9zdC1lbmMvMB4XDTE0MTAxNjE0MjA0NloXDTI0MTAxNjE0MjIyNlowMDEuMCwGA1UEAxMlaHR0cDovL2xvY2FsaG9zdDo4MDgwL3NhbGVzLXBvc3QtZW5jLzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA2+5MCT5BnVN+IYnKZcH6ev1pjXGi4feE0nOycq/VJ3aeaZMi4G9AxOxCBPupErOC7Kgm/Bw5AdJyw+Q12wSRXfJ9FhqCrLXpb7YOhbVSTJ8De5O8mW35DxAlh/cxe9FXjqPb286wKTUZ3LfGYR+X235UQeCTAPS/Ufi21EXaEikCAwEAATANBgkqhkiG9w0BAQsFAAOBgQBMrfGD9QFfx5v7ld/OAto5rjkTe3R1Qei8XRXfcs83vLaqEzjEtTuLGrJEi55kXuJgBpVmQpnwCCkkjSy0JxbqLDdVi9arfWUxEGmOr01ZHycELhDNaQcFqVMPr5kRHIHgktT8hK2IgCvd3Fy9/JCgUgCPxKfhwecyEOKxUc857g==",
+      "saml.signing.private.key": "MIICXQIBAAKBgQDb7kwJPkGdU34hicplwfp6/WmNcaLh94TSc7Jyr9Undp5pkyLgb0DE7EIE+6kSs4LsqCb8HDkB0nLD5DXbBJFd8n0WGoKstelvtg6FtVJMnwN7k7yZbfkPECWH9zF70VeOo9vbzrApNRnct8ZhH5fbflRB4JMA9L9R+LbURdoSKQIDAQABAoGBANtbZG9bruoSGp2s5zhzLzd4hczT6Jfk3o9hYjzNb5Z60ymN3Z1omXtQAdEiiNHkRdNxK+EM7TcKBfmoJqcaeTkW8cksVEAW23ip8W9/XsLqmbU2mRrJiKa+KQNDSHqJi1VGyimi4DDApcaqRZcaKDFXg2KDr/Qt5JFD/o9IIIPZAkEA+ZENdBIlpbUfkJh6Ln+bUTss/FZ1FsrcPZWu13rChRMrsmXsfzu9kZUWdUeQ2Dj5AoW2Q7L/cqdGXS7Mm5XhcwJBAOGZq9axJY5YhKrsksvYRLhQbStmGu5LG75suF+rc/44sFq+aQM7+oeRr4VY88Mvz7mk4esdfnk7ae+cCazqJvMCQQCx1L1cZw3yfRSn6S6u8XjQMjWE/WpjulujeoRiwPPY9WcesOgLZZtYIH8nRL6ehEJTnMnahbLmlPFbttxPRUanAkA11MtSIVcKzkhp2KV2ipZrPJWwI18NuVJXb+3WtjypTrGWFZVNNkSjkLnHIeCYlJIGhDd8OL9zAiBXEm6kmgLNAkBWAg0tK2hCjvzsaA505gWQb4X56uKWdb0IzN+fOLB3Qt7+fLqbVQNQoNGzqey6B4MoS1fUKAStqdGTFYPG/+9t",
+      "saml_idp_initiated_sso_url_name" : "sales"
+    },
+    "baseUrl": "http://localhost:18080/sales-post-enc/",
+    "adminUrl": "http://localhost:18080/sales-post-enc/saml"
+  } ],
+  "identityProviders" : [ {
+    "alias" : "saml-leaf",
+    "providerId" : "saml",
+    "enabled" : true,
+    "updateProfileFirstLoginMode" : "on",
+    "trustEmail" : false,
+    "storeToken" : false,
+    "addReadTokenRoleOnCreate" : false,
+    "authenticateByDefault" : false,
+    "firstBrokerLoginFlowAlias" : "first broker login",
+    "config" : {
+      "nameIDPolicyFormat" : "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent",
+      "postBindingAuthnRequest" : "true",
+      "postBindingResponse" : "true",
+      "singleLogoutServiceUrl" : "${url.realm.provider}/protocol/saml",
+      "singleSignOnServiceUrl" : "${url.realm.provider}/protocol/saml",
+      "validateSignature" : "false",
+      "wantAuthnRequestsSigned" : "false"
+    }
+  } ],
+  "identityProviderMappers" : [ {
+    "name" : "manager-role",
+    "identityProviderAlias" : "saml-leaf",
+    "identityProviderMapper" : "saml-role-idp-mapper",
+    "config" : {
+      "attribute.value" : "manager",
+      "role" : "http://localhost:18080/sales-post-enc/.manager",
+      "attribute.name" : "Role"
+    }
+  } ]
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/org/keycloak/testsuite/broker/kc3731-provider-realm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/org/keycloak/testsuite/broker/kc3731-provider-realm.json
new file mode 100644
index 0000000..8804a36
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/org/keycloak/testsuite/broker/kc3731-provider-realm.json
@@ -0,0 +1,49 @@
+{
+  "id" : "${name.realm.provider}",
+  "realm" : "${name.realm.provider}",
+  "enabled" : true,
+  "sslRequired" : "external",
+  "roles" : {
+    "client" : {
+      "${url.realm.consumer}" : [ {
+        "name" : "manager"
+      } ]
+    }
+  },
+  "clients" : [ {
+    "clientId": "${url.realm.consumer}",
+    "enabled": true,
+    "protocol": "saml",
+    "fullScopeAllowed": true,
+    "redirectUris": [
+      "${url.realm.consumer}/broker/saml-leaf/endpoint"
+    ],
+    "attributes" : {
+      "saml.assertion.signature" : "false",
+      "saml.authnstatement" : "true",
+      "saml.client.signature" : "false",
+      "saml.encrypt" : "false",
+      "saml.force.post.binding" : "true",
+      "saml.server.signature" : "false",
+      "saml_assertion_consumer_url_post" : "${url.realm.consumer}/broker/saml-leaf/endpoint/clients/sales",
+      "saml_force_name_id_format" : "false",
+      "saml_idp_initiated_sso_url_name" : "samlbroker",
+      "saml_name_id_format" : "persistent",
+      "saml_single_logout_service_url_post" : "${url.realm.consumer}/broker/saml-leaf/endpoint"
+    }
+  } ],
+  "users" : [ {
+    "username" : "test",
+    "enabled" : true,
+    "email" : "a@localhost",
+    "firstName": "b",
+    "lastName": "c",
+    "credentials" : [ {
+      "type" : "password",
+      "value" : "test"
+    } ],
+    "clientRoles" : {
+      "${url.realm.consumer}" : [ "manager" ]
+    }
+  } ]
+}