KEYCLOAK-15270 Account REST API doesn't verify audience

This commit is contained in:
vmuzikar 2020-09-07 10:49:50 +02:00 committed by Bruno Oliveira da Silva
parent b62d68a591
commit a9a719b88c
13 changed files with 225 additions and 53 deletions

View file

@ -15,7 +15,7 @@ public class ExampleRestResource {
public ExampleRestResource(KeycloakSession session) {
this.session = session;
this.auth = new AppAuthManager().authenticateBearerToken(session, session.getContext().getRealm());
this.auth = new AppAuthManager.BearerTokenAuthenticator(session).authenticate();
}
@Path("companies")

View file

@ -30,9 +30,7 @@ import org.keycloak.services.managers.AuthenticationManager.AuthResult;
public class Tokens {
public static AccessToken getAccessToken(KeycloakSession keycloakSession) {
AppAuthManager authManager = new AppAuthManager();
KeycloakContext context = keycloakSession.getContext();
AuthResult authResult = authManager.authenticateBearerToken(keycloakSession, context.getRealm(), context.getUri(), context.getConnection(), context.getRequestHeaders());
AuthResult authResult = new AppAuthManager.BearerTokenAuthenticator(keycloakSession).authenticate();
if (authResult != null) {
return authResult.getToken();
@ -42,9 +40,9 @@ public class Tokens {
}
public static AccessToken getAccessToken(String accessToken, KeycloakSession keycloakSession) {
AppAuthManager authManager = new AppAuthManager();
KeycloakContext context = keycloakSession.getContext();
AuthResult authResult = authManager.authenticateBearerToken(accessToken, keycloakSession, context.getRealm(), context.getUri(), context.getConnection(), context.getRequestHeaders());
AuthResult authResult = new AppAuthManager.BearerTokenAuthenticator(keycloakSession)
.setTokenString(accessToken)
.authenticate();
if (authResult != null) {
return authResult.getToken();

View file

@ -84,7 +84,6 @@ import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.Urls;
import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.clientpolicy.DefaultClientPolicyManager;
import org.keycloak.services.clientpolicy.TokenRefreshContext;
import org.keycloak.services.clientpolicy.TokenRequestContext;
import org.keycloak.services.managers.AppAuthManager;
@ -797,7 +796,7 @@ public class TokenEndpoint {
}
AuthenticationManager.AuthResult authResult = AuthenticationManager.verifyIdentityToken(session, realm, session.getContext().getUri(), clientConnection, true, true, false, subjectToken, headers);
AuthenticationManager.AuthResult authResult = AuthenticationManager.verifyIdentityToken(session, realm, session.getContext().getUri(), clientConnection, true, true, null, false, subjectToken, headers);
if (authResult == null) {
event.detail(Details.REASON, "subject_token validation failure");
event.error(Errors.INVALID_TOKEN);

View file

@ -53,7 +53,7 @@ public class AppAuthManager extends AuthenticationManager {
*
* @return the token string or {@literal null}
*/
private String extractTokenStringFromAuthHeader(String authHeader) {
private static String extractTokenStringFromAuthHeader(String authHeader) {
if (authHeader == null) {
return null;
@ -83,7 +83,7 @@ public class AppAuthManager extends AuthenticationManager {
* @param headers
* @return the token string or {@literal null} if the Authorization header is not of type Bearer, or the token string is missing.
*/
public String extractAuthorizationHeaderTokenOrReturnNull(HttpHeaders headers) {
public static String extractAuthorizationHeaderTokenOrReturnNull(HttpHeaders headers) {
String authHeader = headers.getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
return extractTokenStringFromAuthHeader(authHeader);
}
@ -95,7 +95,7 @@ public class AppAuthManager extends AuthenticationManager {
* @return the token string or {@literal null} of the Authorization header is missing
* @throws NotAuthorizedException if the Authorization header is not of type Bearer, or the token string is missing.
*/
public String extractAuthorizationHeaderToken(HttpHeaders headers) {
public static String extractAuthorizationHeaderToken(HttpHeaders headers) {
String authHeader = headers.getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (authHeader == null) {
return null;
@ -107,23 +107,65 @@ public class AppAuthManager extends AuthenticationManager {
return tokenString;
}
public AuthResult authenticateBearerToken(KeycloakSession session, RealmModel realm) {
KeycloakContext ctx = session.getContext();
return authenticateBearerToken(session, realm, ctx.getUri(), ctx.getConnection(), ctx.getRequestHeaders());
}
public static class BearerTokenAuthenticator {
private KeycloakSession session;
private RealmModel realm;
private UriInfo uriInfo;
private ClientConnection connection;
private HttpHeaders headers;
private String tokenString;
private String audience;
public AuthResult authenticateBearerToken(KeycloakSession session) {
return authenticateBearerToken(session, session.getContext().getRealm(), session.getContext().getUri(), session.getContext().getConnection(), session.getContext().getRequestHeaders());
}
public BearerTokenAuthenticator(KeycloakSession session) {
this.session = session;
}
public AuthResult authenticateBearerToken(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers) {
return authenticateBearerToken(extractAuthorizationHeaderToken(headers), session, realm, uriInfo, connection, headers);
}
public BearerTokenAuthenticator setSession(KeycloakSession session) {
this.session = session;
return this;
}
public AuthResult authenticateBearerToken(String tokenString, KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers) {
if (tokenString == null) return null;
AuthResult authResult = verifyIdentityToken(session, realm, uriInfo, connection, true, true, false, tokenString, headers);
return authResult;
public BearerTokenAuthenticator setRealm(RealmModel realm) {
this.realm = realm;
return this;
}
public BearerTokenAuthenticator setUriInfo(UriInfo uriInfo) {
this.uriInfo = uriInfo;
return this;
}
public BearerTokenAuthenticator setConnection(ClientConnection connection) {
this.connection = connection;
return this;
}
public BearerTokenAuthenticator setHeaders(HttpHeaders headers) {
this.headers = headers;
return this;
}
public BearerTokenAuthenticator setTokenString(String tokenString) {
this.tokenString = tokenString;
return this;
}
public BearerTokenAuthenticator setAudience(String audience) {
this.audience = audience;
return this;
}
public AuthResult authenticate() {
KeycloakContext ctx = session.getContext();
if (realm == null) realm = ctx.getRealm();
if (uriInfo == null) uriInfo = ctx.getUri();
if (connection == null) connection = ctx.getConnection();
if (headers == null) headers = ctx.getRequestHeaders();
if (tokenString == null) tokenString = extractAuthorizationHeaderToken(headers);
// audience can be null
return verifyIdentityToken(session, realm, uriInfo, connection, true, true, audience, false, tokenString, headers);
}
}
}

View file

@ -779,7 +779,7 @@ public class AuthenticationManager {
}
String tokenString = cookie.getValue();
AuthResult authResult = verifyIdentityToken(session, realm, session.getContext().getUri(), session.getContext().getConnection(), checkActive, false, true, tokenString, session.getContext().getRequestHeaders(), VALIDATE_IDENTITY_COOKIE);
AuthResult authResult = verifyIdentityToken(session, realm, session.getContext().getUri(), session.getContext().getConnection(), checkActive, false, null, true, tokenString, session.getContext().getRequestHeaders(), VALIDATE_IDENTITY_COOKIE);
if (authResult == null) {
expireIdentityCookie(realm, session.getContext().getUri(), session.getContext().getConnection());
expireOldIdentityCookie(realm, session.getContext().getUri(), session.getContext().getConnection());
@ -1261,7 +1261,7 @@ public class AuthenticationManager {
}
public static AuthResult verifyIdentityToken(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection connection, boolean checkActive, boolean checkTokenType,
boolean isCookie, String tokenString, HttpHeaders headers, Predicate<? super AccessToken>... additionalChecks) {
String checkAudience, boolean isCookie, String tokenString, HttpHeaders headers, Predicate<? super AccessToken>... additionalChecks) {
try {
TokenVerifier<AccessToken> verifier = TokenVerifier.create(tokenString, AccessToken.class)
.withDefaultChecks()
@ -1269,6 +1269,11 @@ public class AuthenticationManager {
.checkActive(checkActive)
.checkTokenType(checkTokenType)
.withChecks(additionalChecks);
if (checkAudience != null) {
verifier.audience(checkAudience);
}
String kid = verifier.getHeader().getKeyId();
String algorithm = verifier.getHeader().getAlgorithm().name();

View file

@ -448,8 +448,11 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
this.event.event(EventType.IDENTITY_PROVIDER_RETRIEVE_TOKEN);
try {
AppAuthManager authManager = new AppAuthManager();
AuthenticationManager.AuthResult authResult = authManager.authenticateBearerToken(this.session, this.realmModel, this.session.getContext().getUri(), this.clientConnection, this.request.getHttpHeaders());
AuthenticationManager.AuthResult authResult = new AppAuthManager.BearerTokenAuthenticator(session)
.setRealm(realmModel)
.setConnection(clientConnection)
.setHeaders(request.getHttpHeaders())
.authenticate();
if (authResult != null) {
AccessToken token = authResult.getToken();

View file

@ -114,7 +114,10 @@ public class AccountLoader {
}
private AccountRestService getAccountRestService(ClientModel client, String versionStr) {
AuthenticationManager.AuthResult authResult = new AppAuthManager().authenticateBearerToken(session);
AuthenticationManager.AuthResult authResult = new AppAuthManager.BearerTokenAuthenticator(session)
.setAudience(client.getClientId())
.authenticate();
if (authResult == null) {
throw new NotAuthorizedException("Bearer token required");
}

View file

@ -87,12 +87,10 @@ public class AdminConsole {
@Context
protected Providers providers;
protected AppAuthManager authManager;
protected RealmModel realm;
public AdminConsole(RealmModel realm) {
this.realm = realm;
this.authManager = new AppAuthManager();
}
public static class WhoAmI {
@ -195,7 +193,12 @@ public class AdminConsole {
@NoCache
public Response whoAmI(final @Context HttpHeaders headers) {
RealmManager realmManager = new RealmManager(session);
AuthenticationManager.AuthResult authResult = authManager.authenticateBearerToken(session, realm, session.getContext().getUri(), clientConnection, headers);
AuthenticationManager.AuthResult authResult = new AppAuthManager.BearerTokenAuthenticator(session)
.setRealm(realm)
.setConnection(clientConnection)
.setHeaders(headers)
.authenticate();
if (authResult == null) {
return Response.status(401).build();
}

View file

@ -72,7 +72,6 @@ public class AdminRoot {
@Context
protected HttpResponse response;
protected AppAuthManager authManager;
protected TokenManager tokenManager;
@Context
@ -80,7 +79,6 @@ public class AdminRoot {
public AdminRoot() {
this.tokenManager = new TokenManager();
this.authManager = new AppAuthManager();
}
public static UriBuilder adminBaseUrl(UriInfo uriInfo) {
@ -153,7 +151,7 @@ public class AdminRoot {
protected AdminAuth authenticateRealmAdminRequest(HttpHeaders headers) {
String tokenString = authManager.extractAuthorizationHeaderToken(headers);
String tokenString = AppAuthManager.extractAuthorizationHeaderToken(headers);
if (tokenString == null) throw new NotAuthorizedException("Bearer");
AccessToken token;
try {
@ -169,7 +167,13 @@ public class AdminRoot {
throw new NotAuthorizedException("Unknown realm in token");
}
session.getContext().setRealm(realm);
AuthenticationManager.AuthResult authResult = authManager.authenticateBearerToken(session, realm, session.getContext().getUri(), clientConnection, headers);
AuthenticationManager.AuthResult authResult = new AppAuthManager.BearerTokenAuthenticator(session)
.setRealm(realm)
.setConnection(clientConnection)
.setHeaders(headers)
.authenticate();
if (authResult == null) {
logger.debug("Token not valid");
throw new NotAuthorizedException("Bearer");

View file

@ -32,7 +32,7 @@ public class ExampleRestResource {
public ExampleRestResource(KeycloakSession session) {
this.session = session;
this.auth = new AppAuthManager().authenticateBearerToken(session, session.getContext().getRealm());
this.auth = new AppAuthManager.BearerTokenAuthenticator(session).authenticate();
}
@Path("companies")

View file

@ -1203,7 +1203,7 @@ public class AccountFormServiceTest extends AbstractTestRealmKeycloakTest {
Map<String, AccountApplicationsPage.AppEntry> apps = applicationsPage.getApplications();
Assert.assertThat(apps.keySet(), containsInAnyOrder(
/* "root-url-client", */ "Account", "Account Console", "test-app", "test-app-scope", "third-party", "test-app-authz", "My Named Test App", "Test App Named - ${client_account}", "direct-grant"));
/* "root-url-client", */ "Account", "Account Console", "test-app", "test-app-scope", "third-party", "test-app-authz", "My Named Test App", "Test App Named - ${client_account}", "direct-grant", "custom-audience"));
rsu.add(testRealm().roles().get("user").toRepresentation())
.update();
@ -1211,7 +1211,7 @@ public class AccountFormServiceTest extends AbstractTestRealmKeycloakTest {
driver.navigate().refresh();
apps = applicationsPage.getApplications();
Assert.assertThat(apps.keySet(), containsInAnyOrder(
"root-url-client", "Account", "Account Console", "test-app", "test-app-scope", "third-party", "test-app-authz", "My Named Test App", "Test App Named - ${client_account}", "direct-grant"));
"root-url-client", "Account", "Account Console", "test-app", "test-app-scope", "third-party", "test-app-authz", "My Named Test App", "Test App Named - ${client_account}", "direct-grant", "custom-audience"));
}
}
@ -1230,7 +1230,7 @@ public class AccountFormServiceTest extends AbstractTestRealmKeycloakTest {
Map<String, AccountApplicationsPage.AppEntry> apps = applicationsPage.getApplications();
Assert.assertThat(apps.keySet(), containsInAnyOrder(
"root-url-client", "Account", "Account Console", "test-app", "test-app-scope", "third-party", "test-app-authz", "My Named Test App", "Test App Named - ${client_account}", "direct-grant"));
"root-url-client", "Account", "Account Console", "test-app", "test-app-scope", "third-party", "test-app-authz", "My Named Test App", "Test App Named - ${client_account}", "direct-grant", "custom-audience"));
}
}
@ -1245,7 +1245,7 @@ public class AccountFormServiceTest extends AbstractTestRealmKeycloakTest {
applicationsPage.assertCurrent();
Map<String, AccountApplicationsPage.AppEntry> apps = applicationsPage.getApplications();
Assert.assertThat(apps.keySet(), containsInAnyOrder("root-url-client", "Account", "Account Console", "Broker", "test-app", "test-app-scope", "third-party", "test-app-authz", "My Named Test App", "Test App Named - ${client_account}", "direct-grant"));
Assert.assertThat(apps.keySet(), containsInAnyOrder("root-url-client", "Account", "Account Console", "Broker", "test-app", "test-app-scope", "third-party", "test-app-authz", "My Named Test App", "Test App Named - ${client_account}", "direct-grant", "custom-audience"));
AccountApplicationsPage.AppEntry accountEntry = apps.get("Account");
Assert.assertThat(accountEntry.getRolesAvailable(), containsInAnyOrder(

View file

@ -47,6 +47,7 @@ import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
import org.keycloak.representations.idm.ClientScopeRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.ErrorRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
import org.keycloak.representations.idm.RequiredActionProviderSimpleRepresentation;
@ -73,6 +74,7 @@ import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.keycloak.common.Profile.Feature.ACCOUNT_API;
@ -294,7 +296,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
null, UserModel.RequiredAction.UPDATE_PASSWORD.toString(), false, 1);
CredentialRepresentation password1 = password.getUserCredentials().get(0);
Assert.assertNull(password1.getSecretData());
assertNull(password1.getSecretData());
Assert.assertNotNull(password1.getCredentialData());
AccountCredentialResource.CredentialContainer otp = credentials.get(1);
@ -341,7 +343,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
Assert.assertEquals(1, credentials.size());
password = credentials.get(0);
Assert.assertEquals(PasswordCredentialModel.TYPE, password.getType());
Assert.assertNull(password.getUserCredentials());
assertNull(password.getUserCredentials());
}
@ -452,8 +454,8 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
credentials = getCredentials();
assertExpectedCredentialTypes(credentials, PasswordCredentialModel.TYPE, OTPCredentialModel.TYPE);
AccountCredentialResource.CredentialContainer otpCredential = credentials.get(1);
Assert.assertNull(otpCredential.getCreateAction());
Assert.assertNull(otpCredential.getUpdateAction());
assertNull(otpCredential.getCreateAction());
assertNull(otpCredential.getUpdateAction());
// Revert - re-enable requiredAction and remove OTP credential from the user
setRequiredActionEnabledStatus(UserModel.RequiredAction.CONFIGURE_TOTP.name(), true);
@ -578,7 +580,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
public void listApplications() throws Exception {
oauth.clientId("in-use-client");
OAuthClient.AccessTokenResponse tokenResponse = oauth.doGrantAccessTokenRequest("secret1", "view-applications-access", "password");
Assert.assertNull(tokenResponse.getErrorDescription());
assertNull(tokenResponse.getErrorDescription());
TokenUtil token = new TokenUtil("view-applications-access", "password");
List<ClientRepresentation> applications = SimpleHttp
@ -600,7 +602,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
public void listApplicationsFiltered() throws Exception {
oauth.clientId("in-use-client");
OAuthClient.AccessTokenResponse tokenResponse = oauth.doGrantAccessTokenRequest("secret1", "view-applications-access", "password");
Assert.assertNull(tokenResponse.getErrorDescription());
assertNull(tokenResponse.getErrorDescription());
TokenUtil token = new TokenUtil("view-applications-access", "password");
List<ClientRepresentation> applications = SimpleHttp
@ -623,7 +625,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
oauth.clientId("offline-client");
OAuthClient.AccessTokenResponse offlineTokenResponse = oauth.doGrantAccessTokenRequest("secret1", "view-applications-access", "password");
Assert.assertNull(offlineTokenResponse.getErrorDescription());
assertNull(offlineTokenResponse.getErrorDescription());
TokenUtil token = new TokenUtil("view-applications-access", "password");
List<ClientRepresentation> applications = SimpleHttp
@ -687,7 +689,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
public void listApplicationsWithRootUrl() throws Exception {
oauth.clientId("root-url-client");
OAuthClient.AccessTokenResponse tokenResponse = oauth.doGrantAccessTokenRequest("password", "view-applications-access", "password");
Assert.assertNull(tokenResponse.getErrorDescription());
assertNull(tokenResponse.getErrorDescription());
TokenUtil token = new TokenUtil("view-applications-access", "password");
List<ClientRepresentation> applications = SimpleHttp
@ -1100,7 +1102,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
oauth.clientId("offline-client");
OAuthClient.AccessTokenResponse offlineTokenResponse = oauth.doGrantAccessTokenRequest("secret1", "view-applications-access", "password");
Assert.assertNull(offlineTokenResponse.getErrorDescription());
assertNull(offlineTokenResponse.getErrorDescription());
TokenUtil token = new TokenUtil("view-applications-access", "password");
@ -1142,4 +1144,47 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
assertEquals("API version not found", response.asJson().get("error").textValue());
assertEquals(404, response.getStatus());
}
@Test
public void testAudience() throws Exception {
oauth.clientId("custom-audience");
OAuthClient.AccessTokenResponse tokenResponse = oauth.doGrantAccessTokenRequest("password", "test-user@localhost", "password");
assertNull(tokenResponse.getErrorDescription());
SimpleHttp.Response response = SimpleHttp.doGet(getAccountUrl(null), httpClient)
.auth(tokenResponse.getAccessToken())
.header("Accept", "application/json")
.asResponse();
assertEquals(401, response.getStatus());
// update to correct audience
org.keycloak.representations.idm.ClientRepresentation clientRep = testRealm().clients().findByClientId("custom-audience").get(0);
ProtocolMapperRepresentation mapperRep = clientRep.getProtocolMappers().stream().filter(m -> m.getName().equals("aud")).findFirst().orElse(null);
assertNotNull("Audience mapper not found", mapperRep);
mapperRep.getConfig().put("included.custom.audience", "account");
testRealm().clients().get(clientRep.getId()).getProtocolMappers().update(mapperRep.getId(), mapperRep);
tokenResponse = oauth.doGrantAccessTokenRequest("password", "test-user@localhost", "password");
assertNull(tokenResponse.getErrorDescription());
response = SimpleHttp.doGet(getAccountUrl(null), httpClient)
.auth(tokenResponse.getAccessToken())
.header("Accept", "application/json")
.asResponse();
assertEquals(200, response.getStatus());
// remove audience completely
testRealm().clients().get(clientRep.getId()).getProtocolMappers().delete(mapperRep.getId());
tokenResponse = oauth.doGrantAccessTokenRequest("password", "test-user@localhost", "password");
assertNull(tokenResponse.getErrorDescription());
response = SimpleHttp.doGet(getAccountUrl(null), httpClient)
.auth(tokenResponse.getAccessToken())
.header("Accept", "application/json")
.asResponse();
assertEquals(401, response.getStatus());
// custom-audience client is used only in this test so no need to revert the changes
}
}

View file

@ -414,7 +414,77 @@
"enabled": true,
"directAccessGrantsEnabled": true,
"secret": "password",
"webOrigins": [ "http://localtest.me:8180" ]
"webOrigins": [ "http://localtest.me:8180" ],
"protocolMappers": [
{
"name": "aud-account",
"protocol": "openid-connect",
"protocolMapper": "oidc-audience-mapper",
"config": {
"included.client.audience": "account",
"id.token.claim": "true",
"access.token.claim": "true"
}
},
{
"name": "aud-admin",
"protocol": "openid-connect",
"protocolMapper": "oidc-audience-mapper",
"config": {
"included.client.audience": "security-admin-console",
"id.token.claim": "true",
"access.token.claim": "true"
}
}
]
},
{
"clientId": "custom-audience",
"enabled": true,
"directAccessGrantsEnabled": true,
"secret": "password",
"protocolMappers": [
{
"name": "aud",
"protocol": "openid-connect",
"protocolMapper": "oidc-audience-mapper",
"config": {
"id.token.claim": "true",
"access.token.claim": "true",
"included.custom.audience": "foo-bar"
}
},
{
"name": "client roles",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-client-role-mapper",
"config": {
"user.attribute": "foo",
"access.token.claim": "true",
"claim.name": "resource_access.${client_id}.roles",
"jsonType.label": "String",
"multivalued": "true"
}
},
{
"name": "realm roles",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-realm-role-mapper",
"config": {
"user.attribute": "foo",
"access.token.claim": "true",
"claim.name": "realm_access.roles",
"jsonType.label": "String",
"multivalued": "true"
}
}
],
"defaultClientScopes": [
"web-origins",
"profile",
"email"
]
}
],
"roles" : {