keycloak-uncached

Merge pull request #4420 from patriot1burke/master token

8/24/2017 9:26:18 PM

Changes

Details

diff --git a/core/src/main/java/org/keycloak/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java
index bedef8d..20d6e73 100644
--- a/core/src/main/java/org/keycloak/OAuth2Constants.java
+++ b/core/src/main/java/org/keycloak/OAuth2Constants.java
@@ -98,6 +98,7 @@ public interface OAuth2Constants {
     String SUBJECT_TOKEN="subject_token";
     String SUBJECT_TOKEN_TYPE="subject_token_type";
     String REQUESTED_TOKEN_TYPE="requested_token_type";
+    String ISSUED_TOKEN_TYPE="issued_token_type";
     String REQUESTED_ISSUER="requested_issuer";
     String ACCESS_TOKEN_TYPE="urn:ietf:params:oauth:token-type:access_token";
     String REFRESH_TOKEN_TYPE="urn:ietf:params:oauth:token-type:refresh_token";
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 0320299..231dbc1 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
@@ -16,22 +16,34 @@
  */
 package org.keycloak.broker.provider;
 
+import org.keycloak.common.util.Base64Url;
+import org.keycloak.common.util.KeycloakUriBuilder;
 import org.keycloak.events.EventBuilder;
+import org.keycloak.models.ClientModel;
 import org.keycloak.models.IdentityProviderModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.UserSessionModel;
+import org.keycloak.representations.AccessToken;
 import org.keycloak.sessions.AuthenticationSessionModel;
 
+import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriInfo;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
 
 /**
  * @author Pedro Igor
  */
 public abstract class AbstractIdentityProvider<C extends IdentityProviderModel> implements IdentityProvider<C> {
 
+    public static final String ACCOUNT_LINK_URL = "account-link-url";
     protected final KeycloakSession session;
     private final C config;
 
@@ -74,6 +86,62 @@ public abstract class AbstractIdentityProvider<C extends IdentityProviderModel> 
 
     }
 
+    public Response exchangeNotSupported() {
+        Map<String, String> error = new HashMap<>();
+        error.put("error", "invalid_target");
+        error.put("error_description", "target_exchange_unsupported");
+        return  Response.status(400).entity(error).type(MediaType.APPLICATION_JSON_TYPE).build();
+    }
+
+    public Response exchangeNotLinked(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token) {
+        return exchangeErrorResponse(uriInfo, authorizedClient, tokenUserSession, token, "invalid_target");
+    }
+
+    protected Response exchangeErrorResponse(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, AccessToken token, String reason) {
+        Map<String, String> error = new HashMap<>();
+        error.put("error", "invalid_target");
+        error.put("error_description", reason);
+        String accountLinkUrl = getLinkingUrl(uriInfo, authorizedClient, tokenUserSession, token);
+        if (accountLinkUrl != null) error.put(ACCOUNT_LINK_URL, accountLinkUrl);
+        return Response.status(400).entity(error).type(MediaType.APPLICATION_JSON_TYPE).build();
+    }
+
+    protected String getLinkingUrl(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, AccessToken token) {
+        if (authorizedClient.getClientId().equals(token.getIssuedFor())) {
+            String provider = getConfig().getAlias();
+            String clientId = authorizedClient.getClientId();
+            String nonce = UUID.randomUUID().toString();
+            MessageDigest md = null;
+            try {
+                md = MessageDigest.getInstance("SHA-256");
+            } catch (NoSuchAlgorithmException e) {
+                throw new RuntimeException(e);
+            }
+            String input = nonce + tokenUserSession.getId() + clientId + provider;
+            byte[] check = md.digest(input.getBytes(StandardCharsets.UTF_8));
+            String hash = Base64Url.encode(check);
+            return KeycloakUriBuilder.fromUri(uriInfo.getBaseUri())
+                    .path("/realms/{realm}/broker/{provider}/link")
+                    .queryParam("nonce", nonce)
+                    .queryParam("hash", hash)
+                    .queryParam("client_id", clientId)
+                    .build(authorizedClient.getRealm().getName(), provider)
+                    .toString();
+        }
+        return null;
+    }
+
+    public Response exchangeTokenExpired(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token) {
+        return exchangeErrorResponse(uriInfo, authorizedClient, tokenUserSession, token, "token_expired");
+    }
+
+    public Response exchangeUnsupportedRequiredType() {
+        Map<String, String> error = new HashMap<>();
+        error.put("error", "invalid_target");
+        error.put("error_description", "response_token_type_unsupported");
+        return Response.status(400).entity(error).type(MediaType.APPLICATION_JSON_TYPE).build();
+    }
+
     @Override
     public void authenticationFinished(AuthenticationSessionModel authSession, BrokeredIdentityContext context) {
 
diff --git a/server-spi-private/src/main/java/org/keycloak/broker/provider/TokenExchangeTo.java b/server-spi-private/src/main/java/org/keycloak/broker/provider/TokenExchangeTo.java
index 36755d4..64f9db1 100644
--- a/server-spi-private/src/main/java/org/keycloak/broker/provider/TokenExchangeTo.java
+++ b/server-spi-private/src/main/java/org/keycloak/broker/provider/TokenExchangeTo.java
@@ -23,6 +23,7 @@ import org.keycloak.representations.AccessToken;
 
 import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
 
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -38,5 +39,5 @@ public interface TokenExchangeTo {
      * @param params form parameters received for requested exchange
      * @return
      */
-    Response exchangeTo(ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token, MultivaluedMap<String, String> params);
+    Response exchangeTo(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token, MultivaluedMap<String, String> params);
 }
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 f4a877e..8431493 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
@@ -24,22 +24,32 @@ 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.util.SimpleHttp;
 import org.keycloak.common.ClientConnection;
+import org.keycloak.events.Details;
 import org.keycloak.events.Errors;
 import org.keycloak.events.EventBuilder;
 import org.keycloak.events.EventType;
+import org.keycloak.models.ClientModel;
 import org.keycloak.models.FederatedIdentityModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.AccessTokenResponse;
 import org.keycloak.services.ErrorPage;
 import org.keycloak.services.messages.Messages;
+import org.keycloak.sessions.AuthenticationSessionModel;
 
 import javax.ws.rs.GET;
 import javax.ws.rs.QueryParam;
 import javax.ws.rs.WebApplicationException;
 import javax.ws.rs.core.Context;
 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;
@@ -51,7 +61,7 @@ import java.util.regex.Pattern;
 /**
  * @author Pedro Igor
  */
-public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityProviderConfig> extends AbstractIdentityProvider<C> {
+public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityProviderConfig> extends AbstractIdentityProvider<C> implements TokenExchangeTo {
     protected static final Logger logger = Logger.getLogger(AbstractOAuth2IdentityProvider.class);
 
     public static final String OAUTH2_GRANT_TYPE_REFRESH_TOKEN = "refresh_token";
@@ -136,14 +146,76 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
         return null;
     }
 
+    @Override
+    public Response exchangeTo(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();
+        }
+        if (!getConfig().isStoreToken()) {
+            String brokerId = tokenUserSession.getNote(Details.IDENTITY_PROVIDER);
+            if (brokerId == null || !brokerId.equals(getConfig().getAlias())) {
+                return exchangeNotSupported();
+            }
+            return exchangeSessionToken(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+        } else {
+            return exchangeStoredToken(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+        }
+    }
+
+    protected Response exchangeStoredToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token) {
+        FederatedIdentityModel model = session.users().getFederatedIdentity(tokenSubject, getConfig().getAlias(), authorizedClient.getRealm());
+        if (model == null || model.getToken() == null) {
+            return exchangeNotLinked(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+        }
+        String accessToken = extractTokenFromResponse(model.getToken(), getAccessTokenResponseParameter());
+        if (accessToken == null) {
+            model.setToken(null);
+            session.users().updateFederatedIdentity(authorizedClient.getRealm(), tokenSubject, model);
+            return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+        }
+        AccessTokenResponse tokenResponse = new AccessTokenResponse();
+        tokenResponse.setToken(accessToken);
+        tokenResponse.setIdToken(null);
+        tokenResponse.setRefreshToken(null);
+        tokenResponse.setRefreshExpiresIn(0);
+        tokenResponse.getOtherClaims().clear();
+        tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
+        tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession, token));
+        return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
+    }
+
+    protected Response exchangeSessionToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token) {
+        String accessToken = tokenUserSession.getNote(FEDERATED_ACCESS_TOKEN);
+        if (accessToken == null) {
+            return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+        }
+        AccessTokenResponse tokenResponse = new AccessTokenResponse();
+        tokenResponse.setToken(accessToken);
+        tokenResponse.setIdToken(null);
+        tokenResponse.setRefreshToken(null);
+        tokenResponse.setRefreshExpiresIn(0);
+        tokenResponse.getOtherClaims().clear();
+        tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
+        tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession, token));
+        return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
+    }
+
+
     public BrokeredIdentityContext getFederatedIdentity(String response) {
-        String accessToken = extractTokenFromResponse(response, OAUTH2_PARAMETER_ACCESS_TOKEN);
+        String accessToken = extractTokenFromResponse(response, getAccessTokenResponseParameter());
 
         if (accessToken == null) {
             throw new IdentityBrokerException("No access token available in OAuth server response: " + response);
         }
 
-        return doGetFederatedIdentity(accessToken);
+        BrokeredIdentityContext context = doGetFederatedIdentity(accessToken);
+        context.getContextData().put(FEDERATED_ACCESS_TOKEN, accessToken);
+        return context;
+    }
+
+    protected String getAccessTokenResponseParameter() {
+        return OAUTH2_PARAMETER_ACCESS_TOKEN;
     }
 
 
