keycloak-aplcache
Merge pull request #3553 from hmlnarik/KEYCLOAK-3731-alternative KEYCLOAK-3731 …
11/28/2016 2:23:26 PM
Changes
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlIdPInitiatedSsoTest.java 132(+132 -0)
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" ]
+ }
+ } ]
+}