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.
This commit is contained in:
Hynek Mlnarik 2016-11-16 13:41:03 +01:00
parent a8de125e26
commit 65b269cd54
6 changed files with 358 additions and 11 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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"
}
} ]
}

View file

@ -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" ]
}
} ]
}