@@ -186,6 +258,12 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
 
     protected abstract String getDefaultScopes();
 
+    @Override
+    public void authenticationFinished(AuthenticationSessionModel authSession, BrokeredIdentityContext context) {
+        String token = (String) context.getContextData().get(FEDERATED_ACCESS_TOKEN);
+        if (token != null) authSession.setUserSessionNote(FEDERATED_ACCESS_TOKEN, token);
+    }
+
     protected class Endpoint {
         protected AuthenticationCallback callback;
         protected RealmModel realm;
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 a5304ba..9daeb14 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
@@ -18,6 +18,7 @@ package org.keycloak.broker.oidc;
 
 import com.fasterxml.jackson.databind.JsonNode;
 import org.jboss.logging.Logger;
+import org.keycloak.OAuth2Constants;
 import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
 import org.keycloak.broker.oidc.util.JsonSimpleHttp;
 import org.keycloak.broker.provider.AuthenticationRequest;
@@ -32,9 +33,13 @@ import org.keycloak.jose.jws.JWSInput;
 import org.keycloak.jose.jws.JWSInputException;
 import org.keycloak.jose.jws.crypto.RSAProvider;
 import org.keycloak.keys.loader.PublicKeyStorageManager;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.FederatedIdentityModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
 import org.keycloak.models.UserSessionModel;
+import org.keycloak.representations.AccessToken;
 import org.keycloak.representations.AccessTokenResponse;
 import org.keycloak.representations.IDToken;
 import org.keycloak.representations.JsonWebToken;
@@ -50,6 +55,7 @@ import javax.ws.rs.GET;
 import javax.ws.rs.Path;
 import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriBuilder;
 import javax.ws.rs.core.UriInfo;
@@ -59,7 +65,7 @@ import java.security.PublicKey;
 /**
  * @author Pedro Igor
  */
-public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIdentityProviderConfig> {
+public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIdentityProviderConfig>  {
     protected static final Logger logger = Logger.getLogger(OIDCIdentityProvider.class);
 
     public static final String OAUTH2_PARAMETER_PROMPT = "prompt";
@@ -68,6 +74,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
     public static final String USER_INFO = "UserInfo";
     public static final String FEDERATED_ACCESS_TOKEN_RESPONSE = "FEDERATED_ACCESS_TOKEN_RESPONSE";
     public static final String VALIDATED_ID_TOKEN = "VALIDATED_ID_TOKEN";
+    public static final String ACCESS_TOKEN_EXPIRATION = "accessTokenExpiration";
 
     public OIDCIdentityProvider(KeycloakSession session, OIDCIdentityProviderConfig config) {
         super(session, config);
@@ -170,7 +177,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
      * @param userSession
      * @return
      */
-    public String refreshToken(KeycloakSession session, UserSessionModel userSession) {
+    public String refreshTokenForLogout(KeycloakSession session, UserSessionModel userSession) {
         String refreshToken = userSession.getNote(FEDERATED_REFRESH_TOKEN);
         try {
             return SimpleHttp.doPost(getConfig().getTokenUrl(), session)
@@ -187,7 +194,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
         long exp = Long.parseLong(userSession.getNote(FEDERATED_TOKEN_EXPIRATION));
         int currentTime = Time.currentTime();
         if (exp > 0 && currentTime > exp) {
-            String response = refreshToken(session, userSession);
+            String response = refreshTokenForLogout(session, userSession);
             AccessTokenResponse tokenResponse = null;
             try {
                 tokenResponse = JsonSerialization.readValue(response, AccessTokenResponse.class);
@@ -215,8 +222,108 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
 
     protected void processAccessTokenResponse(BrokeredIdentityContext context, AccessTokenResponse response) {
 
+
+    }
+
+
+    protected Response exchangeStoredToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token) {
+        FederatedIdentityModel model = session.users().getFederatedIdentity(tokenSubject, getConfig().getAlias(), authorizedClient.getRealm());
+        if (model == null || model.getToken() == null) {
+            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()) {
+                if (tokenResponse.getRefreshToken() == null) {
+                    return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+                }
+                String response = SimpleHttp.doPost(getConfig().getTokenUrl(), session)
+                        .param("refresh_token", tokenResponse.getRefreshToken())
+                        .param(OAUTH2_PARAMETER_GRANT_TYPE, OAUTH2_GRANT_TYPE_REFRESH_TOKEN)
+                        .param(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
+                        .param(OAUTH2_PARAMETER_CLIENT_SECRET, getConfig().getClientSecret()).asString();
+                if (response.contains("error")) {
+                    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();
+                    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;
+                    tokenUserSession.setNote(FEDERATED_TOKEN_EXPIRATION, Long.toString(accessTokenExpiration));
+                    tokenUserSession.setNote(FEDERATED_REFRESH_TOKEN, newResponse.getRefreshToken());
+                    tokenUserSession.setNote(FEDERATED_ACCESS_TOKEN, newResponse.getToken());
+                    tokenUserSession.setNote(FEDERATED_ID_TOKEN, newResponse.getIdToken());
+
+                }
+                model.setToken(response);
+                tokenResponse = newResponse;
+            } else if (exp != null) {
+                tokenResponse.setExpiresIn(exp - Time.currentTime());
+            }
+            tokenResponse.setIdToken(null);
+            tokenResponse.setRefreshToken(null);
+            tokenResponse.setRefreshExpiresIn(0);
+            tokenResponse.getOtherClaims().clear();
+            tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
+            tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession, token));
+            return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
     }
 
+    protected Response exchangeSessionToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token) {
+        try {
+            long expiration = Long.parseLong(tokenUserSession.getNote(FEDERATED_TOKEN_EXPIRATION));
+            String refreshToken = tokenUserSession.getNote(FEDERATED_REFRESH_TOKEN);
+            String accessToken = tokenUserSession.getNote(FEDERATED_ACCESS_TOKEN);
+            String idToken = tokenUserSession.getNote(FEDERATED_ID_TOKEN);
+            if (expiration == 0 || expiration > Time.currentTime()) {
+                AccessTokenResponse tokenResponse = new AccessTokenResponse();
+                tokenResponse.setExpiresIn(expiration);
+                tokenResponse.setToken(accessToken);
+                tokenResponse.setIdToken(null);
+                tokenResponse.setRefreshToken(null);
+                tokenResponse.setRefreshExpiresIn(0);
+                tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
+                tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession, token));
+                return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
+            }
+            String response = SimpleHttp.doPost(getConfig().getTokenUrl(), session)
+                    .param("refresh_token", refreshToken)
+                    .param(OAUTH2_PARAMETER_GRANT_TYPE, OAUTH2_GRANT_TYPE_REFRESH_TOKEN)
+                    .param(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
+                    .param(OAUTH2_PARAMETER_CLIENT_SECRET, getConfig().getClientSecret()).asString();
+            if (response.contains("error")) {
+                return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+            }
+            AccessTokenResponse newResponse = JsonSerialization.readValue(response, AccessTokenResponse.class);
+            long accessTokenExpiration = newResponse.getExpiresIn() > 0 ? Time.currentTime() + 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());
+            tokenUserSession.setNote(FEDERATED_ID_TOKEN, newResponse.getIdToken());
+            newResponse.setIdToken(null);
+            newResponse.setRefreshToken(null);
+            newResponse.setRefreshExpiresIn(0);
+            newResponse.getOtherClaims().clear();
+            newResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
+            newResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession, token));
+            return Response.ok(newResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+
     @Override
     public BrokeredIdentityContext getFederatedIdentity(String response) {
         AccessTokenResponse tokenResponse = null;
@@ -235,6 +342,13 @@ 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 = 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 b0b3e19..c9c394a 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
@@ -23,6 +23,8 @@ import org.jboss.resteasy.spi.ResteasyProviderFactory;
 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.common.ClientConnection;
 import org.keycloak.common.constants.ServiceAccountConstants;
 import org.keycloak.common.util.Base64Url;
@@ -34,6 +36,7 @@ import org.keycloak.events.EventType;
 import org.keycloak.models.AuthenticationFlowModel;
 import org.keycloak.models.AuthenticatedClientSessionModel;
 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;
@@ -53,6 +56,7 @@ import org.keycloak.services.managers.ClientManager;
 import org.keycloak.services.managers.ClientSessionCode;
 import org.keycloak.services.managers.RealmManager;
 import org.keycloak.services.resources.Cors;
+import org.keycloak.services.resources.IdentityBrokerService;
 import org.keycloak.services.resources.admin.permissions.AdminPermissions;
 import org.keycloak.sessions.AuthenticationSessionModel;
 import org.keycloak.util.TokenUtil;
@@ -65,6 +69,7 @@ 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;
@@ -582,10 +587,32 @@ public class TokenEndpoint {
         String requestedIssuer = formParams.getFirst(OAuth2Constants.REQUESTED_ISSUER);
 
         if (requestedIssuer == null) {
+            return exchangeClientToClient(authResult);
+        } else {
+            return exchangeToIdentityProvider(authResult, requestedIssuer);
+        }
+    }
 
+    public Response exchangeToIdentityProvider(AuthenticationManager.AuthResult authResult, String requestedIssuer) {
+        IdentityProviderModel providerModel = realm.getIdentityProviderByAlias(requestedIssuer);
+        if (providerModel == null) {
+            event.error(Errors.UNKNOWN_IDENTITY_PROVIDER);
+            throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Invalid issuer", Response.Status.BAD_REQUEST);
+        }
+
+        IdentityProvider provider = IdentityBrokerService.getIdentityProvider(session, realm, requestedIssuer);
+        if (!(provider instanceof TokenExchangeTo)) {
+            event.error(Errors.UNKNOWN_IDENTITY_PROVIDER);
+            throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Issuer does not support token exchange", Response.Status.BAD_REQUEST);
+        }
+        if (!AdminPermissions.management(session, realm).idps().canExchangeTo(client, providerModel)) {
+            logger.debug("Client not allowed to exchange for linked token");
+            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);
+        return Cors.add(request, Response.fromResponse(response)).auth().allowedOrigins(uriInfo, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
 
-        return exchangeClientToClient(authResult);
     }
 
     public Response exchangeClientToClient(AuthenticationManager.AuthResult subject) {
@@ -617,24 +644,6 @@ public class TokenEndpoint {
             throw new ErrorResponseException(OAuthErrorException.INVALID_CLIENT, "Client requires user consent", Response.Status.BAD_REQUEST);
         }
 
-        boolean exchangeFromAllowed = false;
-        for (String aud : subject.getToken().getAudience()) {
-            ClientModel audClient = realm.getClientByClientId(aud);
-            if (audClient == null) continue;
-            if (audClient.equals(client)) {
-                exchangeFromAllowed = true;
-                break;
-            }
-            if (AdminPermissions.management(session, realm).clients().canExchangeFrom(client, audClient)) {
-                exchangeFromAllowed = true;
-                break;
-            }
-        }
-        if (!exchangeFromAllowed) {
-            logger.debug("Client does not have exchange rights for audience of provided token");
-            event.error(Errors.NOT_ALLOWED);
-            throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
-        }
         if (!AdminPermissions.management(session, realm).clients().canExchangeTo(client, targetClient)) {
             logger.debug("Client does not have exchange rights for target audience");
             event.error(Errors.NOT_ALLOWED);
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java
index 4c08c20..3f07b47 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java
@@ -19,12 +19,15 @@ package org.keycloak.services.resources.admin;
 import org.jboss.logging.Logger;
 import org.jboss.resteasy.annotations.cache.NoCache;
 import org.jboss.resteasy.spi.NotFoundException;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.model.ResourceServer;
 import org.keycloak.broker.provider.IdentityProvider;
 import org.keycloak.broker.provider.IdentityProviderFactory;
 import org.keycloak.broker.provider.IdentityProviderMapper;
 import org.keycloak.broker.social.SocialIdentityProvider;
 import org.keycloak.events.admin.OperationType;
 import org.keycloak.events.admin.ResourceType;
+import org.keycloak.models.ClientModel;
 import org.keycloak.models.FederatedIdentityModel;
 import org.keycloak.models.IdentityProviderMapperModel;
 import org.keycloak.models.IdentityProviderModel;
@@ -43,8 +46,11 @@ import org.keycloak.representations.idm.ConfigPropertyRepresentation;
 import org.keycloak.representations.idm.IdentityProviderMapperRepresentation;
 import org.keycloak.representations.idm.IdentityProviderMapperTypeRepresentation;
 import org.keycloak.representations.idm.IdentityProviderRepresentation;
+import org.keycloak.representations.idm.ManagementPermissionReference;
 import org.keycloak.services.ErrorResponse;
 import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
+import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement;
+import org.keycloak.services.resources.admin.permissions.AdminPermissions;
 
 import javax.ws.rs.Consumes;
 import javax.ws.rs.DELETE;
@@ -402,5 +408,58 @@ public class IdentityProviderResource {
 
     }
 
+    /**
+     * Return object stating whether client Authorization permissions have been initialized or not and a reference
+     *
+     * @return
+     */
+    @Path("management/permissions")
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    @NoCache
+    public ManagementPermissionReference getManagementPermissions() {
+        this.auth.realm().requireViewIdentityProviders();
+
+        AdminPermissionManagement permissions = AdminPermissions.management(session, realm);
+        if (!permissions.idps().isPermissionsEnabled(identityProviderModel)) {
+            return new ManagementPermissionReference();
+        }
+        return toMgmtRef(identityProviderModel, permissions);
+    }
+
+    public static ManagementPermissionReference toMgmtRef(IdentityProviderModel model, AdminPermissionManagement permissions) {
+        ManagementPermissionReference ref = new ManagementPermissionReference();
+        ref.setEnabled(true);
+        ref.setResource(permissions.idps().resource(model).getId());
+        ref.setScopePermissions(permissions.idps().getPermissions(model));
+        return ref;
+    }
+
+
+    /**
+     * Return object stating whether client Authorization permissions have been initialized or not and a reference
+     *
+     *
+     * @return initialized manage permissions reference
+     */
+    @Path("management/permissions")
+    @PUT
+    @Produces(MediaType.APPLICATION_JSON)
+    @Consumes(MediaType.APPLICATION_JSON)
+    @NoCache
+    public ManagementPermissionReference setManagementPermissionsEnabled(ManagementPermissionReference ref) {
+        this.auth.realm().requireManageIdentityProviders();
+        AdminPermissionManagement permissions = AdminPermissions.management(session, realm);
+        permissions.idps().setPermissionsEnabled(identityProviderModel, ref.isEnabled());
+        if (ref.isEnabled()) {
+            return toMgmtRef(identityProviderModel, permissions);
+        } else {
+            return new ManagementPermissionReference();
+        }
+    }
+
+
+
+
 
 }
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 d8eb94a..4dfce43 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,6 @@ 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_FROM_SCOPE="exchange-from";
     public static final String EXCHANGE_TO_SCOPE="exchange-to";
 
     ClientModel getRealmManagementClient();
@@ -38,6 +37,7 @@ public interface AdminPermissionManagement {
     UserPermissionManagement users();
     GroupPermissionManagement groups();
     ClientPermissionManagement clients();
+    IdentityProviderPermissionManagement idps();
 
     ResourceServer realmResourceServer();
 }
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionManagement.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionManagement.java
index ccf9679..03bfa73 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionManagement.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionManagement.java
@@ -41,12 +41,8 @@ public interface ClientPermissionManagement {
 
     Map<String, String> getPermissions(ClientModel client);
 
-    boolean canExchangeFrom(ClientModel authorizedClient, ClientModel from);
-
     boolean canExchangeTo(ClientModel authorizedClient, ClientModel to);
 
-    Policy exchangeFromPermission(ClientModel client);
-
     Policy exchangeToPermission(ClientModel client);
 
     Policy mapRolesPermission(ClientModel client);
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 bbb7bf4..8aeb9ab 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
@@ -18,10 +18,8 @@ package org.keycloak.services.resources.admin.permissions;
 
 import org.jboss.logging.Logger;
 import org.keycloak.authorization.AuthorizationProvider;
-import org.keycloak.authorization.attribute.Attributes;
 import org.keycloak.authorization.common.ClientModelIdentity;
 import org.keycloak.authorization.common.DefaultEvaluationContext;
-import org.keycloak.authorization.identity.Identity;
 import org.keycloak.authorization.model.Policy;
 import org.keycloak.authorization.model.Resource;
 import org.keycloak.authorization.model.ResourceServer;
@@ -32,8 +30,6 @@ import org.keycloak.models.ClientModel;
 import org.keycloak.models.ClientTemplateModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
-import org.keycloak.models.RoleModel;
-import org.keycloak.representations.AccessToken;
 import org.keycloak.services.ForbiddenException;
 
 import java.util.Arrays;
@@ -44,7 +40,6 @@ import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Set;
 
-import static org.keycloak.services.resources.admin.permissions.AdminPermissionManagement.EXCHANGE_FROM_SCOPE;
 import static org.keycloak.services.resources.admin.permissions.AdminPermissionManagement.EXCHANGE_TO_SCOPE;
 
 /**
@@ -54,7 +49,7 @@ import static org.keycloak.services.resources.admin.permissions.AdminPermissionM
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
-class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionManagement {
+class ClientPermissions implements ClientPermissionEvaluator,  ClientPermissionManagement {
     private static final Logger logger = Logger.getLogger(ClientPermissions.class);
     protected final KeycloakSession session;
     protected final RealmModel realm;
@@ -95,11 +90,7 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
         return EXCHANGE_TO_SCOPE + ".permission.client." + client.getId();
     }
 
-    private String getExchangeFromPermissionName(ClientModel client) {
-        return EXCHANGE_FROM_SCOPE + ".permission.client." + client.getId();
-    }
-
-    private void initialize(ClientModel client) {
+     private void initialize(ClientModel client) {
         ResourceServer server = root.findOrCreateResourceServer(client);
         Scope manageScope = manageScope(server);
         if (manageScope == null) {
@@ -116,7 +107,6 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
         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 exchangeFromScope = root.initializeScope(EXCHANGE_FROM_SCOPE, server);
         Scope exchangeToScope = root.initializeScope(EXCHANGE_TO_SCOPE, server);
 
         String resourceName = getResourceName(client);
@@ -131,7 +121,6 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
             scopeset.add(mapRoleScope);
             scopeset.add(mapRoleClientScope);
             scopeset.add(mapRoleCompositeScope);
-            scopeset.add(exchangeFromScope);
             scopeset.add(exchangeToScope);
             resource.updateScopes(scopeset);
         }
@@ -170,11 +159,6 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
         if (exchangeToPermission == null) {
             Helper.addEmptyScopePermission(authz, server, exchangeToPermissionName, resource, exchangeToScope);
         }
-        String exchangeFromPermissionName = getExchangeFromPermissionName(client);
-        Policy exchangeFromPermission = authz.getStoreFactory().getPolicyStore().findByName(exchangeFromPermissionName, server.getId());
-        if (exchangeFromPermission == null) {
-            Helper.addEmptyScopePermission(authz, server, exchangeFromPermissionName, resource, exchangeFromScope);
-        }
     }
 
     private void deletePolicy(String name, ResourceServer server) {
@@ -195,7 +179,6 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
         deletePolicy(getMapRolesCompositePermissionName(client), server);
         deletePolicy(getConfigurePermissionName(client), server);
         deletePolicy(getExchangeToPermissionName(client), server);
-        deletePolicy(getExchangeFromPermissionName(client), server);
         Resource resource = authz.getStoreFactory().getResourceStore().findByName(getResourceName(client), server.getId());;
         if (resource != null) authz.getStoreFactory().getResourceStore().delete(resource.getId());
     }
@@ -223,10 +206,6 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
         return authz.getStoreFactory().getScopeStore().findByName(AdminPermissionManagement.MANAGE_SCOPE, server.getId());
     }
 
-    private Scope exchangeFromScope(ResourceServer server) {
-        return authz.getStoreFactory().getScopeStore().findByName(EXCHANGE_FROM_SCOPE, server.getId());
-    }
-
     private Scope exchangeToScope(ResourceServer server) {
         return authz.getStoreFactory().getScopeStore().findByName(EXCHANGE_TO_SCOPE, server.getId());
     }
@@ -314,60 +293,11 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
         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_FROM_SCOPE, exchangeFromPermission(client).getId());
         scopes.put(EXCHANGE_TO_SCOPE, exchangeToPermission(client).getId());
         return scopes;
     }
 
     @Override
-    public boolean canExchangeFrom(ClientModel authorizedClient, ClientModel from) {
-        if (!authorizedClient.equals(from)) {
-            ResourceServer server = resourceServer(from);
-            if (server == null) {
-                logger.debug("No resource server set up for target client");
-                return false;
-            }
-
-            Resource resource =  authz.getStoreFactory().getResourceStore().findByName(getResourceName(from), server.getId());
-            if (resource == null) {
-                logger.debug("No resource object set up for target client");
-                return false;
-            }
-
-            Policy policy = authz.getStoreFactory().getPolicyStore().findByName(getExchangeFromPermissionName(from), server.getId());
-            if (policy == null) {
-                logger.debug("No permission object set up for target client");
-                return false;
-            }
-
-            Set<Policy> associatedPolicies = policy.getAssociatedPolicies();
-            // if no policies attached to permission then just do default behavior
-            if (associatedPolicies == null || associatedPolicies.isEmpty()) {
-                logger.debug("No policies set up for permission on target client");
-                return false;
-            }
-
-            Scope scope = exchangeFromScope(server);
-            if (scope == null) {
-                logger.debug(EXCHANGE_FROM_SCOPE + " not initialized");
-                return false;
-            }
-            ClientModelIdentity identity = new ClientModelIdentity(session, authorizedClient);
-            EvaluationContext context = new DefaultEvaluationContext(identity, session) {
-                @Override
-                public Map<String, Collection<String>> getBaseAttributes() {
-                    Map<String, Collection<String>> attributes = super.getBaseAttributes();
-                    attributes.put("kc.client.id", Arrays.asList(authorizedClient.getClientId()));
-                    return attributes;
-                }
-
-            };
-            return root.evaluatePermission(resource, scope, server, context);
-        }
-        return true;
-    }
-
-    @Override
     public boolean canExchangeTo(ClientModel authorizedClient, ClientModel to) {
 
         if (!authorizedClient.equals(to)) {
@@ -602,13 +532,6 @@ class ClientPermissions implements ClientPermissionEvaluator, ClientPermissionMa
     }
 
     @Override
-    public Policy exchangeFromPermission(ClientModel client) {
-        ResourceServer server = resourceServer(client);
-        if (server == null) return null;
-        return authz.getStoreFactory().getPolicyStore().findByName(getExchangeFromPermissionName(client), server.getId());
-    }
-
-    @Override
     public Policy exchangeToPermission(ClientModel client) {
         ResourceServer server = resourceServer(client);
         if (server == null) return null;
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/IdentityProviderPermissionManagement.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/IdentityProviderPermissionManagement.java
new file mode 100644
index 0000000..a5b595d
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/IdentityProviderPermissionManagement.java
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+package org.keycloak.services.resources.admin.permissions;
+
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.IdentityProviderModel;
+
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public interface IdentityProviderPermissionManagement {
+    boolean isPermissionsEnabled(IdentityProviderModel idp);
+
+    void setPermissionsEnabled(IdentityProviderModel idp, boolean enable);
+
+    Resource resource(IdentityProviderModel idp);
+
+    Map<String, String> getPermissions(IdentityProviderModel idp);
+
+    boolean canExchangeTo(ClientModel authorizedClient, IdentityProviderModel to);
+
+    Policy exchangeToPermission(IdentityProviderModel idp);
+}
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
new file mode 100644
index 0000000..71661b1
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/IdentityProviderPermissions.java
@@ -0,0 +1,205 @@
+/*
+ * 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.
+ */
+package org.keycloak.services.resources.admin.permissions;
+
+import org.jboss.logging.Logger;
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.common.ClientModelIdentity;
+import org.keycloak.authorization.common.DefaultEvaluationContext;
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.model.Scope;
+import org.keycloak.authorization.policy.evaluation.EvaluationContext;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+import static org.keycloak.services.resources.admin.permissions.AdminPermissionManagement.EXCHANGE_TO_SCOPE;
+
+/**
+ * Manages default policies for all users.
+ *
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+class IdentityProviderPermissions implements  IdentityProviderPermissionManagement {
+    private static final Logger logger = Logger.getLogger(IdentityProviderPermissions.class);
+    protected final KeycloakSession session;
+    protected final RealmModel realm;
+    protected final AuthorizationProvider authz;
+    protected final MgmtPermissions root;
+
+    public IdentityProviderPermissions(KeycloakSession session, RealmModel realm, AuthorizationProvider authz, MgmtPermissions root) {
+        this.session = session;
+        this.realm = realm;
+        this.authz = authz;
+        this.root = root;
+    }
+
+    private String getResourceName(IdentityProviderModel idp) {
+        return "idp.resource." + idp.getInternalId();
+    }
+
+    private String getExchangeToPermissionName(IdentityProviderModel idp) {
+        return EXCHANGE_TO_SCOPE + ".permission.idp." + idp.getInternalId();
+    }
+
+    private void initialize(IdentityProviderModel idp) {
+        ResourceServer server = root.initializeRealmResourceServer();
+        Scope exchangeToScope = root.initializeScope(EXCHANGE_TO_SCOPE, server);
+
+        String resourceName = getResourceName(idp);
+        Resource resource = authz.getStoreFactory().getResourceStore().findByName(resourceName, server.getId());
+        if (resource == null) {
+            resource = authz.getStoreFactory().getResourceStore().create(resourceName, server, server.getClientId());
+            resource.setType("IdentityProvider");
+            Set<Scope> scopeset = new HashSet<>();
+            scopeset.add(exchangeToScope);
+            resource.updateScopes(scopeset);
+        }
+        String exchangeToPermissionName = getExchangeToPermissionName(idp);
+        Policy exchangeToPermission = authz.getStoreFactory().getPolicyStore().findByName(exchangeToPermissionName, server.getId());
+        if (exchangeToPermission == null) {
+            Helper.addEmptyScopePermission(authz, server, exchangeToPermissionName, resource, exchangeToScope);
+        }
+    }
+
+    private void deletePolicy(String name, ResourceServer server) {
+        Policy policy = authz.getStoreFactory().getPolicyStore().findByName(name, server.getId());
+        if (policy != null) {
+            authz.getStoreFactory().getPolicyStore().delete(policy.getId());
+        }
+
+    }
+
+    private void deletePermissions(IdentityProviderModel idp) {
+        ResourceServer server = root.initializeRealmResourceServer();
+        if (server == null) return;
+        deletePolicy(getExchangeToPermissionName(idp), server);
+        Resource resource = authz.getStoreFactory().getResourceStore().findByName(getResourceName(idp), server.getId());;
+        if (resource != null) authz.getStoreFactory().getResourceStore().delete(resource.getId());
+    }
+
+    @Override
+    public boolean isPermissionsEnabled(IdentityProviderModel idp) {
+        ResourceServer server = root.initializeRealmResourceServer();
+        if (server == null) return false;
+
+        return authz.getStoreFactory().getResourceStore().findByName(getResourceName(idp), server.getId()) != null;
+    }
+
+    @Override
+    public void setPermissionsEnabled(IdentityProviderModel idp, boolean enable) {
+        if (enable) {
+            initialize(idp);
+        } else {
+            deletePermissions(idp);
+        }
+    }
+
+
+
+    private Scope exchangeToScope(ResourceServer server) {
+        return authz.getStoreFactory().getScopeStore().findByName(EXCHANGE_TO_SCOPE, server.getId());
+    }
+
+    @Override
+    public Resource resource(IdentityProviderModel idp) {
+        ResourceServer server = root.initializeRealmResourceServer();
+        if (server == null) return null;
+        Resource resource =  authz.getStoreFactory().getResourceStore().findByName(getResourceName(idp), server.getId());
+        if (resource == null) return null;
+        return resource;
+    }
+
+
+    @Override
+    public Map<String, String> getPermissions(IdentityProviderModel idp) {
+        initialize(idp);
+        Map<String, String> scopes = new LinkedHashMap<>();
+        scopes.put(EXCHANGE_TO_SCOPE, exchangeToPermission(idp).getId());
+        return scopes;
+    }
+
+    @Override
+    public boolean canExchangeTo(ClientModel authorizedClient, IdentityProviderModel to) {
+
+        if (!authorizedClient.equals(to)) {
+            ResourceServer server = root.initializeRealmResourceServer();
+            if (server == null) {
+                logger.debug("No resource server set up for target idp");
+                return false;
+            }
+
+            Resource resource =  authz.getStoreFactory().getResourceStore().findByName(getResourceName(to), server.getId());
+            if (resource == null) {
+                logger.debug("No resource object set up for target idp");
+                return false;
+            }
+
+            Policy policy = authz.getStoreFactory().getPolicyStore().findByName(getExchangeToPermissionName(to), server.getId());
+            if (policy == null) {
+                logger.debug("No permission object set up for target idp");
+                return false;
+            }
+
+            Set<Policy> associatedPolicies = policy.getAssociatedPolicies();
+            // if no policies attached to permission then just do default behavior
+            if (associatedPolicies == null || associatedPolicies.isEmpty()) {
+                logger.debug("No policies set up for permission on target idp");
+                return false;
+            }
+
+            Scope scope = exchangeToScope(server);
+            if (scope == null) {
+                logger.debug(EXCHANGE_TO_SCOPE + " not initialized");
+                return false;
+            }
+            ClientModelIdentity identity = new ClientModelIdentity(session, authorizedClient);
+            EvaluationContext context = new DefaultEvaluationContext(identity, session) {
+                @Override
+                public Map<String, Collection<String>> getBaseAttributes() {
+                    Map<String, Collection<String>> attributes = super.getBaseAttributes();
+                    attributes.put("kc.client.id", Arrays.asList(authorizedClient.getClientId()));
+                    return attributes;
+                }
+
+            };
+            return root.evaluatePermission(resource, scope, server, context);
+        }
+        return true;
+    }
+
+    @Override
+    public Policy exchangeToPermission(IdentityProviderModel idp) {
+        ResourceServer server = root.initializeRealmResourceServer();
+        if (server == null) return null;
+        return authz.getStoreFactory().getPolicyStore().findByName(getExchangeToPermissionName(idp), server.getId());
+    }
+
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java
index fe4a11f..80812f2 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java
@@ -67,6 +67,7 @@ class MgmtPermissions implements AdminPermissionEvaluator, AdminPermissionManage
     protected GroupPermissions groups;
     protected RealmPermissions realmPermissions;
     protected ClientPermissions clientPermissions;
+    protected IdentityProviderPermissions idpPermissions;
 
 
     MgmtPermissions(KeycloakSession session, RealmModel realm) {
@@ -224,6 +225,13 @@ class MgmtPermissions implements AdminPermissionEvaluator, AdminPermissionManage
     }
 
     @Override
+    public IdentityProviderPermissions idps() {
+        if (idpPermissions != null) return idpPermissions;
+        idpPermissions = new IdentityProviderPermissions(session, realm, authz, this);
+        return idpPermissions;
+    }
+
+    @Override
     public GroupPermissions groups() {
         if (groups != null) return groups;
         groups = new GroupPermissions(session, realm, authz, this);
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 77009e7..ad9dc40 100755
--- a/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java
@@ -17,18 +17,26 @@
 package org.keycloak.social.twitter;
 
 import org.jboss.logging.Logger;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider;
 import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
 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.social.SocialIdentityProvider;
 import org.keycloak.common.ClientConnection;
+import org.keycloak.events.Details;
 import org.keycloak.events.EventBuilder;
 import org.keycloak.events.EventType;
+import org.keycloak.models.ClientModel;
 import org.keycloak.models.FederatedIdentityModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.representations.AccessTokenResponse;
 import org.keycloak.services.ErrorPage;
 import org.keycloak.services.managers.ClientSessionCode;
 import org.keycloak.services.messages.Messages;
@@ -44,6 +52,7 @@ import javax.ws.rs.WebApplicationException;
 import javax.ws.rs.core.Context;
 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.UriInfo;
 import java.net.URI;
@@ -52,7 +61,10 @@ import java.net.URI;
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
 public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2IdentityProviderConfig> implements
-        SocialIdentityProvider<OAuth2IdentityProviderConfig> {
+        SocialIdentityProvider<OAuth2IdentityProviderConfig>, TokenExchangeTo {
+
+    String TWITTER_TOKEN_TYPE="twitter";
+
 
     protected static final Logger logger = Logger.getLogger(TwitterIdentityProvider.class);
 
@@ -90,6 +102,62 @@ 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) {
+        String requestedType = params.getFirst(OAuth2Constants.REQUESTED_TOKEN_TYPE);
+        if (requestedType != null && !requestedType.equals(TWITTER_TOKEN_TYPE)) {
+            return exchangeUnsupportedRequiredType();
+        }
+        if (!getConfig().isStoreToken()) {
+            String brokerId = tokenUserSession.getNote(Details.IDENTITY_PROVIDER);
+            if (brokerId == null || !brokerId.equals(getConfig().getAlias())) {
+                return exchangeNotSupported();
+            }
+            return exchangeSessionToken(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+        } else {
+            return exchangeStoredToken(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+        }
+    }
+
+    protected Response exchangeStoredToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, org.keycloak.representations.AccessToken token) {
+        FederatedIdentityModel model = session.users().getFederatedIdentity(tokenSubject, getConfig().getAlias(), authorizedClient.getRealm());
+        if (model == null || model.getToken() == null) {
+            return exchangeNotLinked(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+        }
+        String accessToken = model.getToken();
+        if (accessToken == null) {
+            model.setToken(null);
+            session.users().updateFederatedIdentity(authorizedClient.getRealm(), tokenSubject, model);
+            return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+        }
+        AccessTokenResponse tokenResponse = new AccessTokenResponse();
+        tokenResponse.setToken(accessToken);
+        tokenResponse.setIdToken(null);
+        tokenResponse.setRefreshToken(null);
+        tokenResponse.setRefreshExpiresIn(0);
+        tokenResponse.getOtherClaims().clear();
+        tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, TWITTER_TOKEN_TYPE);
+        tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession, token));
+        return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
+    }
+
+    protected Response exchangeSessionToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, org.keycloak.representations.AccessToken token) {
+        String accessToken = tokenUserSession.getNote(AbstractOAuth2IdentityProvider.FEDERATED_ACCESS_TOKEN);
+        if (accessToken == null) {
+            return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+        }
+        AccessTokenResponse tokenResponse = new AccessTokenResponse();
+        tokenResponse.setToken(accessToken);
+        tokenResponse.setIdToken(null);
+        tokenResponse.setRefreshToken(null);
+        tokenResponse.setRefreshExpiresIn(0);
+        tokenResponse.getOtherClaims().clear();
+        tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, TWITTER_TOKEN_TYPE);
+        tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession, token));
+        return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
+    }
+
+
     protected class Endpoint {
         protected RealmModel realm;
         protected AuthenticationCallback callback;
@@ -142,6 +210,7 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
                 identity.setUsername(twitterUser.getScreenName());
                 identity.setName(twitterUser.getName());
 
+
                 StringBuilder tokenBuilder = new StringBuilder();
 
                 tokenBuilder.append("{");
@@ -150,8 +219,12 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
                 tokenBuilder.append("\"screen_name\":").append("\"").append(oAuthAccessToken.getScreenName()).append("\"").append(",");
                 tokenBuilder.append("\"user_id\":").append("\"").append(oAuthAccessToken.getUserId()).append("\"");
                 tokenBuilder.append("}");
+                String token = tokenBuilder.toString();
+                if (getConfig().isStoreToken()) {
+                    identity.setToken(token);
+                }
+                identity.getContextData().put(AbstractOAuth2IdentityProvider.FEDERATED_ACCESS_TOKEN, token);
 
-                identity.setToken(tokenBuilder.toString());
                 identity.setIdpConfig(getConfig());
                 identity.setCode(state);
 
@@ -178,4 +251,11 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
     public Response retrieveToken(KeycloakSession session, FederatedIdentityModel identity) {
         return Response.ok(identity.getToken()).type(MediaType.APPLICATION_JSON).build();
     }
+
+    @Override
+    public void authenticationFinished(AuthenticationSessionModel authSession, BrokeredIdentityContext context) {
+        authSession.setUserSessionNote(AbstractOAuth2IdentityProvider.FEDERATED_ACCESS_TOKEN, (String)context.getContextData().get(AbstractOAuth2IdentityProvider.FEDERATED_ACCESS_TOKEN));
+
+    }
+
 }
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
new file mode 100644
index 0000000..65a945e
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/LinkAndExchangeServlet.java
@@ -0,0 +1,176 @@
+/*
+ * 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.
+ */
+package org.keycloak.testsuite.adapter.servlet;
+
+import org.junit.Assert;
+import org.keycloak.KeycloakSecurityContext;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.common.util.KeycloakUriBuilder;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.AccessTokenResponse;
+import org.keycloak.util.BasicAuthHelper;
+import org.keycloak.util.JsonSerialization;
+
+import javax.servlet.ServletException;
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.ws.rs.core.HttpHeaders;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.UnsupportedEncodingException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+@WebServlet("/client-linking")
+public class LinkAndExchangeServlet extends HttpServlet {
+
+    private String getPostDataString(HashMap<String, String> params) throws UnsupportedEncodingException{
+        StringBuilder result = new StringBuilder();
+        boolean first = true;
+        for(Map.Entry<String, String> entry : params.entrySet()){
+            if (first)
+                first = false;
+            else
+                result.append("&");
+
+            result.append(URLEncoder.encode(entry.getKey(), "UTF-8"));
+            result.append("=");
+            result.append(URLEncoder.encode(entry.getValue(), "UTF-8"));
+        }
+
+        return result.toString();
+    }
+
+    public AccessTokenResponse doTokenExchange(String realm, String token, String requestedIssuer,
+                                               String clientId, String clientSecret) throws Exception {
+        try {
+            String exchangeUrl = KeycloakUriBuilder.fromUri(ServletTestUtils.getAuthServerUrlBase())
+                    .path("/auth/realms/{realm}/protocol/openid-connect/token").build(realm).toString();
+
+            URL url = new URL(exchangeUrl);
+            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+            conn.setRequestMethod("POST");
+            conn.setDoInput(true);
+            conn.setDoOutput(true);
+            HashMap<String, String> parameters = new HashMap<>();
+            if (clientSecret != null) {
+                String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
+                conn.setRequestProperty(HttpHeaders.AUTHORIZATION, authorization);
+            } else {
+                parameters.put("client_id", clientId);
+
+            }
+
+            parameters.put(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE);
+            parameters.put(OAuth2Constants.SUBJECT_TOKEN, token);
+            parameters.put(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
+            parameters.put(OAuth2Constants.REQUESTED_ISSUER, requestedIssuer);
+
+            OutputStream os = conn.getOutputStream();
+            BufferedWriter writer = new BufferedWriter(
+                    new OutputStreamWriter(os, "UTF-8"));
+            writer.write(getPostDataString(parameters));
+
+            writer.flush();
+            writer.close();
+            os.close();
+            AccessTokenResponse tokenResponse = JsonSerialization.readValue(conn.getInputStream(), AccessTokenResponse.class);
+            conn.getInputStream().close();
+            return tokenResponse;
+        } finally {
+        }
+    }
+
+    @Override
+    protected void doGet(HttpServletRequest request, HttpServletResponse resp) throws ServletException, IOException {
+        resp.setHeader("Cache-Control", "no-cache");
+        if (request.getRequestURI().endsWith("/link") && request.getParameter("response") == null) {
+            String provider = request.getParameter("provider");
+            String realm = request.getParameter("realm");
+            KeycloakSecurityContext session = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
+            AccessToken token = session.getToken();
+            String tokenString = session.getTokenString();
+
+            String clientId = token.getAudience()[0];
+            String linkUrl = null;
+            try {
+                AccessTokenResponse response = doTokenExchange(realm, tokenString, provider,  clientId, "password");
+                String error = (String)response.getOtherClaims().get("error");
+                if (error != null) {
+                    System.out.println("*** error : " + error);
+                    System.out.println("*** link-url: " + response.getOtherClaims().get("account-link-url"));
+                    linkUrl = (String)response.getOtherClaims().get("account-link-url");
+                } else {
+                    Assert.assertNotNull(response.getToken());
+                    resp.setStatus(200);
+                    resp.setContentType("text/html");
+                    PrintWriter pw = resp.getWriter();
+                    pw.printf("<html><head><title>%s</title></head><body>", "Client Linking");
+                    pw.println("Account Linked");
+                    pw.print("</body></html>");
+                    pw.flush();
+                    return;
+                }
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            }
+
+            String redirectUri = KeycloakUriBuilder.fromUri(request.getRequestURL().toString())
+                    .replaceQuery(null)
+                    .queryParam("response", "true").build().toString();
+            String accountLinkUrl = KeycloakUriBuilder.fromUri(linkUrl)
+                    .queryParam("redirect_uri", redirectUri).build().toString();
+            resp.setStatus(302);
+            resp.setHeader("Location", accountLinkUrl);
+        } else if (request.getRequestURI().endsWith("/link") && request.getParameter("response") != null) {
+            resp.setStatus(200);
+            resp.setContentType("text/html");
+            PrintWriter pw = resp.getWriter();
+            pw.printf("<html><head><title>%s</title></head><body>", "Client Linking");
+            String error = request.getParameter("link_error");
+            if (error != null) {
+                pw.println("Link error: " + error);
+            } else {
+                pw.println("Account Linked");
+            }
+            pw.print("</body></html>");
+            pw.flush();
+        } else {
+            resp.setStatus(200);
+            resp.setContentType("text/html");
+            PrintWriter pw = resp.getWriter();
+            pw.printf("<html><head><title>%s</title></head><body>", "Client Linking");
+            pw.println("Unknown request: " + request.getRequestURL().toString());
+            pw.print("</body></html>");
+            pw.flush();
+
+        }
+
+    }
+}
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
new file mode 100644
index 0000000..bbc5cdb
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractLinkAndExchangeTest.java
@@ -0,0 +1,657 @@
+/*
+ * 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.
+ */
+package org.keycloak.testsuite.adapter.servlet;
+
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.container.test.api.OperateOnDeployment;
+import org.jboss.arquillian.graphene.page.Page;
+import org.jboss.arquillian.test.api.ArquillianResource;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.common.util.Base64Url;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.Constants;
+import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
+import org.keycloak.representations.AccessTokenResponse;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.FederatedIdentityRepresentation;
+import org.keycloak.representations.idm.IdentityProviderRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation;
+import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement;
+import org.keycloak.services.resources.admin.permissions.AdminPermissions;
+import org.keycloak.testsuite.ActionURIUtils;
+import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest;
+import org.keycloak.testsuite.admin.ApiUtil;
+import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
+import org.keycloak.testsuite.broker.BrokerTestTools;
+import org.keycloak.testsuite.page.AbstractPageWithInjectedUrl;
+import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
+import org.keycloak.testsuite.pages.ErrorPage;
+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.JsonSerialization;
+
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.core.UriBuilder;
+import java.net.URL;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT;
+import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT_LINKS;
+import static org.keycloak.models.Constants.ACCOUNT_MANAGEMENT_CLIENT_ID;
+import static org.keycloak.testsuite.admin.ApiUtil.createUserAndResetPasswordWithAdminClient;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public abstract class AbstractLinkAndExchangeTest extends AbstractServletsAdapterTest {
+    public static final String CHILD_IDP = "child";
+    public static final String PARENT_IDP = "parent-idp";
+    public static final String PARENT_USERNAME = "parent";
+
+    @Page
+    protected LoginUpdateProfilePage loginUpdateProfilePage;
+
+    @Page
+    protected AccountUpdateProfilePage profilePage;
+
+    @Page
+    private LoginPage loginPage;
+
+    @Page
+    protected ErrorPage errorPage;
+
+    public static class ClientApp extends AbstractPageWithInjectedUrl {
+
+        public static final String DEPLOYMENT_NAME = "client-linking";
+
+        @ArquillianResource
+        @OperateOnDeployment(DEPLOYMENT_NAME)
+        private URL url;
+
+        @Override
+        public URL getInjectedUrl() {
+            return url;
+        }
+
+    }
+
+    @Page
+    private ClientApp appPage;
+
+    @Override
+    public void beforeAuthTest() {
+    }
+    
+    @Override
+    public void addAdapterTestRealms(List<RealmRepresentation> testRealms) {
+        RealmRepresentation realm = new RealmRepresentation();
+        realm.setRealm(CHILD_IDP);
+        realm.setEnabled(true);
+        ClientRepresentation servlet = new ClientRepresentation();
+        servlet.setClientId("client-linking");
+        servlet.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+        String uri = "/client-linking";
+        if (!isRelative()) {
+            uri = appServerContextRootPage.toString() + uri;
+        }
+        servlet.setAdminUrl(uri);
+        servlet.setDirectAccessGrantsEnabled(true);
+        servlet.setBaseUrl(uri);
+        servlet.setRedirectUris(new LinkedList<>());
+        servlet.getRedirectUris().add(uri + "/*");
+        servlet.setSecret("password");
+        servlet.setFullScopeAllowed(true);
+        realm.setClients(new LinkedList<>());
+        realm.getClients().add(servlet);
+        testRealms.add(realm);
+
+
+        realm = new RealmRepresentation();
+        realm.setRealm(PARENT_IDP);
+        realm.setEnabled(true);
+
+        testRealms.add(realm);
+
+    }
+
+
+    @Deployment(name = ClientApp.DEPLOYMENT_NAME)
+    protected static WebArchive accountLink() {
+        return servletDeployment(ClientApp.DEPLOYMENT_NAME, LinkAndExchangeServlet.class, ServletTestUtils.class);
+    }
+    
+    @Before
+    public void addIdpUser() {
+        RealmResource realm = adminClient.realms().realm(PARENT_IDP);
+        UserRepresentation user = new UserRepresentation();
+        user.setUsername(PARENT_USERNAME);
+        user.setEnabled(true);
+        String userId = createUserAndResetPasswordWithAdminClient(realm, user, "password");
+
+    }
+
+    private String childUserId = null;
+
+
+    @Before
+    public void addChildUser() {
+        RealmResource realm = adminClient.realms().realm(CHILD_IDP);
+        UserRepresentation user = new UserRepresentation();
+        user.setUsername("child");
+        user.setEnabled(true);
+        childUserId = createUserAndResetPasswordWithAdminClient(realm, user, "password");
+        UserRepresentation user2 = new UserRepresentation();
+        user2.setUsername("child2");
+        user2.setEnabled(true);
+        String user2Id = createUserAndResetPasswordWithAdminClient(realm, user2, "password");
+
+        // have to add a role as undertow default auth manager doesn't like "*". todo we can remove this eventually as undertow fixes this in later versions
+        realm.roles().create(new RoleRepresentation("user", null, false));
+        RoleRepresentation role = realm.roles().get("user").toRepresentation();
+        List<RoleRepresentation> roles = new LinkedList<>();
+        roles.add(role);
+        realm.users().get(childUserId).roles().realmLevel().add(roles);
+        realm.users().get(user2Id).roles().realmLevel().add(roles);
+        ClientRepresentation brokerService = realm.clients().findByClientId(Constants.BROKER_SERVICE_CLIENT_ID).get(0);
+        role = realm.clients().get(brokerService.getId()).roles().get(Constants.READ_TOKEN_ROLE).toRepresentation();
+        roles.clear();
+        roles.add(role);
+        realm.users().get(childUserId).roles().clientLevel(brokerService.getId()).add(roles);
+        realm.users().get(user2Id).roles().clientLevel(brokerService.getId()).add(roles);
+
+    }
+
+    public static void setupRealm(KeycloakSession session) {
+        RealmModel realm = session.realms().getRealmByName(CHILD_IDP);
+        ClientModel client = realm.getClientByClientId("client-linking");
+        IdentityProviderModel idp = realm.getIdentityProviderByAlias(PARENT_IDP);
+        Assert.assertNotNull(idp);
+
+        AdminPermissionManagement management = AdminPermissions.management(session, realm);
+        management.idps().setPermissionsEnabled(idp, true);
+        ClientPolicyRepresentation clientRep = new ClientPolicyRepresentation();
+        clientRep.setName("toIdp");
+        clientRep.addClient(client.getId());
+        ResourceServer server = management.realmResourceServer();
+        Policy clientPolicy = management.authz().getStoreFactory().getPolicyStore().create(clientRep, server);
+        management.idps().exchangeToPermission(idp).addAssociatedPolicy(clientPolicy);
+
+    }
+    @Before
+    public void createBroker() {
+        createParentChild();
+        testingClient.server().run(AbstractLinkAndExchangeTest::setupRealm);
+    }
+
+    public void createParentChild() {
+        BrokerTestTools.createKcOidcBroker(adminClient, CHILD_IDP, PARENT_IDP, suiteContext);
+    }
+
+
+    @Test
+    public void testErrorConditions() throws Exception {
+
+        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())
+                .path("link");
+        String clientLinkUrl = 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);
+        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);
+
+        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);
+
+        logoutAll();
+
+        navigateTo(clientLinkUrl);
+        Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
+        loginPage.login("child", "password");
+
+        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);
+
+        links = realm.users().get(childUserId).getFederatedIdentity();
+        Assert.assertTrue(links.isEmpty());
+
+        logoutAll();
+
+
+
+
+
+
+    }
+
+    @Test
+    public void testAccountLink() throws Exception {
+        RealmResource realm = adminClient.realms().realm(CHILD_IDP);
+        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();
+        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"));
+
+        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());
+
+        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();
+        navigateTo(logoutUri);
+        logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder()).build(PARENT_IDP).toString();
+        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(driver);
+
+        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(driver);
+
+        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(driver);
+    }
+
+    
+
+}
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
new file mode 100644
index 0000000..5230156
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/undertow/servlet/UndertowLinkAndExchangeTest.java
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+package org.keycloak.testsuite.adapter.undertow.servlet;
+
+import org.junit.Test;
+import org.keycloak.testsuite.adapter.servlet.AbstractClientInitiatedAccountLinkTest;
+import org.keycloak.testsuite.adapter.servlet.AbstractLinkAndExchangeTest;
+import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
+
+/**
+ *
+ * @author <a href="mailto:vramik@redhat.com">Vlastislav Ramik</a>
+ */
+@AppServerContainer("auth-server-undertow")
+public class UndertowLinkAndExchangeTest extends AbstractLinkAndExchangeTest {
+
+    //@Test
+    public void testUi() throws Exception {
+        Thread.sleep(1000000000);
+
+    }
+
+    @Override
+    @Test
+    public void testAccountLink() throws Exception {
+        super.testAccountLink();
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenExchangeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenExchangeTest.java
index 2889118..fc6b5ae 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenExchangeTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenExchangeTest.java
@@ -102,15 +102,6 @@ public class TokenExchangeTest extends AbstractKeycloakTest {
         illegal.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
         illegal.setFullScopeAllowed(false);
 
-        ClientModel illegalTo = realm.addClient("illegal-to");
-        illegalTo.setClientId("illegal-to");
-        illegalTo.setPublicClient(false);
-        illegalTo.setDirectAccessGrantsEnabled(true);
-        illegalTo.setEnabled(true);
-        illegalTo.setSecret("secret");
-        illegalTo.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
-        illegalTo.setFullScopeAllowed(false);
-
         ClientModel legal = realm.addClient("legal");
         legal.setClientId("legal");
         legal.setPublicClient(false);
@@ -131,15 +122,6 @@ public class TokenExchangeTest extends AbstractKeycloakTest {
         Policy clientPolicy = management.authz().getStoreFactory().getPolicyStore().create(clientRep, server);
         management.clients().exchangeToPermission(target).addAssociatedPolicy(clientPolicy);
 
-        management.clients().setPermissionsEnabled(clientExchanger, true);
-        ClientPolicyRepresentation client2Rep = new ClientPolicyRepresentation();
-        client2Rep.setName("from");
-        client2Rep.addClient(legal.getId());
-        client2Rep.addClient(illegalTo.getId());
-        Policy client2Policy = management.authz().getStoreFactory().getPolicyStore().create(client2Rep, server);
-        management.clients().exchangeFromPermission(clientExchanger).addAssociatedPolicy(client2Policy);
-
-
         UserModel user = session.users().addUser(realm, "user");
         user.setEnabled(true);
         session.userCredentialManager().updateCredential(realm, user, UserCredentialModel.password("password"));
@@ -194,10 +176,6 @@ public class TokenExchangeTest extends AbstractKeycloakTest {
             response = oauth.doTokenExchange(TEST, accessToken, "target", "illegal", "secret");
             Assert.assertEquals(403, response.getStatusCode());
         }
-        {
-            response = oauth.doTokenExchange(TEST, accessToken, "target", "illegal-to", "secret");
-            Assert.assertEquals(403, response.getStatusCode());
-        }
 
 
     }
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 f261105..b857d94 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
@@ -1341,7 +1341,6 @@ manage-authz-group-scope-description=Policies that decide if an admin can manage
 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.
-exchange-from-authz-client-scope-description=Policies that decide which clients are allowed to exchange tokens that were generated for this client.
 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