From 65b269cd543a47e2496ad76e931aea30ebcc0281 Mon Sep 17 00:00:00 2001 From: Hynek Mlnarik Date: Wed, 16 Nov 2016 13:41:03 +0100 Subject: [PATCH] KEYCLOAK-3731 Provide functionality for IdP-initiated SSO for broker A SAML brokered IdP can send unsolicited login response to the broker. This commit adds a new GET/POST endpoint under [broker SAML endpoint]/clients/{client_id}. Broken will respond to submission to this new endpoint by looking up a SAML client with URL name equal to client_id, and if found, it performs IdP-initiated SSO to that client. --- .../keycloak/broker/saml/SAMLEndpoint.java | 41 +++++- .../keycloak/protocol/saml/SamlService.java | 22 ++- .../resources/IdentityBrokerService.java | 61 +++++++- .../broker/KcSamlIdPInitiatedSsoTest.java | 132 ++++++++++++++++++ .../testsuite/broker/kc3731-broker-realm.json | 64 +++++++++ .../broker/kc3731-provider-realm.json | 49 +++++++ 6 files changed, 358 insertions(+), 11 deletions(-) create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlIdPInitiatedSsoTest.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/resources/org/keycloak/testsuite/broker/kc3731-broker-realm.json create mode 100644 testsuite/integration-arquillian/tests/base/src/test/resources/org/keycloak/testsuite/broker/kc3731-provider-realm.json 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 0ef527629d..951d03ae8c 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 14c550396b..c404ef8eee 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 a8c4cc4040..b1f4587e28 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 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 not 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 0000000000..4bb367fce8 --- /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 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 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 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 0000000000..6e5c7e06ce --- /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 0000000000..8804a367c1 --- /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" ] + } + } ] +}