keycloak-memoizeit

Changes

Details

diff --git a/core/src/main/java/org/keycloak/representations/JsonWebToken.java b/core/src/main/java/org/keycloak/representations/JsonWebToken.java
index 043f659..42dd196 100755
--- a/core/src/main/java/org/keycloak/representations/JsonWebToken.java
+++ b/core/src/main/java/org/keycloak/representations/JsonWebToken.java
@@ -30,6 +30,7 @@ import org.keycloak.json.StringOrArrayDeserializer;
 import org.keycloak.json.StringOrArraySerializer;
 
 import java.io.Serializable;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -161,6 +162,24 @@ public class JsonWebToken implements Serializable, Token {
         return this;
     }
 
+    public JsonWebToken addAudience(String audience) {
+        if (this.audience == null) {
+            this.audience = new String[] { audience };
+        } else {
+            // Check if audience is already there
+            for (String aud : this.audience) {
+                if (audience.equals(aud)) {
+                    return this;
+                }
+            }
+
+            String[] newAudience = Arrays.copyOf(this.audience, this.audience.length + 1);
+            newAudience[this.audience.length] = audience;
+            this.audience = newAudience;
+        }
+        return this;
+    }
+
     public String getSubject() {
         return subject;
     }
diff --git a/core/src/test/java/org/keycloak/jose/JsonWebTokenTest.java b/core/src/test/java/org/keycloak/jose/JsonWebTokenTest.java
index 0830cde..0cc33c7 100644
--- a/core/src/test/java/org/keycloak/jose/JsonWebTokenTest.java
+++ b/core/src/test/java/org/keycloak/jose/JsonWebTokenTest.java
@@ -48,6 +48,25 @@ public class JsonWebTokenTest {
     }
 
     @Test
