keycloak-aplcache

KEYCLOAK-5490 (#4477)

9/15/2017 6:36:48 AM

Changes

Details

diff --git a/server-spi-private/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java b/server-spi-private/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java
index 231dbc1..cd9a421 100755
--- a/server-spi-private/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java
@@ -94,7 +94,11 @@ public abstract class AbstractIdentityProvider<C extends IdentityProviderModel> 
     }
 
     public Response exchangeNotLinked(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token) {
-        return exchangeErrorResponse(uriInfo, authorizedClient, tokenUserSession, token, "invalid_target");
+        return exchangeErrorResponse(uriInfo, authorizedClient, tokenUserSession, token, "identity provider is not linked");
+    }
+
+    public Response exchangeNotLinkedNoStore(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token) {
+        return exchangeErrorResponse(uriInfo, authorizedClient, tokenUserSession, token, "identity provider is not linked, can only link to current user session");
     }
 
     protected Response exchangeErrorResponse(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, AccessToken token, String reason) {
diff --git a/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java b/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
index 7c754c2..468dc4b 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
@@ -24,7 +24,7 @@ import org.keycloak.broker.provider.AbstractIdentityProvider;
 import org.keycloak.broker.provider.AuthenticationRequest;
 import org.keycloak.broker.provider.BrokeredIdentityContext;
 import org.keycloak.broker.provider.IdentityBrokerException;
-import org.keycloak.broker.provider.TokenExchangeTo;
+import org.keycloak.broker.provider.ExchangeTokenToIdentityProviderToken;
 import org.keycloak.broker.provider.util.SimpleHttp;
 import org.keycloak.common.ClientConnection;
 import org.keycloak.events.Details;
@@ -62,7 +62,7 @@ import java.util.regex.Pattern;
 /**
  * @author Pedro Igor
  */
-public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityProviderConfig> extends AbstractIdentityProvider<C> implements TokenExchangeTo {
+public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityProviderConfig> extends AbstractIdentityProvider<C> implements ExchangeTokenToIdentityProviderToken {
     protected static final Logger logger = Logger.getLogger(AbstractOAuth2IdentityProvider.class);
 
     public static final String OAUTH2_GRANT_TYPE_REFRESH_TOKEN = "refresh_token";
@@ -148,7 +148,7 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
     }
 
     @Override
-    public Response exchangeTo(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token, MultivaluedMap<String, String> params) {
+    public Response exchangeFromToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token, MultivaluedMap<String, String> params) {
         String requestedType = params.getFirst(OAuth2Constants.REQUESTED_TOKEN_TYPE);
         if (requestedType != null && !requestedType.equals(OAuth2Constants.ACCESS_TOKEN_TYPE)) {
             return exchangeUnsupportedRequiredType();
@@ -156,7 +156,7 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
         if (!getConfig().isStoreToken()) {
             String brokerId = tokenUserSession.getNote(Details.IDENTITY_PROVIDER);
             if (brokerId == null || !brokerId.equals(getConfig().getAlias())) {
-                return exchangeNotSupported();
+                return exchangeNotLinkedNoStore(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
             }
             return exchangeSessionToken(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
         } else {
@@ -317,7 +317,9 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
                     BrokeredIdentityContext federatedIdentity = getFederatedIdentity(response);
 
                     if (getConfig().isStoreToken()) {
-                        federatedIdentity.setToken(response);
+                        // make sure that token wasn't already set by getFederatedIdentity();
+                        // want to be able to allow provider to set the token itself.
+                        if (federatedIdentity.getToken() == null)federatedIdentity.setToken(response);
                     }
 
                     federatedIdentity.setIdpConfig(getConfig());
diff --git a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
index 316d493..ca6a76f 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
@@ -231,9 +231,10 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
             return exchangeNotLinked(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
         }
         try {
-            AccessTokenResponse tokenResponse = JsonSerialization.readValue(model.getToken(), AccessTokenResponse.class);
-            Long exp = (Long)tokenResponse.getOtherClaims().get(ACCESS_TOKEN_EXPIRATION);
-            if (exp != null && (long)exp < Time.currentTime()) {
+            String modelTokenString = model.getToken();
+            AccessTokenResponse tokenResponse = JsonSerialization.readValue(modelTokenString, AccessTokenResponse.class);
+            Integer exp = (Integer)tokenResponse.getOtherClaims().get(ACCESS_TOKEN_EXPIRATION);
+            if (exp != null && exp < Time.currentTime()) {
                 if (tokenResponse.getRefreshToken() == null) {
                     return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
                 }
@@ -243,19 +244,20 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
                         .param(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
                         .param(OAUTH2_PARAMETER_CLIENT_SECRET, getConfig().getClientSecret()).asString();
                 if (response.contains("error")) {
+                    logger.debugv("Error refreshing token, refresh token expiration?: {0}", response);
                     model.setToken(null);
                     session.users().updateFederatedIdentity(authorizedClient.getRealm(), tokenSubject, model);
                     return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
                 }
                 AccessTokenResponse newResponse = JsonSerialization.readValue(response, AccessTokenResponse.class);
                 if (newResponse.getExpiresIn() > 0) {
-                    long accessTokenExpiration = Time.currentTime() + newResponse.getExpiresIn();
+                    int accessTokenExpiration = Time.currentTime() + (int)newResponse.getExpiresIn();
                     newResponse.getOtherClaims().put(ACCESS_TOKEN_EXPIRATION, accessTokenExpiration);
                     response = JsonSerialization.writeValueAsString(newResponse);
                 }
                 String oldToken = tokenUserSession.getNote(FEDERATED_ACCESS_TOKEN);
                 if (oldToken != null && oldToken.equals(tokenResponse.getToken())) {
-                    long accessTokenExpiration = newResponse.getExpiresIn() > 0 ? Time.currentTime() + newResponse.getExpiresIn() : 0;
+                    int accessTokenExpiration = newResponse.getExpiresIn() > 0 ? Time.currentTime() + (int)newResponse.getExpiresIn() : 0;
                     tokenUserSession.setNote(FEDERATED_TOKEN_EXPIRATION, Long.toString(accessTokenExpiration));
                     tokenUserSession.setNote(FEDERATED_REFRESH_TOKEN, newResponse.getRefreshToken());
                     tokenUserSession.setNote(FEDERATED_ACCESS_TOKEN, newResponse.getToken());
@@ -302,6 +304,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
                     .param(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
                     .param(OAUTH2_PARAMETER_CLIENT_SECRET, getConfig().getClientSecret()).asString();
             if (response.contains("error")) {
+                logger.debugv("Error refreshing token, refresh token expiration?: {0}", response);
                 return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
             }
             AccessTokenResponse newResponse = JsonSerialization.readValue(response, AccessTokenResponse.class);
@@ -341,13 +344,11 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
             BrokeredIdentityContext identity = extractIdentity(tokenResponse, accessToken, idToken);
 
             if (getConfig().isStoreToken()) {
-                String response1 = response;
                 if (tokenResponse.getExpiresIn() > 0) {
                     long accessTokenExpiration = Time.currentTime() + tokenResponse.getExpiresIn();
                     tokenResponse.getOtherClaims().put(ACCESS_TOKEN_EXPIRATION, accessTokenExpiration);
-                    response1 = JsonSerialization.writeValueAsString(tokenResponse);
+                    response = JsonSerialization.writeValueAsString(tokenResponse);
                 }
-                response = response1;
                 identity.setToken(response);
             }
 
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
index a3d6e0c..2a2f80e 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
@@ -24,7 +24,7 @@ import org.keycloak.OAuth2Constants;
 import org.keycloak.OAuthErrorException;
 import org.keycloak.authentication.AuthenticationProcessor;
 import org.keycloak.broker.provider.IdentityProvider;
-import org.keycloak.broker.provider.TokenExchangeTo;
+import org.keycloak.broker.provider.ExchangeTokenToIdentityProviderToken;
 import org.keycloak.common.ClientConnection;
 import org.keycloak.common.constants.ServiceAccountConstants;
 import org.keycloak.common.util.Base64Url;
@@ -39,7 +39,6 @@ import org.keycloak.models.ClientModel;
 import org.keycloak.models.IdentityProviderModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
-import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.UserSessionModel;
 import org.keycloak.protocol.oidc.OIDCLoginProtocol;
@@ -69,7 +68,6 @@ import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.core.Response;
-import javax.ws.rs.core.UriBuilder;
 import javax.ws.rs.core.UriInfo;
 import java.util.Map;
 import java.util.Objects;
@@ -587,7 +585,7 @@ public class TokenEndpoint {
         String requestedIssuer = formParams.getFirst(OAuth2Constants.REQUESTED_ISSUER);
 
         if (requestedIssuer == null) {
-            return exchangeClientToClient(authResult);
+            return exchangeClientToClient(authResult.getUser(), authResult.getSession());
         } else {
             return exchangeToIdentityProvider(authResult, requestedIssuer);
         }
@@ -601,7 +599,7 @@ public class TokenEndpoint {
         }
 
         IdentityProvider provider = IdentityBrokerService.getIdentityProvider(session, realm, requestedIssuer);
-        if (!(provider instanceof TokenExchangeTo)) {
+        if (!(provider instanceof ExchangeTokenToIdentityProviderToken)) {
             event.error(Errors.UNKNOWN_IDENTITY_PROVIDER);
             throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Issuer does not support token exchange", Response.Status.BAD_REQUEST);
         }
@@ -610,12 +608,12 @@ public class TokenEndpoint {
             event.error(Errors.NOT_ALLOWED);
             throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
         }
-        Response response = ((TokenExchangeTo)provider).exchangeTo(uriInfo, client, authResult.getSession(), authResult.getUser(), authResult.getToken(), formParams);
+        Response response = ((ExchangeTokenToIdentityProviderToken)provider).exchangeFromToken(uriInfo, client, authResult.getSession(), authResult.getUser(), authResult.getToken(), formParams);
         return Cors.add(request, Response.fromResponse(response)).auth().allowedOrigins(uriInfo, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
 
     }
 
-    public Response exchangeClientToClient(AuthenticationManager.AuthResult subject) {
+    protected Response exchangeClientToClient(UserModel targetUser, UserSessionModel targetUserSession) {
         String requestedTokenType = formParams.getFirst(OAuth2Constants.REQUESTED_TOKEN_TYPE);
         if (requestedTokenType == null) {
             requestedTokenType = OAuth2Constants.REFRESH_TOKEN_TYPE;
@@ -653,25 +651,19 @@ public class TokenEndpoint {
         String scope = formParams.getFirst(OAuth2Constants.SCOPE);
 
         AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, targetClient, false);
-        authSession.setAuthenticatedUser(subject.getUser());
+        authSession.setAuthenticatedUser(targetUser);
         authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
         authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
         authSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, scope);
 
-        UserSessionModel userSession = subject.getSession();
-        event.session(userSession);
+        event.session(targetUserSession);
 
         AuthenticationManager.setRolesAndMappersInSession(authSession);
-        AuthenticatedClientSessionModel clientSession = TokenManager.attachAuthenticationSession(session, userSession, authSession);
-
-        // Notes about client details
-        userSession.setNote(ServiceAccountConstants.CLIENT_ID, client.getClientId());
-        userSession.setNote(ServiceAccountConstants.CLIENT_HOST, clientConnection.getRemoteHost());
-        userSession.setNote(ServiceAccountConstants.CLIENT_ADDRESS, clientConnection.getRemoteAddr());
+        AuthenticatedClientSessionModel clientSession = TokenManager.attachAuthenticationSession(this.session, targetUserSession, authSession);
 
-        updateUserSessionFromClientAuth(userSession);
+        updateUserSessionFromClientAuth(targetUserSession);
 
-        TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager.responseBuilder(realm, targetClient, event, session, userSession, clientSession)
+        TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager.responseBuilder(realm, targetClient, event, this.session, targetUserSession, clientSession)
                 .generateAccessToken();
         responseBuilder.getAccessToken().issuedFor(client.getClientId());
 
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissionManagement.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissionManagement.java
index 4dfce43..b35e5b4 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissionManagement.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissionManagement.java
@@ -27,7 +27,7 @@ import org.keycloak.models.ClientModel;
 public interface AdminPermissionManagement {
     public static final String MANAGE_SCOPE = "manage";
     public static final String VIEW_SCOPE = "view";
-    public static final String EXCHANGE_TO_SCOPE="exchange-to";
+    public static final String TOKEN_EXCHANGE ="token-exchange";
 
     ClientModel getRealmManagementClient();
 
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java
index 149b313..1c5978e 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissions.java
@@ -40,7 +40,7 @@ import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Set;
 
-import static org.keycloak.services.resources.admin.permissions.AdminPermissionManagement.EXCHANGE_TO_SCOPE;
+import static org.keycloak.services.resources.admin.permissions.AdminPermissionManagement.TOKEN_EXCHANGE;
 
 /**
  * Manages default policies for all users.
@@ -87,7 +87,7 @@ class ClientPermissions implements ClientPermissionEvaluator,  ClientPermissionM
     }
 
     private String getExchangeToPermissionName(ClientModel client) {
-        return EXCHANGE_TO_SCOPE + ".permission.client." + client.getId();
+        return TOKEN_EXCHANGE + ".permission.client." + client.getId();
     }
 
      private void initialize(ClientModel client) {
@@ -107,7 +107,7 @@ class ClientPermissions implements ClientPermissionEvaluator,  ClientPermissionM
         Scope mapRoleClientScope = root.initializeScope(MAP_ROLES_CLIENT_SCOPE, server);
         Scope mapRoleCompositeScope = root.initializeScope(MAP_ROLES_COMPOSITE_SCOPE, server);
         Scope configureScope = root.initializeScope(CONFIGURE_SCOPE, server);
-        Scope exchangeToScope = root.initializeScope(EXCHANGE_TO_SCOPE, server);
+        Scope exchangeToScope = root.initializeScope(TOKEN_EXCHANGE, server);
 
         String resourceName = getResourceName(client);
         Resource resource = authz.getStoreFactory().getResourceStore().findByName(resourceName, server.getId());
@@ -207,7 +207,7 @@ class ClientPermissions implements ClientPermissionEvaluator,  ClientPermissionM
     }
 
     private Scope exchangeToScope(ResourceServer server) {
-        return authz.getStoreFactory().getScopeStore().findByName(EXCHANGE_TO_SCOPE, server.getId());
+        return authz.getStoreFactory().getScopeStore().findByName(TOKEN_EXCHANGE, server.getId());
     }
 
     private Scope configureScope(ResourceServer server) {
@@ -293,7 +293,7 @@ class ClientPermissions implements ClientPermissionEvaluator,  ClientPermissionM
         scopes.put(MAP_ROLES_SCOPE,  mapRolesPermission(client).getId());
         scopes.put(MAP_ROLES_CLIENT_SCOPE, mapRolesClientScopePermission(client).getId());
         scopes.put(MAP_ROLES_COMPOSITE_SCOPE, mapRolesCompositePermission(client).getId());
-        scopes.put(EXCHANGE_TO_SCOPE, exchangeToPermission(client).getId());
+        scopes.put(TOKEN_EXCHANGE, exchangeToPermission(client).getId());
         return scopes;
     }
 
@@ -328,7 +328,7 @@ class ClientPermissions implements ClientPermissionEvaluator,  ClientPermissionM
 
             Scope scope = exchangeToScope(server);
             if (scope == null) {
-                logger.debug(EXCHANGE_TO_SCOPE + " not initialized");
+                logger.debug(TOKEN_EXCHANGE + " not initialized");
                 return false;
             }
             ClientModelIdentity identity = new ClientModelIdentity(session, authorizedClient);
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/IdentityProviderPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/IdentityProviderPermissions.java
index 9be37d6..a0e2e44 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/IdentityProviderPermissions.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/IdentityProviderPermissions.java
@@ -37,10 +37,10 @@ import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Set;
 
-import static org.keycloak.services.resources.admin.permissions.AdminPermissionManagement.EXCHANGE_TO_SCOPE;
+import static org.keycloak.services.resources.admin.permissions.AdminPermissionManagement.TOKEN_EXCHANGE;
 
 /**
- * Manages default policies for all users.
+ * Manages default policies for identity providers.
  *
  *
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -65,12 +65,12 @@ class IdentityProviderPermissions implements  IdentityProviderPermissionManageme
     }
 
     private String getExchangeToPermissionName(IdentityProviderModel idp) {
-        return EXCHANGE_TO_SCOPE + ".permission.idp." + idp.getInternalId();
+        return TOKEN_EXCHANGE + ".permission.idp." + idp.getInternalId();
     }
 
     private void initialize(IdentityProviderModel idp) {
         ResourceServer server = root.initializeRealmResourceServer();
-        Scope exchangeToScope = root.initializeScope(EXCHANGE_TO_SCOPE, server);
+        Scope exchangeToScope = root.initializeScope(TOKEN_EXCHANGE, server);
 
         String resourceName = getResourceName(idp);
         Resource resource = authz.getStoreFactory().getResourceStore().findByName(resourceName, server.getId());
@@ -124,7 +124,7 @@ class IdentityProviderPermissions implements  IdentityProviderPermissionManageme
 
 
     private Scope exchangeToScope(ResourceServer server) {
-        return authz.getStoreFactory().getScopeStore().findByName(EXCHANGE_TO_SCOPE, server.getId());
+        return authz.getStoreFactory().getScopeStore().findByName(TOKEN_EXCHANGE, server.getId());
     }
 
     @Override
@@ -141,7 +141,7 @@ class IdentityProviderPermissions implements  IdentityProviderPermissionManageme
     public Map<String, String> getPermissions(IdentityProviderModel idp) {
         initialize(idp);
         Map<String, String> scopes = new LinkedHashMap<>();
-        scopes.put(EXCHANGE_TO_SCOPE, exchangeToPermission(idp).getId());
+        scopes.put(TOKEN_EXCHANGE, exchangeToPermission(idp).getId());
         return scopes;
     }
 
@@ -176,7 +176,7 @@ class IdentityProviderPermissions implements  IdentityProviderPermissionManageme
 
             Scope scope = exchangeToScope(server);
             if (scope == null) {
-                logger.debug(EXCHANGE_TO_SCOPE + " not initialized");
+                logger.debug(TOKEN_EXCHANGE + " not initialized");
                 return false;
             }
             ClientModelIdentity identity = new ClientModelIdentity(session, authorizedClient);
diff --git a/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java b/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java
index ad9dc40..a3312d8 100755
--- a/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java
@@ -24,7 +24,7 @@ import org.keycloak.broker.provider.AbstractIdentityProvider;
 import org.keycloak.broker.provider.AuthenticationRequest;
 import org.keycloak.broker.provider.BrokeredIdentityContext;
 import org.keycloak.broker.provider.IdentityBrokerException;
-import org.keycloak.broker.provider.TokenExchangeTo;
+import org.keycloak.broker.provider.ExchangeTokenToIdentityProviderToken;
 import org.keycloak.broker.social.SocialIdentityProvider;
 import org.keycloak.common.ClientConnection;
 import org.keycloak.events.Details;
@@ -61,7 +61,7 @@ import java.net.URI;
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
 public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2IdentityProviderConfig> implements
-        SocialIdentityProvider<OAuth2IdentityProviderConfig>, TokenExchangeTo {
+        SocialIdentityProvider<OAuth2IdentityProviderConfig>, ExchangeTokenToIdentityProviderToken {
 
     String TWITTER_TOKEN_TYPE="twitter";
 
@@ -103,7 +103,7 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
     }
 
     @Override
-    public Response exchangeTo(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, org.keycloak.representations.AccessToken token, MultivaluedMap<String, String> params) {
+    public Response exchangeFromToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, org.keycloak.representations.AccessToken token, MultivaluedMap<String, String> params) {
         String requestedType = params.getFirst(OAuth2Constants.REQUESTED_TOKEN_TYPE);
         if (requestedType != null && !requestedType.equals(TWITTER_TOKEN_TYPE)) {
             return exchangeUnsupportedRequiredType();
@@ -111,7 +111,7 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
         if (!getConfig().isStoreToken()) {
             String brokerId = tokenUserSession.getNote(Details.IDENTITY_PROVIDER);
             if (brokerId == null || !brokerId.equals(getConfig().getAlias())) {
-                return exchangeNotSupported();
+                return exchangeNotLinkedNoStore(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
             }
             return exchangeSessionToken(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
         } else {
diff --git a/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/LinkAndExchangeServlet.java b/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/LinkAndExchangeServlet.java
index 65a945e..44ebb6d 100644
--- a/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/LinkAndExchangeServlet.java
+++ b/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/LinkAndExchangeServlet.java
@@ -47,7 +47,7 @@ import java.util.Map;
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
-@WebServlet("/client-linking")
+@WebServlet("/exchange-linking")
 public class LinkAndExchangeServlet extends HttpServlet {
 
     private String getPostDataString(HashMap<String, String> params) throws UnsupportedEncodingException{
@@ -100,9 +100,18 @@ public class LinkAndExchangeServlet extends HttpServlet {
             writer.flush();
             writer.close();
             os.close();
-            AccessTokenResponse tokenResponse = JsonSerialization.readValue(conn.getInputStream(), AccessTokenResponse.class);
-            conn.getInputStream().close();
-            return tokenResponse;
+            if (conn.getResponseCode() == 200) {
+                AccessTokenResponse tokenResponse = JsonSerialization.readValue(conn.getInputStream(), AccessTokenResponse.class);
+                conn.getInputStream().close();
+                return tokenResponse;
+            } else if (conn.getResponseCode() == 400) {
+                AccessTokenResponse tokenResponse = JsonSerialization.readValue(conn.getErrorStream(), AccessTokenResponse.class);
+                conn.getErrorStream().close();
+                return tokenResponse;
+
+            } else {
+                throw new RuntimeException("Unknown error!");
+            }
         } finally {
         }
     }
@@ -143,7 +152,9 @@ public class LinkAndExchangeServlet extends HttpServlet {
 
             String redirectUri = KeycloakUriBuilder.fromUri(request.getRequestURL().toString())
                     .replaceQuery(null)
-                    .queryParam("response", "true").build().toString();
+                    .queryParam("response", "true")
+                    .queryParam("realm", realm)
+                    .queryParam("provider", provider).build().toString();
             String accountLinkUrl = KeycloakUriBuilder.fromUri(linkUrl)
                     .queryParam("redirect_uri", redirectUri).build().toString();
             resp.setStatus(302);
@@ -159,6 +170,24 @@ public class LinkAndExchangeServlet extends HttpServlet {
             } else {
                 pw.println("Account Linked");
             }
+            pw.println("trying exchange");
+            try {
+                String provider = request.getParameter("provider");
+                String realm = request.getParameter("realm");
+                KeycloakSecurityContext session = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
+                AccessToken token = session.getToken();
+                String clientId = token.getAudience()[0];
+                String tokenString = session.getTokenString();
+                AccessTokenResponse response = doTokenExchange(realm, tokenString, provider,  clientId, "password");
+                error = (String)response.getOtherClaims().get("error");
+                if (error == null) {
+                    if (response.getToken() != null) pw.println("Exchange token received");
+                } else {
+                    pw.print("Error with exchange: " + error);
+                }
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            }
             pw.print("</body></html>");
             pw.flush();
         } else {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractLinkAndExchangeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractLinkAndExchangeTest.java
index 312517c..a382f27 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractLinkAndExchangeTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractLinkAndExchangeTest.java
@@ -59,10 +59,16 @@ import org.keycloak.testsuite.pages.LoginPage;
 import org.keycloak.testsuite.pages.LoginUpdateProfilePage;
 import org.keycloak.testsuite.util.OAuthClient;
 import org.keycloak.testsuite.util.WaitUtils;
+import org.keycloak.util.BasicAuthHelper;
 import org.keycloak.util.JsonSerialization;
 
 import javax.ws.rs.client.Client;
 import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Form;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriBuilder;
 import java.net.URL;
 import java.util.LinkedList;
@@ -98,7 +104,7 @@ public abstract class AbstractLinkAndExchangeTest extends AbstractServletsAdapte
 
     public static class ClientApp extends AbstractPageWithInjectedUrl {
 
-        public static final String DEPLOYMENT_NAME = "client-linking";
+        public static final String DEPLOYMENT_NAME = "exchange-linking";
 
         @ArquillianResource
         @OperateOnDeployment(DEPLOYMENT_NAME)
@@ -124,9 +130,9 @@ public abstract class AbstractLinkAndExchangeTest extends AbstractServletsAdapte
         realm.setRealm(CHILD_IDP);
         realm.setEnabled(true);
         ClientRepresentation servlet = new ClientRepresentation();
-        servlet.setClientId("client-linking");
+        servlet.setClientId(ClientApp.DEPLOYMENT_NAME);
         servlet.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
-        String uri = "/client-linking";
+        String uri = "/" + ClientApp.DEPLOYMENT_NAME;
         if (!isRelative()) {
             uri = appServerContextRootPage.toString() + uri;
         }
@@ -199,7 +205,7 @@ public abstract class AbstractLinkAndExchangeTest extends AbstractServletsAdapte
 
     public static void setupRealm(KeycloakSession session) {
         RealmModel realm = session.realms().getRealmByName(CHILD_IDP);
-        ClientModel client = realm.getClientByClientId("client-linking");
+        ClientModel client = realm.getClientByClientId(ClientApp.DEPLOYMENT_NAME);
         IdentityProviderModel idp = realm.getIdentityProviderByAlias(PARENT_IDP);
         Assert.assertNotNull(idp);
 
@@ -213,6 +219,19 @@ public abstract class AbstractLinkAndExchangeTest extends AbstractServletsAdapte
         management.idps().exchangeToPermission(idp).addAssociatedPolicy(clientPolicy);
 
     }
+    public static void turnOffTokenStore(KeycloakSession session) {
+        RealmModel realm = session.realms().getRealmByName(CHILD_IDP);
+        IdentityProviderModel idp = realm.getIdentityProviderByAlias(PARENT_IDP);
+        idp.setStoreToken(false);
+        realm.updateIdentityProvider(idp);
+
+    }
+    public static void turnOnTokenStore(KeycloakSession session) {
+        RealmModel realm = session.realms().getRealmByName(CHILD_IDP);
+        IdentityProviderModel idp = realm.getIdentityProviderByAlias(PARENT_IDP);
+        idp.setStoreToken(true);
+        realm.updateIdentityProvider(idp);
+    }
     @Before
     public void createBroker() {
         createParentChild();
@@ -225,186 +244,101 @@ public abstract class AbstractLinkAndExchangeTest extends AbstractServletsAdapte
 
 
     @Test
-    public void testErrorConditions() throws Exception {
+    public void testAccountLink() throws Exception {
+        testingClient.server().run(AbstractLinkAndExchangeTest::turnOnTokenStore);
 
         RealmResource realm = adminClient.realms().realm(CHILD_IDP);
         List<FederatedIdentityRepresentation> links = realm.users().get(childUserId).getFederatedIdentity();
         Assert.assertTrue(links.isEmpty());
 
-        ClientRepresentation client = adminClient.realms().realm(CHILD_IDP).clients().findByClientId("client-linking").get(0);
-
-        UriBuilder redirectUri = UriBuilder.fromUri(appPage.getInjectedUrl().toString())
-                .path("link")
-                .queryParam("response", "true");
-
-        UriBuilder directLinking = UriBuilder.fromUri(AuthServerTestEnricher.getAuthServerContextRoot() + "/auth")
-                .path("realms/child/broker/{provider}/link")
-                .queryParam("client_id", "client-linking")
-                .queryParam("redirect_uri", redirectUri.build())
-                .queryParam("hash", Base64Url.encode("crap".getBytes()))
-                .queryParam("nonce", UUID.randomUUID().toString());
-
-        String linkUrl = directLinking
-                .build(PARENT_IDP).toString();
-
-        // test not logged in
-
-        navigateTo(linkUrl);
-        Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
-        loginPage.login("child", "password");
-
-        Assert.assertTrue(driver.getCurrentUrl().contains("link_error=not_logged_in"));
-
-        logoutAll();
-
-        // now log in
-
-        navigateTo( appPage.getInjectedUrl() + "/hello");
-        Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
-        loginPage.login("child", "password");
-        Assert.assertTrue(driver.getCurrentUrl().startsWith(appPage.getInjectedUrl() + "/hello"));
-        Assert.assertTrue(driver.getPageSource().contains("Unknown request:"));
-
-        // now test CSRF with bad hash.
-
-        navigateTo(linkUrl);
-
-        Assert.assertTrue(driver.getPageSource().contains("We're sorry..."));
-
-        logoutAll();
-
-        // now log in again with client that does not have scope
-
-        String accountId = adminClient.realms().realm(CHILD_IDP).clients().findByClientId(ACCOUNT_MANAGEMENT_CLIENT_ID).get(0).getId();
-        RoleRepresentation manageAccount = adminClient.realms().realm(CHILD_IDP).clients().get(accountId).roles().get(MANAGE_ACCOUNT).toRepresentation();
-        RoleRepresentation manageLinks = adminClient.realms().realm(CHILD_IDP).clients().get(accountId).roles().get(MANAGE_ACCOUNT_LINKS).toRepresentation();
-        RoleRepresentation userRole = adminClient.realms().realm(CHILD_IDP).roles().get("user").toRepresentation();
-
-        client.setFullScopeAllowed(false);
-        ClientResource clientResource = adminClient.realms().realm(CHILD_IDP).clients().get(client.getId());
-        clientResource.update(client);
-
-        List<RoleRepresentation> roles = new LinkedList<>();
-        roles.add(userRole);
-        clientResource.getScopeMappings().realmLevel().add(roles);
-
-        navigateTo( appPage.getInjectedUrl() + "/hello");
-        Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
-        loginPage.login("child", "password");
-        Assert.assertTrue(driver.getCurrentUrl().startsWith(appPage.getInjectedUrl() + "/hello"));
-        Assert.assertTrue(driver.getPageSource().contains("Unknown request:"));
-
-
-        UriBuilder linkBuilder = UriBuilder.fromUri(appPage.getInjectedUrl().toString())
+        String servletUri = appPage.getInjectedUrl().toString();
+        UriBuilder linkBuilder = UriBuilder.fromUri(servletUri)
                 .path("link");
-        String clientLinkUrl = linkBuilder.clone()
+        String linkUrl = linkBuilder.clone()
                 .queryParam("realm", CHILD_IDP)
                 .queryParam("provider", PARENT_IDP).build().toString();
-
-
-        navigateTo(clientLinkUrl);
-
-        Assert.assertTrue(driver.getCurrentUrl().contains("error=not_allowed"));
-
-        logoutAll();
-
-        // add MANAGE_ACCOUNT_LINKS scope should pass.
-
-        links = realm.users().get(childUserId).getFederatedIdentity();
-        Assert.assertTrue(links.isEmpty());
-
-
-        roles = new LinkedList<>();
-        roles.add(manageLinks);
-        clientResource.getScopeMappings().clientLevel(accountId).add(roles);
-
-        navigateTo(clientLinkUrl);
+        System.out.println("linkUrl: " + linkUrl);
+        navigateTo(linkUrl);
         Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
+        Assert.assertTrue(driver.getPageSource().contains(PARENT_IDP));
         loginPage.login("child", "password");
         Assert.assertTrue(loginPage.isCurrent(PARENT_IDP));
         loginPage.login(PARENT_USERNAME, "password");
-
+        System.out.println("After linking: " + driver.getCurrentUrl());
+        System.out.println(driver.getPageSource());
         Assert.assertTrue(driver.getCurrentUrl().startsWith(linkBuilder.toTemplate()));
         Assert.assertTrue(driver.getPageSource().contains("Account Linked"));
+        Assert.assertTrue(driver.getPageSource().contains("Exchange token received"));
 
         links = realm.users().get(childUserId).getFederatedIdentity();
         Assert.assertFalse(links.isEmpty());
 
-        realm.users().get(childUserId).removeFederatedIdentity(PARENT_IDP);
-        links = realm.users().get(childUserId).getFederatedIdentity();
-        Assert.assertTrue(links.isEmpty());
-
-        clientResource.getScopeMappings().clientLevel(accountId).remove(roles);
-
-        logoutAll();
-
-        navigateTo(clientLinkUrl);
-        Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
-        loginPage.login("child", "password");
-
-        Assert.assertTrue(driver.getCurrentUrl().contains("link_error=not_allowed"));
-
-        logoutAll();
-
-        // add MANAGE_ACCOUNT scope should pass
-
-        links = realm.users().get(childUserId).getFederatedIdentity();
-        Assert.assertTrue(links.isEmpty());
-
-
-        roles = new LinkedList<>();
-        roles.add(manageAccount);
-        clientResource.getScopeMappings().clientLevel(accountId).add(roles);
-
-        navigateTo(clientLinkUrl);
-        Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
-        loginPage.login("child", "password");
-        Assert.assertTrue(loginPage.isCurrent(PARENT_IDP));
-        loginPage.login(PARENT_USERNAME, "password");
-
-        Assert.assertTrue(driver.getCurrentUrl().startsWith(linkBuilder.toTemplate()));
-        Assert.assertTrue(driver.getPageSource().contains("Account Linked"));
 
-        links = realm.users().get(childUserId).getFederatedIdentity();
-        Assert.assertFalse(links.isEmpty());
-
-        realm.users().get(childUserId).removeFederatedIdentity(PARENT_IDP);
-        links = realm.users().get(childUserId).getFederatedIdentity();
-        Assert.assertTrue(links.isEmpty());
 
-        clientResource.getScopeMappings().clientLevel(accountId).remove(roles);
+        // do exchange
 
-        logoutAll();
+        String accessToken = oauth.doGrantAccessTokenRequest(CHILD_IDP, "child", "password", null, ClientApp.DEPLOYMENT_NAME, "password").getAccessToken();
+        Client httpClient = ClientBuilder.newClient();
 
-        navigateTo(clientLinkUrl);
-        Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
-        loginPage.login("child", "password");
+        WebTarget exchangeUrl = httpClient.target(OAuthClient.AUTH_SERVER_ROOT)
+                .path("/realms")
+                .path(CHILD_IDP)
+                .path("protocol/openid-connect/token");
+        System.out.println("Exchange url: " + exchangeUrl.getUri().toString());
+
+        Response response = exchangeUrl.request()
+                .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(ClientApp.DEPLOYMENT_NAME, "password"))
+                .post(Entity.form(
+                        new Form()
+                        .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
+                        .param(OAuth2Constants.SUBJECT_TOKEN, accessToken)
+                        .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE)
+                        .param(OAuth2Constants.REQUESTED_ISSUER, PARENT_IDP)
+
+                ));
+        Assert.assertEquals(200, response.getStatus());
+        AccessTokenResponse tokenResponse = response.readEntity(AccessTokenResponse.class);
+        response.close();
+        String externalToken = tokenResponse.getToken();
+        Assert.assertNotNull(externalToken);
+        Assert.assertTrue(tokenResponse.getExpiresIn() > 0);
+        setTimeOffset((int)tokenResponse.getExpiresIn() + 1);
+
+        // test that token refresh happens
+
+        // get access token again because we may have timed out
+        accessToken = oauth.doGrantAccessTokenRequest(CHILD_IDP, "child", "password", null, ClientApp.DEPLOYMENT_NAME, "password").getAccessToken();
+        response = exchangeUrl.request()
+                .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(ClientApp.DEPLOYMENT_NAME, "password"))
+                .post(Entity.form(
+                        new Form()
+                                .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
+                                .param(OAuth2Constants.SUBJECT_TOKEN, accessToken)
+                                .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE)
+                                .param(OAuth2Constants.REQUESTED_ISSUER, PARENT_IDP)
+
+                ));
+        Assert.assertEquals(200, response.getStatus());
+        tokenResponse = response.readEntity(AccessTokenResponse.class);
+        response.close();
+        Assert.assertNotEquals(externalToken, tokenResponse.getToken());
 
-        Assert.assertTrue(driver.getCurrentUrl().contains("link_error=not_allowed"));
 
         logoutAll();
 
 
-        // undo fullScopeAllowed
-
-        client = adminClient.realms().realm(CHILD_IDP).clients().findByClientId("client-linking").get(0);
-        client.setFullScopeAllowed(true);
-        clientResource.update(client);
-
+        realm.users().get(childUserId).removeFederatedIdentity(PARENT_IDP);
         links = realm.users().get(childUserId).getFederatedIdentity();
         Assert.assertTrue(links.isEmpty());
 
-        logoutAll();
-
-
-
-
 
 
     }
 
     @Test
-    public void testAccountLink() throws Exception {
+    public void testAccountLinkNoTokenStore() throws Exception {
+        testingClient.server().run(AbstractLinkAndExchangeTest::turnOffTokenStore);
+
         RealmResource realm = adminClient.realms().realm(CHILD_IDP);
         List<FederatedIdentityRepresentation> links = realm.users().get(childUserId).getFederatedIdentity();
         Assert.assertTrue(links.isEmpty());
@@ -425,50 +359,24 @@ public abstract class AbstractLinkAndExchangeTest extends AbstractServletsAdapte
         System.out.println(driver.getPageSource());
         Assert.assertTrue(driver.getCurrentUrl().startsWith(linkBuilder.toTemplate()));
         Assert.assertTrue(driver.getPageSource().contains("Account Linked"));
+        Assert.assertTrue(driver.getPageSource().contains("Exchange token received"));
 
-        OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest(CHILD_IDP, "child", "password", null, "client-linking", "password");
-        Assert.assertNotNull(response.getAccessToken());
-        Assert.assertNull(response.getError());
-        Client httpClient = ClientBuilder.newClient();
-        String firstToken = getToken(response, httpClient);
-        Assert.assertNotNull(firstToken);
-
-
-        navigateTo(linkUrl);
-        Assert.assertTrue(driver.getPageSource().contains("Account Linked"));
-        String nextToken = getToken(response, httpClient);
-        Assert.assertNotNull(nextToken);
-        Assert.assertNotEquals(firstToken, nextToken);
-
-
+        links = realm.users().get(childUserId).getFederatedIdentity();
+        Assert.assertFalse(links.isEmpty());
 
 
 
+        logoutAll();
 
-        links = realm.users().get(childUserId).getFederatedIdentity();
-        Assert.assertFalse(links.isEmpty());
 
         realm.users().get(childUserId).removeFederatedIdentity(PARENT_IDP);
         links = realm.users().get(childUserId).getFederatedIdentity();
         Assert.assertTrue(links.isEmpty());
 
-        logoutAll();
 
 
     }
 
-    private String getToken(OAuthClient.AccessTokenResponse response, Client httpClient) throws Exception {
-        String idpToken =  httpClient.target(OAuthClient.AUTH_SERVER_ROOT)
-                .path("realms")
-                .path("child/broker")
-                .path(PARENT_IDP)
-                .path("token")
-                .request()
-                .header("Authorization", "Bearer " + response.getAccessToken())
-                .get(String.class);
-        AccessTokenResponse res = JsonSerialization.readValue(idpToken, AccessTokenResponse.class);
-        return res.getToken();
-    }
 
     public void logoutAll() {
         String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder()).build(CHILD_IDP).toString();
@@ -477,176 +385,6 @@ public abstract class AbstractLinkAndExchangeTest extends AbstractServletsAdapte
         navigateTo(logoutUri);
     }
 
-    @Test
-    public void testLinkOnlyProvider() throws Exception {
-        RealmResource realm = adminClient.realms().realm(CHILD_IDP);
-        IdentityProviderRepresentation rep = realm.identityProviders().get(PARENT_IDP).toRepresentation();
-        rep.setLinkOnly(true);
-        realm.identityProviders().get(PARENT_IDP).update(rep);
-        try {
-
-            List<FederatedIdentityRepresentation> links = realm.users().get(childUserId).getFederatedIdentity();
-            Assert.assertTrue(links.isEmpty());
-
-            UriBuilder linkBuilder = UriBuilder.fromUri(appPage.getInjectedUrl().toString())
-                    .path("link");
-            String linkUrl = linkBuilder.clone()
-                    .queryParam("realm", CHILD_IDP)
-                    .queryParam("provider", PARENT_IDP).build().toString();
-            navigateTo(linkUrl);
-            Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
-
-            // should not be on login page.  This is what we are testing
-            Assert.assertFalse(driver.getPageSource().contains(PARENT_IDP));
-
-            // now test that we can still link.
-            loginPage.login("child", "password");
-            Assert.assertTrue(loginPage.isCurrent(PARENT_IDP));
-            loginPage.login(PARENT_USERNAME, "password");
-            System.out.println("After linking: " + driver.getCurrentUrl());
-            System.out.println(driver.getPageSource());
-            Assert.assertTrue(driver.getCurrentUrl().startsWith(linkBuilder.toTemplate()));
-            Assert.assertTrue(driver.getPageSource().contains("Account Linked"));
-
-            links = realm.users().get(childUserId).getFederatedIdentity();
-            Assert.assertFalse(links.isEmpty());
-
-            realm.users().get(childUserId).removeFederatedIdentity(PARENT_IDP);
-            links = realm.users().get(childUserId).getFederatedIdentity();
-            Assert.assertTrue(links.isEmpty());
-
-            logoutAll();
-
-            System.out.println("testing link-only attack");
-
-            navigateTo(linkUrl);
-            Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
-
-            System.out.println("login page uri is: " + driver.getCurrentUrl());
-
-            // ok, now scrape the code from page
-            String pageSource = driver.getPageSource();
-            String action = ActionURIUtils.getActionURIFromPageSource(pageSource);
-            System.out.println("action uri: " + action);
-
-            Map<String, String> queryParams = ActionURIUtils.parseQueryParamsFromActionURI(action);
-            System.out.println("query params: " + queryParams);
-
-            // now try and use the code to login to remote link-only idp
-
-            String uri = "/auth/realms/child/broker/parent-idp/login";
-
-            uri = UriBuilder.fromUri(AuthServerTestEnricher.getAuthServerContextRoot())
-                    .path(uri)
-                    .queryParam(OAuth2Constants.CODE, queryParams.get(OAuth2Constants.CODE))
-                    .queryParam(Constants.CLIENT_ID, queryParams.get(Constants.CLIENT_ID))
-                    .build().toString();
-
-            System.out.println("hack uri: " + uri);
-
-            navigateTo(uri);
-
-            Assert.assertTrue(driver.getPageSource().contains("Could not send authentication request to identity provider."));
-
-
-
-
-
-        } finally {
-
-            rep.setLinkOnly(false);
-            realm.identityProviders().get(PARENT_IDP).update(rep);
-        }
-
-
-    }
-
-
-    @Test
-    public void testAccountNotLinkedAutomatically() throws Exception {
-        RealmResource realm = adminClient.realms().realm(CHILD_IDP);
-        List<FederatedIdentityRepresentation> links = realm.users().get(childUserId).getFederatedIdentity();
-        Assert.assertTrue(links.isEmpty());
-
-        // Login to account mgmt first
-        profilePage.open(CHILD_IDP);
-        WaitUtils.waitForPageToLoad();
-
-        Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
-        loginPage.login("child", "password");
-        profilePage.assertCurrent();
-
-        // Now in another tab, open login screen with "prompt=login" . Login screen will be displayed even if I have SSO cookie
-        UriBuilder linkBuilder = UriBuilder.fromUri(appPage.getInjectedUrl().toString())
-                .path("nosuch");
-        String linkUrl = linkBuilder.clone()
-                .queryParam(OIDCLoginProtocol.PROMPT_PARAM, OIDCLoginProtocol.PROMPT_VALUE_LOGIN)
-                .build().toString();
-
-        navigateTo(linkUrl);
-        Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
-        loginPage.clickSocial(PARENT_IDP);
-        Assert.assertTrue(loginPage.isCurrent(PARENT_IDP));
-        loginPage.login(PARENT_USERNAME, "password");
-
-        // Test I was not automatically linked.
-        links = realm.users().get(childUserId).getFederatedIdentity();
-        Assert.assertTrue(links.isEmpty());
-
-        loginUpdateProfilePage.assertCurrent();
-        loginUpdateProfilePage.update("Joe", "Doe", "joe@parent.com");
-
-        errorPage.assertCurrent();
-        Assert.assertEquals("You are already authenticated as different user 'child' in this session. Please logout first.", errorPage.getError());
-
-        logoutAll();
-
-        // Remove newly created user
-        String newUserId = ApiUtil.findUserByUsername(realm, "parent").getId();
-        getCleanup("child").addUserId(newUserId);
-    }
-
-
-    @Test
-    public void testAccountLinkingExpired() throws Exception {
-        RealmResource realm = adminClient.realms().realm(CHILD_IDP);
-        List<FederatedIdentityRepresentation> links = realm.users().get(childUserId).getFederatedIdentity();
-        Assert.assertTrue(links.isEmpty());
-
-        // Login to account mgmt first
-        profilePage.open(CHILD_IDP);
-        WaitUtils.waitForPageToLoad();
-
-        Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
-        loginPage.login("child", "password");
-        profilePage.assertCurrent();
-
-        // Now in another tab, request account linking
-        UriBuilder linkBuilder = UriBuilder.fromUri(appPage.getInjectedUrl().toString())
-                .path("link");
-        String linkUrl = linkBuilder.clone()
-                .queryParam("realm", CHILD_IDP)
-                .queryParam("provider", PARENT_IDP).build().toString();
-        navigateTo(linkUrl);
-
-        Assert.assertTrue(loginPage.isCurrent(PARENT_IDP));
-
-        // Logout "child" userSession in the meantime (for example through admin request)
-        realm.logoutAll();
-
-        // Finish login on parent.
-        loginPage.login(PARENT_USERNAME, "password");
-
-        // Test I was not automatically linked
-        links = realm.users().get(childUserId).getFederatedIdentity();
-        Assert.assertTrue(links.isEmpty());
-
-        errorPage.assertCurrent();
-        Assert.assertEquals("Requested broker account linking, but current session is no longer valid.", errorPage.getError());
-
-        logoutAll();
-    }
-
     private void navigateTo(String uri) {
         driver.navigate().to(uri);
         WaitUtils.waitForPageToLoad();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/undertow/servlet/UndertowLinkAndExchangeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/undertow/servlet/UndertowLinkAndExchangeTest.java
index 5230156..a2059b9 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/undertow/servlet/UndertowLinkAndExchangeTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/undertow/servlet/UndertowLinkAndExchangeTest.java
@@ -39,4 +39,10 @@ public class UndertowLinkAndExchangeTest extends AbstractLinkAndExchangeTest {
     public void testAccountLink() throws Exception {
         super.testAccountLink();
     }
+
+    @Override
+    @Test
+    public void testAccountLinkNoTokenStore() throws Exception {
+        super.testAccountLinkNoTokenStore();
+    }
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/exchange-linking/childrealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/exchange-linking/childrealm.json
new file mode 100644
index 0000000..5d624f4
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/exchange-linking/childrealm.json
@@ -0,0 +1,38 @@
+{
+    "id": "child",
+    "realm": "child",
+    "enabled": true,
+    "accessTokenLifespan": 600,
+    "accessCodeLifespan": 10,
+    "accessCodeLifespanUserAction": 6000,
+    "sslRequired": "external",
+    "registrationAllowed": false,
+    "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
+    "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+    "requiredCredentials": [ "password" ],
+    "users" : [
+        {
+            "username" : "bburke@redhat.com",
+            "enabled": true,
+            "email" : "bburke@redhat.com",
+            "firstName": "Bill",
+            "lastName": "Burke",
+            "credentials" : [
+                { "type" : "password",
+                    "value" : "password" }
+            ]
+        }
+    ],
+    "clients": [
+        {
+            "clientId": "exchange-linking",
+            "enabled": true,
+            "adminUrl": "/exchange-linking",
+            "baseUrl": "/exchange-linking",
+            "redirectUris": [
+                "/exchange-linking/*"
+            ],
+            "secret": "password"
+        }
+    ]
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/exchange-linking/META-INF/context.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/exchange-linking/META-INF/context.xml
new file mode 100644
index 0000000..b4ddcce
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/exchange-linking/META-INF/context.xml
@@ -0,0 +1,20 @@
+<!--
+  ~ Copyright 2016 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.
+  -->
+
+<Context path="/customer-portal">
+    <Valve className="org.keycloak.adapters.tomcat.KeycloakAuthenticatorValve"/>
+</Context>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/exchange-linking/WEB-INF/jetty-web.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/exchange-linking/WEB-INF/jetty-web.xml
new file mode 100644
index 0000000..8c59313
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/exchange-linking/WEB-INF/jetty-web.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0"?>
+<!--
+  ~ Copyright 2016 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.
+  -->
+
+<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://www.eclipse.org/jetty/configure_9_0.dtd">
+<Configure class="org.eclipse.jetty.webapp.WebAppContext">
+    <Get name="securityHandler">
+        <Set name="authenticator">
+            <New class="org.keycloak.adapters.jetty.KeycloakJettyAuthenticator">
+                <!--
+                <Set name="adapterConfig">
+                    <New class="org.keycloak.representations.adapters.config.AdapterConfig">
+                        <Set name="realm">tomcat</Set>
+                        <Set name="resource">customer-portal</Set>
+                        <Set name="authServerUrl">http://localhost:8180/auth</Set>
+                        <Set name="sslRequired">external</Set>
+                        <Set name="credentials">
+                            <Map>
+                                <Entry>
+                                    <Item>secret</Item>
+                                    <Item>password</Item>
+                                </Entry>
+                            </Map>
+                        </Set>
+                        <Set name="realmKey">MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB</Set>
+                    </New>
+                </Set>
+                -->
+            </New>
+        </Set>
+    </Get>
+</Configure>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/exchange-linking/WEB-INF/keycloak.json b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/exchange-linking/WEB-INF/keycloak.json
new file mode 100644
index 0000000..7b41211
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/exchange-linking/WEB-INF/keycloak.json
@@ -0,0 +1,10 @@
+{
+  "realm" : "child",
+  "resource" : "exchange-linking",
+  "auth-server-url" : "http://localhost:8180/auth",
+  "ssl-required" : "external",
+  "min-time-between-jwks-requests" : 0,
+  "credentials" : {
+      "secret": "password"
+   }
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/exchange-linking/WEB-INF/web.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/exchange-linking/WEB-INF/web.xml
new file mode 100644
index 0000000..4e87ad2
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/exchange-linking/WEB-INF/web.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2016 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.
+  -->
+
+<web-app xmlns="http://java.sun.com/xml/ns/javaee"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
+         version="3.0">
+
+    <module-name>exchange-linking</module-name>
+
+    <servlet>
+        <servlet-name>Servlet</servlet-name>
+        <servlet-class>org.keycloak.testsuite.adapter.servlet.LinkAndExchangeServlet</servlet-class>
+    </servlet>
+
+    <servlet-mapping>
+        <servlet-name>Servlet</servlet-name>
+        <url-pattern>/*</url-pattern>
+    </servlet-mapping>
+
+    <security-constraint>
+        <web-resource-collection>
+            <web-resource-name>Users</web-resource-name>
+            <url-pattern>/*</url-pattern>
+        </web-resource-collection>
+        <auth-constraint>
+            <role-name>user</role-name>
+        </auth-constraint>
+    </security-constraint>
+
+    <login-config>
+        <auth-method>KEYCLOAK</auth-method>
+        <realm-name>child</realm-name>
+    </login-config>
+
+    <security-role>
+        <role-name>user</role-name>
+    </security-role>
+</web-app>
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 3ae3500..8848371 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
@@ -1344,7 +1344,8 @@ manage-permissions-group.tooltip=Fine grain permssions for admins that want to m
 manage-authz-group-scope-description=Policies that decide if an admin can manage this group
 view-authz-group-scope-description=Policies that decide if an admin can view this group
 view-members-authz-group-scope-description=Policies that decide if an admin can manage the members of this group
-exchange-to-authz-client-scope-description=Policies that decide which clients are allowed exchange tokens for a token that is targeted to this client.
+token-exchange-authz-client-scope-description=Policies that decide which clients are allowed exchange tokens for a token that is targeted to this client.
+token-exchange-authz-idp-scope-description=Policies that decide which clients are allowed exchange tokens for an external token minted by this identity provider.
 manage-authz-client-scope-description=Policies that decide if an admin can manage this client
 configure-authz-client-scope-description=Reduced management permissions for admin.  Cannot set scope, template, or protocol mappers.
 view-authz-client-scope-description=Policies that decide if an admin can view this client
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-app.js b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-app.js
index a59ebf3..fbd5379 100644
--- a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-app.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-app.js
@@ -470,6 +470,18 @@ module.config(['$routeProvider', function ($routeProvider) {
             },
             controller : 'GroupPermissionsCtrl'
         })
+        .when('/realms/:realm/identity-provider-settings/provider/:provider_id/:alias/permissions', {
+            templateUrl : function(params){ return resourceUrl + '/partials/authz/mgmt/broker-permissions.html'; },
+            resolve : {
+                realm : function(RealmLoader) {
+                    return RealmLoader();
+                },
+                identityProvider : function(IdentityProviderLoader) {
+                    return IdentityProviderLoader();
+                }
+             },
+            controller : 'IdentityProviderPermissionCtrl'
+        })
     ;
 }]);
 
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js
index 76dc5aa..9233a04 100644
--- a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js
@@ -2544,7 +2544,6 @@ module.controller('RealmRolePermissionsCtrl', function($scope, $http, $route, $l
         $scope.permissions = data;
         $scope.$watch('permissions.enabled', function(newVal, oldVal) {
             if (newVal != oldVal) {
-                console.log('Changing permissions enabled to: ' + $scope.permissions.enabled);
                 var param = {enabled: $scope.permissions.enabled};
                 $scope.permissions= RoleManagementPermissions.update({realm: realm.realm, role:role.id}, param);
             }
@@ -2563,7 +2562,6 @@ module.controller('ClientRolePermissionsCtrl', function($scope, $http, $route, $
         $scope.permissions = data;
         $scope.$watch('permissions.enabled', function(newVal, oldVal) {
             if (newVal != oldVal) {
-                console.log('Changing permissions enabled to: ' + $scope.permissions.enabled);
                 var param = {enabled: $scope.permissions.enabled};
                 $scope.permissions = RoleManagementPermissions.update({realm: realm.realm, role:role.id}, param);
             }
@@ -2582,7 +2580,6 @@ module.controller('UsersPermissionsCtrl', function($scope, $http, $route, $locat
         $scope.permissions = data;
         $scope.$watch('permissions.enabled', function(newVal, oldVal) {
             if (newVal != oldVal) {
-                console.log('Changing permissions enabled to: ' + $scope.permissions.enabled);
                 var param = {enabled: $scope.permissions.enabled};
                 $scope.permissions = UsersManagementPermissions.update({realm: realm.realm}, param);
 
@@ -2605,7 +2602,6 @@ module.controller('ClientPermissionsCtrl', function($scope, $http, $route, $loca
         $scope.permissions = data;
         $scope.$watch('permissions.enabled', function(newVal, oldVal) {
             if (newVal != oldVal) {
-                console.log('Changing permissions enabled to: ' + $scope.permissions.enabled);
                 var param = {enabled: $scope.permissions.enabled};
                 $scope.permissions = ClientManagementPermissions.update({realm: realm.realm, client: client.id}, param);
             }
@@ -2616,6 +2612,23 @@ module.controller('ClientPermissionsCtrl', function($scope, $http, $route, $loca
     });
 });
 
+module.controller('IdentityProviderPermissionCtrl', function($scope, $http, $route, $location, realm, identityProvider, Client, IdentityProviderManagementPermissions, Notifications) {
+    $scope.identityProvider = identityProvider;
+    $scope.realm = realm;
+    IdentityProviderManagementPermissions.get({realm: realm.realm, alias: identityProvider.alias}, function(data) {
+        $scope.permissions = data;
+        $scope.$watch('permissions.enabled', function(newVal, oldVal) {
+            if (newVal != oldVal) {
+                var param = {enabled: $scope.permissions.enabled};
+                $scope.permissions = IdentityProviderManagementPermissions.update({realm: realm.realm, alias: identityProvider.alias}, param);
+            }
+        }, true);
+    });
+    Client.query({realm: realm.realm, clientId: getManageClientId(realm)}, function(data) {
+        $scope.realmManagementClientId = data[0].id;
+    });
+});
+
 module.controller('GroupPermissionsCtrl', function($scope, $http, $route, $location, realm, group, GroupManagementPermissions, Client, Notifications) {
     $scope.group = group;
     $scope.realm = realm;
@@ -2626,7 +2639,6 @@ module.controller('GroupPermissionsCtrl', function($scope, $http, $route, $locat
         $scope.permissions = data;
         $scope.$watch('permissions.enabled', function(newVal, oldVal) {
             if (newVal != oldVal) {
-                console.log('Changing permissions enabled to: ' + $scope.permissions.enabled);
                 var param = {enabled: $scope.permissions.enabled};
                 $scope.permissions = GroupManagementPermissions.update({realm: realm.realm, group: group.id}, param);
             }
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-services.js b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-services.js
index f56ca8f..12315ed 100644
--- a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-services.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-services.js
@@ -178,6 +178,17 @@ module.factory('ClientManagementPermissions', function($resource) {
     });
 });
 
+module.factory('IdentityProviderManagementPermissions', function($resource) {
+    return $resource(authUrl + '/admin/realms/:realm/identity-provider/instances/:alias/management/permissions', {
+        realm : '@realm',
+        alias : '@alias'
+    }, {
+        update: {
+            method: 'PUT'
+        }
+    });
+});
+
 module.factory('GroupManagementPermissions', function($resource) {
     return $resource(authUrl + '/admin/realms/:realm/groups/:group/management/permissions', {
         realm : '@realm',
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
index 2644379..ad8a407 100644
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
@@ -1001,7 +1001,7 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload
         } else {
             IdentityProvider.update({
                 realm: $scope.realm.realm,
-                id: $scope.identityProvider.internalId
+                alias: $scope.identityProvider.alias
             }, $scope.identityProvider, function () {
                 $route.reload();
                 Notifications.success("The " + $scope.identityProvider.alias + " provider has been updated.");
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/broker-permissions.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/broker-permissions.html
new file mode 100644
index 0000000..2e389ff
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/mgmt/broker-permissions.html
@@ -0,0 +1,40 @@
+<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}}/identity-provider-settings">{{:: 'identity-providers' | translate}}</a></li>
+        <li data-ng-show="!newIdentityProvider && identityProvider.displayName">{{identityProvider.displayName}}</li>
+        <li data-ng-show="!newIdentityProvider && !identityProvider.displayName">{{identityProvider.alias}}</li>
+    </ol>
+
+    <kc-tabs-identity-provider></kc-tabs-identity-provider>
+
+    <form class=form-horizontal" name="enableForm" novalidate kc-read-only="!access.manageIdentityProviders || !access.manageAuthorization">
+        <fieldset class="border-top">
+        <div class="form-group">
+            <label class="col-md-2 control-label" for="permissionsEnabled">{{:: 'permissions-enabled-role' | translate}}</label>
+            <div class="col-md-6">
+                <input ng-model="permissions.enabled" name="permissionsEnabled" id="permissionsEnabled" ng-disabled="!access.manageAuthorization" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
+            </div>
+            <kc-tooltip>{{:: 'permissions-enabled-role.tooltip' | translate}}</kc-tooltip>
+        </div>
+        </fieldset>
+    </form>
+    <table class="datatable table table-striped table-bordered dataTable no-footer" data-ng-show="permissions.enabled">
+        <thead>
+        <tr>
+            <th>{{:: 'scope-name' | translate}}</th>
+            <th>{{:: 'description' | translate}}</th>
+            <th colspan="2">{{:: 'actions' | translate}}</th>
+        </tr>
+        </thead>
+        <tbody>
+        <tr ng-repeat="(scopeName, scopeId) in permissions.scopePermissions">
+            <td><a href="#/realms/{{realm.realm}}/clients/{{realmManagementClientId}}/authz/resource-server/permission/scope/{{scopeId}}">{{scopeName}}</a></td>
+            <td translate="{{scopeName}}-authz-idp-scope-description"></td>
+            <td class="kc-action-cell" kc-open="/realms/{{realm.realm}}/clients/{{realmManagementClientId}}/authz/resource-server/permission/scope/{{scopeId}}">{{:: 'edit' | translate}}</td>
+        </tr>
+        </tbody>
+    </table>
+
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-identity-provider.html b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-identity-provider.html
index 279e598..d6a9dfd 100644
--- a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-identity-provider.html
+++ b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-identity-provider.html
@@ -12,5 +12,6 @@
         <li ng-class="{active: !path[6] && path.length > 5}"><a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}">{{:: 'settings' | translate}}</a></li>
         <li ng-class="{active: path[4] == 'mappers'}"><a href="#/realms/{{realm.realm}}/identity-provider-mappers/{{identityProvider.alias}}/mappers">{{:: 'mappers' | translate}}</a></li>
         <li ng-class="{active: path[6] == 'export'}"><a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}/export" data-ng-show="!importFile && !newIdentityProvider && identityProvider.providerId == 'saml'">{{:: 'export' | translate}}</a></li>
+        <li ng-class="{active: path[4] == 'permissions'}" data-ng-show="!newIdentityProvider && access.manageAuthorization"><a href="#/realms/{{realm.realm}}/identity-provider-settings/provider/{{identityProvider.providerId}}/{{identityProvider.alias}}/permissions">{{:: 'authz-permissions' | translate}}</a></li>
     </ul>
 </div>