+    public void testAddAudience() throws IOException {
+        // Token with no audience
+        JsonWebToken s = new JsonWebToken();
+        s.addAudience("audience-1");
+        assertArrayEquals(new String[] { "audience-1"}, s.getAudience());
+
+        // Add to existing
+        s.addAudience("audience-2");
+        assertArrayEquals(new String[]{"audience-1", "audience-2"}, s.getAudience());
+
+        s.addAudience("audience-3");
+        assertArrayEquals(new String[]{"audience-1", "audience-2", "audience-3"}, s.getAudience());
+
+        // Add existing. Shouldn't be added as it's already there
+        s.addAudience("audience-2");
+        assertArrayEquals(new String[]{"audience-1", "audience-2", "audience-3"}, s.getAudience());
+    }
+
+    @Test
     public void test() throws IOException {
         JsonWebToken jsonWebToken = new JsonWebToken();
         jsonWebToken.audience("test");
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientScopesResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientScopesResource.java
index f8e5230..c51d7c2 100755
--- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientScopesResource.java
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientScopesResource.java
@@ -17,6 +17,7 @@
 
 package org.keycloak.admin.client.resource;
 
+import org.jboss.resteasy.annotations.cache.NoCache;
 import org.keycloak.representations.idm.ClientScopeRepresentation;
 
 import javax.ws.rs.Consumes;
@@ -25,6 +26,7 @@ import javax.ws.rs.POST;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import java.util.List;
@@ -39,12 +41,24 @@ public interface ClientScopesResource {
 
     @POST
     @Consumes(MediaType.APPLICATION_JSON)
-    public Response create(ClientScopeRepresentation clientScopeRepresentation);
+    Response create(ClientScopeRepresentation clientScopeRepresentation);
 
     @GET
     @Produces(MediaType.APPLICATION_JSON)
-    public List<ClientScopeRepresentation> findAll();
+    List<ClientScopeRepresentation> findAll();
 
 
+    /**
+     * Generate new client scope for specified service client. The "Frontend" clients, who will use this client scope, will be able to
+     * send their access token to authenticate against specified service client
+     *
+     * @param clientId Client ID of service client (typically bearer-only client)
+     * @return
+     */
+    @Path("generate-audience-client-scope")
+    @POST
+    @NoCache
+    Response generateAudienceClientScope(final @QueryParam("clientId") String clientId);
+
 
 }
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AudienceProtocolMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AudienceProtocolMapper.java
new file mode 100644
index 0000000..8cd4eae
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AudienceProtocolMapper.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.protocol.oidc.mappers;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.provider.ProviderConfigProperty;
+import org.keycloak.representations.IDToken;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class AudienceProtocolMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, OIDCIDTokenMapper {
+
+    private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
+
+    private static final String INCLUDED_CLIENT_AUDIENCE = "included.client.audience";
+    private static final String INCLUDED_CLIENT_AUDIENCE_LABEL = "included.client.audience.label";
+    private static final String INCLUDED_CLIENT_AUDIENCE_HELP_TEXT = "included.client.audience.tooltip";
+
+    private static final String INCLUDED_CUSTOM_AUDIENCE = "included.custom.audience";
+    private static final String INCLUDED_CUSTOM_AUDIENCE_LABEL = "included.custom.audience.label";
+    private static final String INCLUDED_CUSTOM_AUDIENCE_HELP_TEXT = "included.custom.audience.tooltip";
+
+    static {
+        ProviderConfigProperty property;
+        property = new ProviderConfigProperty();
+        property.setName(INCLUDED_CLIENT_AUDIENCE);
+        property.setLabel(INCLUDED_CLIENT_AUDIENCE_LABEL);
+        property.setHelpText(INCLUDED_CLIENT_AUDIENCE_HELP_TEXT);
+        property.setType(ProviderConfigProperty.CLIENT_LIST_TYPE);
+        configProperties.add(property);
+
+        property = new ProviderConfigProperty();
+        property.setName(INCLUDED_CUSTOM_AUDIENCE);
+        property.setLabel(INCLUDED_CUSTOM_AUDIENCE_LABEL);
+        property.setHelpText(INCLUDED_CUSTOM_AUDIENCE_HELP_TEXT);
+        property.setType(ProviderConfigProperty.STRING_TYPE);
+        configProperties.add(property);
+
+
+        OIDCAttributeMapperHelper.addIncludeInTokensConfig(configProperties, AudienceProtocolMapper.class);
+
+        // Don't include audience in ID Token by default
+        for (ProviderConfigProperty prop : configProperties) {
+            if (OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN.equals(prop.getName())) {
+                prop.setDefaultValue("false");
+            }
+        }
+    }
+
+    public static final String PROVIDER_ID = "oidc-audience-mapper";
+
+
+    public List<ProviderConfigProperty> getConfigProperties() {
+        return configProperties;
+    }
+
+    @Override
+    public String getId() {
+        return PROVIDER_ID;
+    }
+
+    @Override
+    public String getDisplayType() {
+        return "Audience";
+    }
+
+    @Override
+    public String getDisplayCategory() {
+        return TOKEN_MAPPER_CATEGORY;
+    }
+
+    @Override
+    public String getHelpText() {
+        return "Add specified audience to the audience (aud) field of token";
+    }
+
+    protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession keycloakSession) {
+        String audienceValue = mappingModel.getConfig().get(INCLUDED_CLIENT_AUDIENCE);
+
+        if (audienceValue == null) {
+            // Fallback to custom audience
+            audienceValue = mappingModel.getConfig().get(INCLUDED_CUSTOM_AUDIENCE);
+        }
+
+        if (audienceValue == null) return;
+        token.addAudience(audienceValue);
+    }
+
+    public static ProtocolMapperModel createClaimMapper(String name,
+                                                        String includedClientAudience,
+                                                        String includedCustomAudience,
+                                                        boolean accessToken, boolean idToken) {
+        ProtocolMapperModel mapper = new ProtocolMapperModel();
+        mapper.setName(name);
+        mapper.setProtocolMapper(PROVIDER_ID);
+        mapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+
+        Map<String, String> config = new HashMap<String, String>();
+        if (includedClientAudience != null) {
+            config.put(INCLUDED_CLIENT_AUDIENCE, includedClientAudience);
+        }
+        if (includedCustomAudience != null) {
+            config.put(INCLUDED_CUSTOM_AUDIENCE, includedCustomAudience);
+        }
+
+        if (accessToken) config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true");
+        if (idToken) config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true");
+        mapper.setConfig(config);
+        return mapper;
+    }
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientScopesResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientScopesResource.java
index 81b0c74..91fb49a 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/ClientScopesResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientScopesResource.java
@@ -22,12 +22,17 @@ import org.jboss.resteasy.spi.NotFoundException;
 import org.jboss.resteasy.spi.ResteasyProviderFactory;
 import org.keycloak.events.admin.OperationType;
 import org.keycloak.events.admin.ResourceType;
+import org.keycloak.models.ClientModel;
 import org.keycloak.models.ClientScopeModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.ModelDuplicateException;
+import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
 import org.keycloak.models.utils.ModelToRepresentation;
 import org.keycloak.models.utils.RepresentationToModel;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.protocol.oidc.mappers.AudienceProtocolMapper;
 import org.keycloak.representations.idm.ClientScopeRepresentation;
 import org.keycloak.services.ErrorResponse;
 import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
@@ -38,6 +43,7 @@ import javax.ws.rs.POST;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
@@ -119,6 +125,55 @@ public class ClientScopesResource {
     }
 
     /**
+     * Generate new client scope for specified service client. The "Frontend" clients, who will use this client scope, will be able to
+     * send their access token to authenticate against specified service client
+     *
+     * @param clientId Client ID of service client (typically bearer-only client)
+     * @return
+     */
+    @Path("generate-audience-client-scope")
+    @POST
+    @NoCache
+    public Response generateAudienceClientScope(final @QueryParam("clientId") String clientId) {
+        auth.clients().requireManageClientScopes();
+
+        logger.debugf("Generating audience scope for service client: " + clientId);
+
+        String clientScopeName = clientId;
+        try {
+            ClientModel serviceClient = realm.getClientByClientId(clientId);
+            if (serviceClient == null) {
+                logger.warnf("Referenced service client '%s' doesn't exists", clientId);
+                return ErrorResponse.exists("Referenced service client doesn't exists");
+            }
+
+            ClientScopeModel clientScopeModel = realm.addClientScope(clientScopeName);
+            clientScopeModel.setDescription("Client scope useful for frontend clients, which want to call service " + clientId);
+            clientScopeModel.setProtocol(serviceClient.getProtocol()==null ? OIDCLoginProtocol.LOGIN_PROTOCOL : serviceClient.getProtocol());
+            clientScopeModel.setDisplayOnConsentScreen(true);
+
+            String consentText = serviceClient.getName() != null ? serviceClient.getName() : serviceClient.getClientId();
+            consentText = consentText.substring(0, 1).toUpperCase() + consentText.substring(1);
+            clientScopeModel.setConsentScreenText(consentText);
+
+            // Add audience protocol mapper
+            ProtocolMapperModel audienceMapper = AudienceProtocolMapper.createClaimMapper("Audience for " + clientId, clientId, null,true, false);
+            clientScopeModel.addProtocolMapper(audienceMapper);
+
+            // Add scope to client roles
+            for (RoleModel role : serviceClient.getRoles()) {
+                clientScopeModel.addScopeMapping(role);
+            }
+
+            adminEvent.operation(OperationType.CREATE).resourcePath(session.getContext().getUri()).success();
+
+            return Response.created(session.getContext().getUri().getAbsolutePathBuilder().path(clientScopeModel.getId()).build()).build();
+        } catch (ModelDuplicateException e) {
+            return ErrorResponse.exists("Client Scope " + clientScopeName + " already exists");
+        }
+    }
+
+    /**
      * Base path for managing a specific client scope.
      *
      * @param id id of client scope (not name)
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper b/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper
index 70eaf04..a2ededd 100755
--- a/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper
+++ b/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper
@@ -24,6 +24,7 @@ org.keycloak.protocol.oidc.mappers.HardcodedRole
 org.keycloak.protocol.oidc.mappers.RoleNameMapper
 org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper
 org.keycloak.protocol.oidc.mappers.GroupMembershipMapper
+org.keycloak.protocol.oidc.mappers.AudienceProtocolMapper
 org.keycloak.protocol.saml.mappers.RoleListMapper
 org.keycloak.protocol.saml.mappers.RoleNameMapper
 org.keycloak.protocol.saml.mappers.HardcodedRole
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java
index 0a23bb1..19b0f21 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java
@@ -36,6 +36,7 @@ import org.keycloak.testsuite.AssertEvents;
 import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
 import org.keycloak.testsuite.admin.ApiUtil;
 import org.keycloak.testsuite.oidc.OIDCScopeTest;
+import org.keycloak.testsuite.oidc.AbstractOIDCScopeTest;
 import org.keycloak.testsuite.util.KeycloakModelUtils;
 import org.keycloak.testsuite.util.OAuthClient.AccessTokenResponse;
 import org.keycloak.testsuite.util.TokenSignatureUtil;
@@ -226,7 +227,7 @@ public class TokenIntrospectionTest extends AbstractTestRealmKeycloakTest {
         assertEquals(loginEvent.getUserId(), rep.getSubject());
 
         // Assert expected scope
-        OIDCScopeTest.assertScopes("openid email profile", rep.getScope());
+        AbstractOIDCScopeTest.assertScopes("openid email profile", rep.getScope());
     }
 
     @Test
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/AbstractOIDCScopeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/AbstractOIDCScopeTest.java
new file mode 100644
index 0000000..3cccfae
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/AbstractOIDCScopeTest.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.oidc;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import org.jboss.arquillian.graphene.page.Page;
+import org.junit.Rule;
+import org.keycloak.events.Details;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.IDToken;
+import org.keycloak.representations.idm.EventRepresentation;
+import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
+import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.pages.AccountApplicationsPage;
+import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
+import org.keycloak.testsuite.pages.AppPage;
+import org.keycloak.testsuite.pages.ErrorPage;
+import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.pages.OAuthGrantPage;
+import org.keycloak.testsuite.util.OAuthClient;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public abstract class AbstractOIDCScopeTest extends AbstractTestRealmKeycloakTest {
+
+    @Rule
+    public AssertEvents events = new AssertEvents(this);
+
+    @Page
+    protected AppPage appPage;
+
+    @Page
+    protected LoginPage loginPage;
+
+    @Page
+    protected AccountUpdateProfilePage profilePage;
+
+    @Page
+    protected OAuthGrantPage grantPage;
+
+    @Page
+    protected AccountApplicationsPage accountAppsPage;
+
+    @Page
+    protected ErrorPage errorPage;
+
+
+    protected AbstractOIDCScopeTest.Tokens sendTokenRequest(EventRepresentation loginEvent, String userId, String expectedScope, String clientId) {
+        String sessionId = loginEvent.getSessionId();
+        String codeId = loginEvent.getDetails().get(Details.CODE_ID);
+
+        String code = new OAuthClient.AuthorizationEndpointResponse(oauth).getCode();
+        OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
+        Assert.assertEquals(200, response.getStatusCode());
+
+        // Test scopes
+        log.info("expectedScopes = " + expectedScope);
+        log.info("responseScopes = " + response.getScope());
+        assertScopes(expectedScope, response.getScope());
+
+        IDToken idToken = oauth.verifyIDToken(response.getIdToken());
+        AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
+
+        // Test scope in the access token
+        assertScopes(expectedScope, accessToken.getScope());
+
+        EventRepresentation codeToTokenEvent = events.expectCodeToToken(codeId, sessionId)
+                .user(userId)
+                .client(clientId)
+                .assertEvent();
+
+        // Test scope in the event
+        assertScopes(expectedScope, codeToTokenEvent.getDetails().get(Details.SCOPE));
+
+        return new AbstractOIDCScopeTest.Tokens(idToken, accessToken, response.getRefreshToken());
+    }
+
+    public static void assertScopes(String expectedScope, String receivedScope) {
+        Collection<String> expectedScopes = Arrays.asList(expectedScope.split(" "));
+        Collection<String> receivedScopes = Arrays.asList(receivedScope.split(" "));
+        Assert.assertTrue("Not matched. expectedScope: " + expectedScope + ", receivedScope: " + receivedScope,
+                expectedScopes.containsAll(receivedScopes) && receivedScopes.containsAll(expectedScopes));
+    }
+
+
+    static class Tokens {
+        final IDToken idToken;
+        final AccessToken accessToken;
+        final String refreshToken;
+
+        private Tokens(IDToken idToken, AccessToken accessToken, String refreshToken) {
+            this.idToken = idToken;
+            this.accessToken = accessToken;
+            this.refreshToken = refreshToken;
+        }
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/AudienceTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/AudienceTest.java
new file mode 100644
index 0000000..ba9186b
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/AudienceTest.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.oidc;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import javax.ws.rs.core.Response;
+
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.Test;
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.admin.client.resource.ClientScopeResource;
+import org.keycloak.events.Details;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.representations.JsonWebToken;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.ClientScopeRepresentation;
+import org.keycloak.representations.idm.EventRepresentation;
+import org.keycloak.representations.idm.ProtocolMapperRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
+import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.admin.ApiUtil;
+import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
+import org.keycloak.testsuite.util.ProtocolMapperUtil;
+import org.keycloak.testsuite.util.UserBuilder;
+
+/**
+ * Test for the 'aud' claim in tokens
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class AudienceTest extends AbstractOIDCScopeTest {
+
+    @Deployment
+    public static WebArchive deploy() {
+        return RunOnServerDeployment.create(OIDCAdvancedRequestParamsTest.class, AbstractTestRealmKeycloakTest.class);
+    }
+
+    private static String userId = KeycloakModelUtils.generateId();
+
+
+    @Override
+    public void configureTestRealm(RealmRepresentation testRealm) {
+        // Create service client with some client role
+        ClientRepresentation client1 = new ClientRepresentation();
+        client1.setClientId("service-client");
+        client1.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+        client1.setBearerOnly(true);
+        client1.setBaseUrl("http://foo/service-client");
+        testRealm.getClients().add(client1);
+
+        RoleRepresentation role1 = new RoleRepresentation();
+        role1.setName("role1");
+        testRealm.getRoles().getClient().put("service-client", Arrays.asList(role1));
+
+        // Create client scope 'audience-scope' and add as optional scope to the 'test-app' client
+        ClientScopeRepresentation clientScope = new ClientScopeRepresentation();
+        clientScope.setName("audience-scope");
+        clientScope.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+        testRealm.setClientScopes(Arrays.asList(clientScope));
+
+        ClientRepresentation testApp = testRealm.getClients().stream().filter((ClientRepresentation client) -> {
+            return "test-app".equals(client.getClientId());
+        }).findFirst().get();
+        testApp.setOptionalClientScopes(Arrays.asList("audience-scope"));
+
+        testApp.setFullScopeAllowed(false);
+
+        // Create sample user
+        UserRepresentation user = UserBuilder.create()
+                .id(userId)
+                .username("john")
+                .enabled(true)
+                .email("john@email.cz")
+                .firstName("John")
+                .lastName("Doe")
+                .password("password")
+                .role("account", "manage-account")
+                .role("account", "view-profile")
+                .role("service-client", "role1")
+                .build();
+        testRealm.getUsers().add(user);
+    }
+
+
+    @Test
+    public void testAudienceProtocolMapperWithClientAudience() throws Exception {
+        // Add audience protocol mapper to the clientScope "audience-scope"
+        ProtocolMapperRepresentation audienceMapper = ProtocolMapperUtil.createAudienceMapper("audience mapper", "service-client",
+                null, true, false);
+        ClientScopeResource clientScope = ApiUtil.findClientScopeByName(testRealm(), "audience-scope");
+        Response resp = clientScope.getProtocolMappers().createMapper(audienceMapper);
+        String mapperId = ApiUtil.getCreatedId(resp);
+        resp.close();
+
+        // Login and check audiences in the token (just accessToken contains it)
+        oauth.scope("openid audience-scope");
+        oauth.doLogin("john", "password");
+        EventRepresentation loginEvent = events.expectLogin()
+                .user(userId)
+                .assertEvent();
+        Tokens tokens = sendTokenRequest(loginEvent, userId,"openid audience-scope", "test-app");
+        // TODO: Frontend client itself should not be in the audiences of access token. Will be fixed in the future
+        assertAudiences(tokens.accessToken, "test-app", "service-client");
+        assertAudiences(tokens.idToken, "test-app");
+
+        // Revert
+        clientScope.getProtocolMappers().delete(mapperId);
+    }
+
+
+    @Test
+    public void testAudienceProtocolMapperWithCustomAudience() throws Exception {
+        // Add audience protocol mapper to the clientScope "audience-scope"
+        ProtocolMapperRepresentation audienceMapper = ProtocolMapperUtil.createAudienceMapper("audience mapper 1", null,
+                "http://host/service/ctx1", true, false);
+        ClientScopeResource clientScope = ApiUtil.findClientScopeByName(testRealm(), "audience-scope");
+        Response resp = clientScope.getProtocolMappers().createMapper(audienceMapper);
+        String mapper1Id = ApiUtil.getCreatedId(resp);
+        resp.close();
+
+        audienceMapper = ProtocolMapperUtil.createAudienceMapper("audience mapper 2", null,
+                "http://host/service/ctx2", true, true);
+        resp = clientScope.getProtocolMappers().createMapper(audienceMapper);
+        String mapper2Id = ApiUtil.getCreatedId(resp);
+        resp.close();
+
+        // Login and check audiences in the token
+        oauth.scope("openid audience-scope");
+        oauth.doLogin("john", "password");
+        EventRepresentation loginEvent = events.expectLogin()
+                .user(userId)
+                .assertEvent();
+        Tokens tokens = sendTokenRequest(loginEvent, userId,"openid audience-scope", "test-app");
+        // TODO: Frontend client itself should not be in the audiences of access token. Will be fixed in the future
+        assertAudiences(tokens.accessToken, "test-app", "http://host/service/ctx1", "http://host/service/ctx2");
+        assertAudiences(tokens.idToken, "test-app", "http://host/service/ctx2");
+
+        // Revert
+        clientScope.getProtocolMappers().delete(mapper1Id);
+        clientScope.getProtocolMappers().delete(mapper2Id);
+    }
+
+
+    @Test
+    public void testAudienceClientScopeGeneration() throws Exception {
+        // Generate the "Audience" client scope for the "service-client" as an audience
+        Response resp = testRealm().clientScopes().generateAudienceClientScope("service-client");
+        String audienceScopeId = ApiUtil.getCreatedId(resp);
+        resp.close();
+
+        // Login and check audiences in the token. It's no audience for "service-client" yet and no clientRoles of "service-client" in the token
+        oauth.scope("openid service-client");
+        oauth.doLogin("john", "password");
+        EventRepresentation loginEvent = events.expectLogin()
+                .user(userId)
+                .assertEvent();
+        Tokens tokens = sendTokenRequest(loginEvent, userId,"openid", "test-app");
+        assertAudiences(tokens.accessToken, "test-app");
+        assertAudiences(tokens.idToken, "test-app");
+        Assert.assertFalse(tokens.accessToken.getResourceAccess().containsKey("service-client"));
+
+        // Logout
+        oauth.doLogout(tokens.refreshToken, "password");
+        events.expectLogout(tokens.idToken.getSessionState())
+                .client("test-app")
+                .user(userId)
+                .removeDetail(Details.REDIRECT_URI).assertEvent();
+
+
+        // Add clientScope to the test-app client
+        ClientResource testApp = ApiUtil.findClientByClientId(testRealm(), "test-app");
+        testApp.addOptionalClientScope(audienceScopeId);
+
+        // Login again and check audiences in the token. Now there is audience for "service-client" and clientRoles of "service-client" in the token
+        oauth.scope("openid service-client");
+        oauth.doLogin("john", "password");
+        loginEvent = events.expectLogin()
+                .user(userId)
+                .assertEvent();
+        tokens = sendTokenRequest(loginEvent, userId,"openid service-client", "test-app");
+        assertAudiences(tokens.accessToken, "test-app", "service-client");
+        assertAudiences(tokens.idToken, "test-app");
+        Assert.assertTrue(tokens.accessToken.getResourceAccess().containsKey("service-client"));
+        Assert.assertNames(tokens.accessToken.getResourceAccess().get("service-client").getRoles(), "role1");
+
+        // Revert
+        testApp.removeOptionalClientScope(audienceScopeId);
+        testRealm().clientScopes().get(audienceScopeId).remove();
+    }
+
+
+
+    private void assertAudiences(JsonWebToken token, String... expectedAudience) {
+        Collection<String> audiences = Arrays.asList(token.getAudience());
+        Collection<String> expectedAudiences = Arrays.asList(expectedAudience);
+        Assert.assertTrue("Not matched. expectedAudiences: " + expectedAudiences + ", audiences: " + audiences,
+                expectedAudiences.containsAll(audiences) && audiences.containsAll(expectedAudiences));
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCScopeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCScopeTest.java
index 905077e..b4a64b8 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCScopeTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCScopeTest.java
@@ -18,17 +18,14 @@
 package org.keycloak.testsuite.oidc;
 
 import java.util.Arrays;
-import java.util.Collection;
 
 import javax.ws.rs.NotFoundException;
 import javax.ws.rs.core.Response;
 
 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.After;
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.keycloak.admin.client.resource.ClientResource;
 import org.keycloak.admin.client.resource.ClientScopeResource;
@@ -50,13 +47,7 @@ import org.keycloak.representations.idm.RoleRepresentation;
 import org.keycloak.representations.idm.UserRepresentation;
 import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
 import org.keycloak.testsuite.Assert;
-import org.keycloak.testsuite.AssertEvents;
 import org.keycloak.testsuite.admin.ApiUtil;
-import org.keycloak.testsuite.pages.AccountApplicationsPage;
-import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
-import org.keycloak.testsuite.pages.AppPage;
-import org.keycloak.testsuite.pages.ErrorPage;
-import org.keycloak.testsuite.pages.LoginPage;
 import org.keycloak.testsuite.pages.OAuthGrantPage;
 import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
 import org.keycloak.testsuite.util.ClientManager;
@@ -70,28 +61,7 @@ import static org.junit.Assert.assertEquals;
  *
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
  */
-public class OIDCScopeTest extends AbstractTestRealmKeycloakTest {
-
-    @Rule
-    public AssertEvents events = new AssertEvents(this);
-
-    @Page
-    protected AppPage appPage;
-
-    @Page
-    protected LoginPage loginPage;
-
-    @Page
-    protected AccountUpdateProfilePage profilePage;
-
-    @Page
-    protected OAuthGrantPage grantPage;
-
-    @Page
-    protected AccountApplicationsPage accountAppsPage;
-
-    @Page
-    protected ErrorPage errorPage;
+public class OIDCScopeTest extends AbstractOIDCScopeTest {
 
     @Deployment
     public static WebArchive deploy() {
@@ -160,7 +130,7 @@ public class OIDCScopeTest extends AbstractTestRealmKeycloakTest {
                 .user(userId)
                 .assertEvent();
 
-        Tokens tokens = sendTokenRequest(loginEvent, "openid email profile", "test-app");
+        Tokens tokens = sendTokenRequest(loginEvent, userId, "openid email profile", "test-app");
         IDToken idToken = tokens.idToken;
 
         assertProfile(idToken, true);
@@ -181,7 +151,7 @@ public class OIDCScopeTest extends AbstractTestRealmKeycloakTest {
         loginEvent = events.expectLogin()
                 .user(userId)
                 .assertEvent();
-        tokens = sendTokenRequest(loginEvent, "openid email profile address phone", "test-app");
+        tokens = sendTokenRequest(loginEvent, userId,"openid email profile address phone", "test-app");
         idToken = tokens.idToken;
 
         assertProfile(idToken, true);
@@ -256,7 +226,7 @@ public class OIDCScopeTest extends AbstractTestRealmKeycloakTest {
                 .user(userId)
                 .assertEvent();
 
-        Tokens tokens = sendTokenRequest(loginEvent, "openid", "test-app");
+        Tokens tokens = sendTokenRequest(loginEvent, userId,"openid", "test-app");
         IDToken idToken = tokens.idToken;
 
         assertProfile(idToken, false);
@@ -277,7 +247,7 @@ public class OIDCScopeTest extends AbstractTestRealmKeycloakTest {
         loginEvent = events.expectLogin()
                 .user(userId)
                 .assertEvent();
-        tokens = sendTokenRequest(loginEvent, "openid profile", "test-app");
+        tokens = sendTokenRequest(loginEvent, userId,"openid profile", "test-app");
         idToken = tokens.idToken;
 
         assertProfile(idToken, true);
@@ -313,7 +283,7 @@ public class OIDCScopeTest extends AbstractTestRealmKeycloakTest {
                 .detail(Details.CONSENT, Details.CONSENT_VALUE_CONSENT_GRANTED)
                 .assertEvent();
 
-        Tokens tokens = sendTokenRequest(loginEvent, "openid email profile", "third-party");
+        Tokens tokens = sendTokenRequest(loginEvent, userId,"openid email profile", "third-party");
         IDToken idToken = tokens.idToken;
 
         assertProfile(idToken, true);
@@ -341,7 +311,7 @@ public class OIDCScopeTest extends AbstractTestRealmKeycloakTest {
                 .detail(Details.CONSENT, Details.CONSENT_VALUE_CONSENT_GRANTED)
                 .user(userId)
                 .assertEvent();
-        tokens = sendTokenRequest(loginEvent, "openid email profile address phone", "third-party");
+        tokens = sendTokenRequest(loginEvent, userId,"openid email profile address phone", "third-party");
         idToken = tokens.idToken;
 
         assertProfile(idToken, true);
@@ -378,7 +348,7 @@ public class OIDCScopeTest extends AbstractTestRealmKeycloakTest {
                 .detail(Details.CONSENT, Details.CONSENT_VALUE_CONSENT_GRANTED)
                 .assertEvent();
 
-        Tokens tokens = sendTokenRequest(loginEvent, "openid email profile", "third-party");
+        Tokens tokens = sendTokenRequest(loginEvent, userId,"openid email profile", "third-party");
         IDToken idToken = tokens.idToken;
 
         assertProfile(idToken, true);
@@ -408,7 +378,7 @@ public class OIDCScopeTest extends AbstractTestRealmKeycloakTest {
                 .detail(Details.CONSENT, Details.CONSENT_VALUE_CONSENT_GRANTED)
                 .assertEvent();
 
-        Tokens tokens = sendTokenRequest(loginEvent, "openid email profile", "third-party");
+        Tokens tokens = sendTokenRequest(loginEvent, userId,"openid email profile", "third-party");
         IDToken idToken = tokens.idToken;
         RefreshToken refreshToken1 = oauth.parseRefreshToken(tokens.refreshToken);
 
@@ -496,7 +466,7 @@ public class OIDCScopeTest extends AbstractTestRealmKeycloakTest {
                 .user(userId)
                 .assertEvent();
 
-        Tokens tokens1 = sendTokenRequest(loginEvent, "openid email profile scope-role-1", "test-app");
+        Tokens tokens1 = sendTokenRequest(loginEvent, userId,"openid email profile scope-role-1", "test-app");
         Assert.assertTrue(tokens1.accessToken.getRealmAccess().isUserInRole("role-1"));
         Assert.assertFalse(tokens1.accessToken.getRealmAccess().isUserInRole("role-2"));
 
@@ -504,7 +474,7 @@ public class OIDCScopeTest extends AbstractTestRealmKeycloakTest {
         oauth.scope("scope-role-2");
         oauth.openLoginForm();
         loginEvent = events.expectLogin().user(userId).removeDetail(Details.USERNAME).client("test-app").assertEvent();
-        Tokens tokens2 = sendTokenRequest(loginEvent, "openid email profile scope-role-2", "test-app");
+        Tokens tokens2 = sendTokenRequest(loginEvent, userId,"openid email profile scope-role-2", "test-app");
         Assert.assertFalse(tokens2.accessToken.getRealmAccess().isUserInRole("role-1"));
         Assert.assertTrue(tokens2.accessToken.getRealmAccess().isUserInRole("role-2"));
 
@@ -529,55 +499,4 @@ public class OIDCScopeTest extends AbstractTestRealmKeycloakTest {
         testApp.removeOptionalClientScope(scope2Id);
     }
 
-
-    protected Tokens sendTokenRequest(EventRepresentation loginEvent, String expectedScope, String clientId) {
-        String sessionId = loginEvent.getSessionId();
-        String codeId = loginEvent.getDetails().get(Details.CODE_ID);
-
-        String code = new OAuthClient.AuthorizationEndpointResponse(oauth).getCode();
-        OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
-        Assert.assertEquals(200, response.getStatusCode());
-
-        // Test scopes
-        log.info("expectedScopes = " + expectedScope);
-        log.info("responseScopes = " + response.getScope());
-        assertScopes(expectedScope, response.getScope());
-
-        IDToken idToken = oauth.verifyIDToken(response.getIdToken());
-        AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
-
-        // Test scope in the access token
-        assertScopes(expectedScope, accessToken.getScope());
-
-        EventRepresentation codeToTokenEvent = events.expectCodeToToken(codeId, sessionId)
-                .user(userId)
-                .client(clientId)
-                .assertEvent();
-
-        // Test scope in the event
-        assertScopes(expectedScope, codeToTokenEvent.getDetails().get(Details.SCOPE));
-
-        return new Tokens(idToken, accessToken, response.getRefreshToken());
-    }
-
-    public static void assertScopes(String expectedScope, String receivedScope) {
-        Collection<String> expectedScopes = Arrays.asList(expectedScope.split(" "));
-        Collection<String> receivedScopes = Arrays.asList(receivedScope.split(" "));
-        Assert.assertTrue("Not matched. expectedScope: " + expectedScope + ", receivedScope: " + receivedScope,
-                expectedScopes.containsAll(receivedScopes) && receivedScopes.containsAll(expectedScopes));
-    }
-
-
-    private static class Tokens {
-        private final IDToken idToken;
-        private final AccessToken accessToken;
-        private final String refreshToken;
-
-        private Tokens(IDToken idToken, AccessToken accessToken, String refreshToken) {
-            this.idToken = idToken;
-            this.accessToken = accessToken;
-            this.refreshToken = refreshToken;
-        }
-    }
-
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ProtocolMapperUtil.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ProtocolMapperUtil.java
index 4a493c0..16e807b 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ProtocolMapperUtil.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ProtocolMapperUtil.java
@@ -3,6 +3,7 @@ package org.keycloak.testsuite.util;
 import org.keycloak.admin.client.resource.ProtocolMappersResource;
 import org.keycloak.models.utils.ModelToRepresentation;
 import org.keycloak.protocol.oidc.mappers.AddressMapper;
+import org.keycloak.protocol.oidc.mappers.AudienceProtocolMapper;
 import org.keycloak.protocol.oidc.mappers.HardcodedClaim;
 import org.keycloak.protocol.oidc.mappers.HardcodedRole;
 import org.keycloak.protocol.oidc.mappers.RoleNameMapper;
@@ -160,4 +161,14 @@ public class ProtocolMapperUtil {
     public static ProtocolMapperRepresentation createPairwiseMapper(String sectorIdentifierUri, String salt) {
         return SHA256PairwiseSubMapper.createPairwiseMapper(sectorIdentifierUri, salt);
     }
+
+    public static ProtocolMapperRepresentation createAudienceMapper(String name,
+                                                                    String includedClientAudience,
+                                                                    String includedCustomAudience,
+                                                                    boolean accessToken, boolean idToken) {
+
+        return ModelToRepresentation.toRepresentation(
+                AudienceProtocolMapper.createClaimMapper(name, includedClientAudience, includedCustomAudience, accessToken, idToken)
+        );
+    }
 }
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index 380c59c..17c5a62 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -9,6 +9,7 @@ displayName=Display name
 displayNameHtml=HTML Display name
 save=Save
 cancel=Cancel
+next=Next
 onText=ON
 offText=OFF
 client=Client
@@ -239,6 +240,10 @@ addressClaim.country.label=User Attribute Name for Country
 addressClaim.country.tooltip=Name of User Attribute, which will be used to map to 'country' subclaim inside 'address' token claim. Defaults to 'country' .
 addressClaim.formatted.label=User Attribute Name for Formatted Address
 addressClaim.formatted.tooltip=Name of User Attribute, which will be used to map to 'formatted' subclaim inside 'address' token claim. Defaults to 'formatted' .
+included.client.audience.label=Included Client Audience
+included.client.audience.tooltip=The Client ID of the specified audience client will be included in audience (aud) field of the token. If there are existing audiences in the token, the specified value is just added to them. It won't override existing audiences.
+included.custom.audience.label=Included Custom Audience
+included.custom.audience.tooltip=This is used just if 'Included Client Audience' is not filled. The specified value will be included in audience (aud) field of the token. If there are existing audiences in the token, the specified value is just added to them. It won't override existing audiences.
 
 # client details
 clients.tooltip=Clients are trusted browser apps and web services in a realm. These clients can request a login. You can also define client specific roles.
@@ -830,6 +835,12 @@ client-storage=Client Storage
 no-client-storage-providers-configured=No client storage providers configured
 client-stores.tooltip=Keycloak can retrieve clients and their details from external stores.
 
+add-client-scope-step-1=Add Client Scope - Step 1
+add-client-scope-step-2=Add Client Scope - Step 2
+client-scope-template=Client Scope Template
+client-scope-template.tooltip=Choose if you want to use some client scope template. Template allows you to easily create client scope with some pre-set options and protocol mappers. It is similar to the Archetype concept in Maven.
+audience-client=Audience
+audience-client.tooltip=Used audience (service client). Newly created client scope will contain audience protocol mapper and role scopes of all client roles of chosen service client. This is useful if you want to invoke the specified audience (service client) from your frontend clients.
 client-scope.name.tooltip=Name of the client scope. Must be unique in the realm. Name shouldn't contain space characters as it's used as value of scope parameter
 client-scope.description.tooltip=Description of the client scope
 client-scope.protocol.tooltip=Which SSO protocol configuration is being supplied by this client scope
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/app.js b/themes/src/main/resources/theme/base/admin/resources/js/app.js
index f03208c..8afc0a8 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/app.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/app.js
@@ -1419,7 +1419,19 @@ module.config([ '$routeProvider', function($routeProvider) {
             },
             controller : 'ClientDetailCtrl'
         })
-        .when('/create/client-scope/:realm', {
+        .when('/create/client-scope/step-1/:realm', {
+            templateUrl : resourceUrl + '/partials/client-scope-create-step-1.html',
+            resolve : {
+                realm : function(RealmLoader) {
+                    return RealmLoader();
+                },
+                clients : function(ClientListLoader) {
+                    return ClientListLoader();
+                }
+            },
+            controller : 'ClientScopeCreateStep1Ctrl'
+        })
+        .when('/create/client-scope/step-2/:realm', {
             templateUrl : resourceUrl + '/partials/client-scope-detail.html',
             resolve : {
                 realm : function(RealmLoader) {
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
index 1301b21..b8ffc66 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
@@ -2588,6 +2588,85 @@ module.controller('ClientScopesRealmDefaultCtrl', function($scope, realm, Realm,
     };
 });
 
+module.controller('ClientScopeCreateStep1Ctrl', function($scope, realm, clients, $route, ClientScopeGenerateAudienceClientScope, Client, $location, $modal, Dialog, Notifications) {
+    console.log('ClientScopeCreateStep1Ctrl');
+
+    $scope.realm = realm;
+    $scope.clientScopeTemplate = "none";
+
+    $scope.serviceClients = [];
+    for (var i = 0; i < clients.length; i++) {
+        if (clients[i].bearerOnly) {
+            $scope.serviceClients.push(clients[i]);
+        }
+    }
+
+    $scope.clientScopeTemplates = [
+        { name: "No template", value:  "none"  },
+        { name: "Audience template", value: "audience" }
+    ];
+
+    $scope.audienceClientUiSelect = {
+        minimumInputLength: 1,
+        delay: 500,
+        allowClear: true,
+        query: function (query) {
+            var data = {results: []};
+            if ('' == query.term.trim()) {
+                query.callback(data);
+                return;
+            }
+            Client.query({realm: $route.current.params.realm, search: query.term.trim(), max: 20}, function(response) {
+                for (i = 0; i < response.length; i++) {
+                    if (response[i].clientId.indexOf(query.term) != -1) {
+                        data.results.push(response[i]);
+                    }
+                }
+                query.callback(data);
+            });
+        },
+        formatResult: function(object, container, query) {
+            object.text = object.clientId;
+            return object.clientId;
+        }
+    };
+
+    $scope.selectedAudienceClient = null;
+
+    $scope.selectAudienceClient = function(audienceClient) {
+
+        if (!audienceClient || !audienceClient.id) {
+            $scope.selectedAudienceClient = null;
+            $scope.audienceClientId = '';
+            return;
+        }
+
+        $scope.audienceClientId = audienceClient.clientId;
+    }
+
+    $scope.next = function() {
+        if ($scope.clientScopeTemplate !== 'audience') {
+            $location.url("/create/client-scope/step-2/" + realm.realm);
+        } else {
+            if (!$scope.audienceClientId) {
+                Notifications.error("You must select audience (service client)");
+            } else {
+                ClientScopeGenerateAudienceClientScope.save({ realm: realm.realm, clientId : $scope.audienceClientId }, function (data, headers) {
+                    $scope.changed = false;
+                    var l = headers().location;
+                    var id = l.substring(l.lastIndexOf("/") + 1);
+                    $location.url("/realms/" + realm.realm + "/client-scopes/" + id);
+                    Notifications.success("The client scope has been created.");
+                });
+            }
+        }
+    };
+
+    $scope.cancel = function() {
+        $location.url("/realms/" + realm.realm + "/client-scopes");
+    };
+});
+
 module.controller('ClientScopeDetailCtrl', function($scope, realm, clientScope, $route, serverInfo, ClientScope, $location, $modal, Dialog, Notifications) {
     $scope.protocols = serverInfo.listProviderIds('login-protocol');
 
@@ -2814,6 +2893,23 @@ module.controller('ClientScopeProtocolMapperCreateCtrl', function($scope, realm,
         mapperTypes: serverInfo.protocolMapperTypes[protocol]
     }
 
+    // apply default configurations on change for selected protocolmapper type.
+    $scope.$watch('model.mapperType', function() {
+        var currentMapperType = $scope.model.mapperType;
+        var defaultConfig = {};
+
+        if (currentMapperType && Array.isArray(currentMapperType.properties)) {
+            for (var i = 0; i < currentMapperType.properties.length; i++) {
+                var property = currentMapperType.properties[i];
+                if (property && property.name && property.defaultValue) {
+                    defaultConfig[property.name] = property.defaultValue;
+                }
+            }
+        }
+
+        $scope.model.mapper.config = defaultConfig;
+    }, true);
+
     $scope.model.mapperType = $scope.model.mapperTypes[0];
 
     $scope.$watch(function() {
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/services.js b/themes/src/main/resources/theme/base/admin/resources/js/services.js
index e63a200..1f3783c 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/services.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/services.js
@@ -1210,6 +1210,13 @@ module.factory('Client', function($resource) {
     });
 });
 
+module.factory('ClientScopeGenerateAudienceClientScope', function($resource) {
+    return $resource(authUrl + '/admin/realms/:realm/client-scopes/generate-audience-client-scope?clientId=:clientId', {
+        realm : '@realm',
+        clientId : "@clientId"
+    });
+});
+
 module.factory('ClientScope', function($resource) {
     return $resource(authUrl + '/admin/realms/:realm/client-scopes/:clientScope', {
         realm : '@realm',
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-scope-create-step-1.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-scope-create-step-1.html
new file mode 100644
index 0000000..09d8a66
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-scope-create-step-1.html
@@ -0,0 +1,46 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+    <ol class="breadcrumb">
+        <li><a href="#/realms/{{realm.realm}}/client-scopes">{{:: 'client-scopes' | translate}}</a></li>
+        <li>{{:: 'add-client-scope-step-1' | translate}}</li>
+    </ol>
+
+    <h1>{{:: 'add-client-scope-step-1' | translate}}</h1>
+
+    <form class="form-horizontal" name="clientForm" novalidate kc-read-only="!access.manageClients">
+        <fieldset class="border-top">
+            <div class="form-group">
+                <label class="col-md-2 control-label" for="clientScopeTemplate">{{:: 'client-scope-template' | translate}}</label>
+                <div class="col-sm-6">
+                    <div>
+                        <select class="form-control" id="clientScopeTemplate"
+                                ng-model="clientScopeTemplate"
+                                ng-options="temp.value as temp.name for temp in clientScopeTemplates">
+                        </select>
+                    </div>
+                </div>
+                <kc-tooltip>{{:: 'client-scope-template.tooltip' | translate}}</kc-tooltip>
+            </div>
+
+            <div class="form-group clearfix" data-ng-show="clientScopeTemplate === 'audience'">
+                <label class="col-md-2 control-label" for="clients">{{:: 'audience-client' | translate}}</label>
+
+                <div class="col-md-6">
+                    <input type="hidden" ui-select2="audienceClientUiSelect" id="clients" data-ng-model="selectedAudienceClient" data-ng-change="selectAudienceClient(selectedAudienceClient);" data-placeholder="{{:: 'authz-select-client' | translate}}...">
+                    </input>
+                </div>
+
+                <kc-tooltip>{{:: 'audience-client.tooltip' | translate}}</kc-tooltip>
+            </div>
+        </fieldset>
+
+        <div class="form-group">
+            <div class="col-md-10 col-md-offset-2" data-ng-show="access.manageClients">
+                <button class="btn btn-primary" data-ng-click="next()">{{:: 'next' | translate}}</button>
+                <button kc-cancel data-ng-click="cancel()">{{:: 'cancel' | translate}}</button>
+            </div>
+        </div>
+    </form>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-scope-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-scope-detail.html
index dcd2826..8daee59 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/client-scope-detail.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-scope-detail.html
@@ -2,7 +2,8 @@
 
     <ol class="breadcrumb">
         <li><a href="#/realms/{{realm.realm}}/client-scopes">{{:: 'client-scopes' | translate}}</a></li>
-        <li data-ng-show="create">{{:: 'add-client-scope' | translate}}</li>
+        <li data-ng-show="create"><a href="#/create/client-scope/step-1/{{realm.realm}}">{{:: 'add-client-scope-step-1' | translate}}</a></li>
+        <li data-ng-show="create">{{:: 'add-client-scope-step-2' | translate}}</li>
         <li data-ng-hide="create">{{clientScope.name}}</li>
     </ol>
 
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-scope-list.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-scope-list.html
index 1f945fc..550aea2 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/client-scope-list.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-scope-list.html
@@ -29,7 +29,7 @@
                     </div>
 
                     <div class="pull-right" data-ng-show="access.manageClients">
-                        <a id="createClient" class="btn btn-default" href="#/create/client-scope/{{realm.realm}}">{{:: 'create' | translate}}</a>
+                        <a id="createClient" class="btn btn-default" href="#/create/client-scope/step-1/{{realm.realm}}">{{:: 'create' | translate}}</a>
                     </div>
                 </div>
             </th>
diff --git a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client-scope.html b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client-scope.html
index 72f59e9..8a6636b 100755
--- a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client-scope.html
+++ b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client-scope.html
@@ -1,6 +1,6 @@
 <div data-ng-controller="ClientScopeTabCtrl">
 
-    <h1 data-ng-show="create">{{:: 'add-client-scope' | translate}}</h1>
+    <h1 data-ng-show="create">{{:: 'add-client-scope-step-2' | translate}}</h1>
     <h1 data-ng-hide="create">
         {{clientScope.name|capitalize}}
         <i id="removeClientScope" class="pficon pficon-delete clickable" data-ng-show="access.manageClients" data-ng-click="removeClientScope()"></i>