keycloak-aplcache

KEYCLOAK-5491 KEYCLOAK-5492

9/15/2017 5:30:45 PM

Changes

testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenExchangeTest.java 182(+0 -182)

Details

diff --git a/core/src/main/java/org/keycloak/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java
index 20d6e73..244de98 100644
--- a/core/src/main/java/org/keycloak/OAuth2Constants.java
+++ b/core/src/main/java/org/keycloak/OAuth2Constants.java
@@ -95,6 +95,7 @@ public interface OAuth2Constants {
 
     String TOKEN_EXCHANGE_GRANT_TYPE="urn:ietf:params:oauth:grant-type:token-exchange";
     String AUDIENCE="audience";
+    String REQUESTED_SUBJECT="requested_subject";
     String SUBJECT_TOKEN="subject_token";
     String SUBJECT_TOKEN_TYPE="subject_token_type";
     String REQUESTED_TOKEN_TYPE="requested_token_type";
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 cd9a421..701e2d8 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
@@ -93,50 +93,47 @@ public abstract class AbstractIdentityProvider<C extends IdentityProviderModel> 
         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, "identity provider is not linked");
+    public Response exchangeNotLinked(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject) {
+        return exchangeErrorResponse(uriInfo, authorizedClient, tokenUserSession, "identity provider is not linked");
     }
 
-    public Response exchangeNotLinkedNoStore(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token) {
-        return exchangeErrorResponse(uriInfo, authorizedClient, tokenUserSession, token, "identity provider is not linked, can only link to current user session");
+    public Response exchangeNotLinkedNoStore(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject) {
+        return exchangeErrorResponse(uriInfo, authorizedClient, tokenUserSession, "identity provider is not linked, can only link to current user session");
     }
 
-    protected Response exchangeErrorResponse(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, AccessToken token, String reason) {
+    protected Response exchangeErrorResponse(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, 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);
+        String accountLinkUrl = getLinkingUrl(uriInfo, authorizedClient, tokenUserSession);
         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();
+    protected String getLinkingUrl(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession) {
+        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);
         }
-        return null;
-    }
-
-    public Response exchangeTokenExpired(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token) {
-        return exchangeErrorResponse(uriInfo, authorizedClient, tokenUserSession, token, "token_expired");
+        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();
+    }
+
+    public Response exchangeTokenExpired(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject) {
+        return exchangeErrorResponse(uriInfo, authorizedClient, tokenUserSession, "token_expired");
     }
 
     public Response exchangeUnsupportedRequiredType() {
diff --git a/server-spi-private/src/main/java/org/keycloak/broker/provider/ExchangeTokenToIdentityProviderToken.java b/server-spi-private/src/main/java/org/keycloak/broker/provider/ExchangeTokenToIdentityProviderToken.java
index f97887a..0478051 100644
--- a/server-spi-private/src/main/java/org/keycloak/broker/provider/ExchangeTokenToIdentityProviderToken.java
+++ b/server-spi-private/src/main/java/org/keycloak/broker/provider/ExchangeTokenToIdentityProviderToken.java
@@ -35,9 +35,8 @@ public interface ExchangeTokenToIdentityProviderToken {
      * @param authorizedClient client requesting exchange
      * @param tokenUserSession UserSessionModel of token exchanging from
      * @param tokenSubject UserModel of token exchanging from
-     * @param token access token representation of token exchanging from
      * @param params form parameters received for requested exchange
      * @return
      */
-    Response exchangeFromToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token, MultivaluedMap<String, String> params);
+    Response exchangeFromToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, MultivaluedMap<String, String> params);
 }
diff --git a/server-spi-private/src/main/java/org/keycloak/events/Details.java b/server-spi-private/src/main/java/org/keycloak/events/Details.java
index 5e3a9b3..bfe9073 100755
--- a/server-spi-private/src/main/java/org/keycloak/events/Details.java
+++ b/server-spi-private/src/main/java/org/keycloak/events/Details.java
@@ -46,6 +46,9 @@ public interface Details {
     String NODE_HOST = "node_host";
     String REASON = "reason";
     String REVOKED_CLIENT = "revoked_client";
+    String AUDIENCE = "audience";
+    String REQUESTED_ISSUER = "requested_issuer";
+    String REQUESTED_SUBJECT = "requested_subject";
     String CLIENT_SESSION_STATE = "client_session_state";
     String CLIENT_SESSION_HOST = "client_session_host";
     String RESTART_AFTER_TIMEOUT = "restart_after_timeout";
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 468dc4b..4e2ea95 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
@@ -148,7 +148,7 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
     }
 
     @Override
-    public Response exchangeFromToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token, MultivaluedMap<String, String> params) {
+    public Response exchangeFromToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, MultivaluedMap<String, String> params) {
         String requestedType = params.getFirst(OAuth2Constants.REQUESTED_TOKEN_TYPE);
         if (requestedType != null && !requestedType.equals(OAuth2Constants.ACCESS_TOKEN_TYPE)) {
             return exchangeUnsupportedRequiredType();
@@ -156,24 +156,24 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
         if (!getConfig().isStoreToken()) {
             String brokerId = tokenUserSession.getNote(Details.IDENTITY_PROVIDER);
             if (brokerId == null || !brokerId.equals(getConfig().getAlias())) {
-                return exchangeNotLinkedNoStore(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+                return exchangeNotLinkedNoStore(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
             }
-            return exchangeSessionToken(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+            return exchangeSessionToken(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
         } else {
-            return exchangeStoredToken(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+            return exchangeStoredToken(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
         }
     }
 
-    protected Response exchangeStoredToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token) {
+    protected Response exchangeStoredToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject) {
         FederatedIdentityModel model = session.users().getFederatedIdentity(tokenSubject, getConfig().getAlias(), authorizedClient.getRealm());
         if (model == null || model.getToken() == null) {
-            return exchangeNotLinked(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+            return exchangeNotLinked(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
         }
         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);
+            return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
         }
         AccessTokenResponse tokenResponse = new AccessTokenResponse();
         tokenResponse.setToken(accessToken);
@@ -182,14 +182,14 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
         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));
+        tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession));
         return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
     }
 
-    protected Response exchangeSessionToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token) {
+    protected Response exchangeSessionToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject) {
         String accessToken = tokenUserSession.getNote(FEDERATED_ACCESS_TOKEN);
         if (accessToken == null) {
-            return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+            return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
         }
         AccessTokenResponse tokenResponse = new AccessTokenResponse();
         tokenResponse.setToken(accessToken);
@@ -198,7 +198,7 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
         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));
+        tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession));
         return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
     }
 
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 ca6a76f..1dbce4c 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
@@ -225,10 +225,10 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
     }
 
 
-    protected Response exchangeStoredToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, AccessToken token) {
+    protected Response exchangeStoredToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject) {
         FederatedIdentityModel model = session.users().getFederatedIdentity(tokenSubject, getConfig().getAlias(), authorizedClient.getRealm());
         if (model == null || model.getToken() == null) {
-            return exchangeNotLinked(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+            return exchangeNotLinked(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
         }
         try {
             String modelTokenString = model.getToken();
@@ -236,7 +236,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
             Integer exp = (Integer)tokenResponse.getOtherClaims().get(ACCESS_TOKEN_EXPIRATION);
             if (exp != null && exp < Time.currentTime()) {
                 if (tokenResponse.getRefreshToken() == null) {
-                    return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+                    return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
                 }
                 String response = SimpleHttp.doPost(getConfig().getTokenUrl(), session)
                         .param("refresh_token", tokenResponse.getRefreshToken())
@@ -247,7 +247,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
                     logger.debugv("Error refreshing token, refresh token expiration?: {0}", response);
                     model.setToken(null);
                     session.users().updateFederatedIdentity(authorizedClient.getRealm(), tokenSubject, model);
-                    return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+                    return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
                 }
                 AccessTokenResponse newResponse = JsonSerialization.readValue(response, AccessTokenResponse.class);
                 if (newResponse.getExpiresIn() > 0) {
@@ -274,14 +274,14 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
             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));
+            tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession));
             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) {
+    protected Response exchangeSessionToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject) {
         try {
             long expiration = Long.parseLong(tokenUserSession.getNote(FEDERATED_TOKEN_EXPIRATION));
             String refreshToken = tokenUserSession.getNote(FEDERATED_REFRESH_TOKEN);
@@ -295,7 +295,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
                 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));
+                tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession));
                 return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
             }
             String response = SimpleHttp.doPost(getConfig().getTokenUrl(), session)
@@ -305,7 +305,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
                     .param(OAUTH2_PARAMETER_CLIENT_SECRET, getConfig().getClientSecret()).asString();
             if (response.contains("error")) {
                 logger.debugv("Error refreshing token, refresh token expiration?: {0}", response);
-                return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+                return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
             }
             AccessTokenResponse newResponse = JsonSerialization.readValue(response, AccessTokenResponse.class);
             long accessTokenExpiration = newResponse.getExpiresIn() > 0 ? Time.currentTime() + newResponse.getExpiresIn() : 0;
@@ -318,7 +318,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
             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));
+            newResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession));
             return Response.ok(newResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
         } catch (IOException e) {
             throw new RuntimeException(e);
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 2a2f80e..056932d 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
@@ -41,6 +41,7 @@ import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.protocol.oidc.OIDCLoginProtocol;
 import org.keycloak.protocol.oidc.TokenManager;
 import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
@@ -56,6 +57,7 @@ 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.AdminAuth;
 import org.keycloak.services.resources.admin.permissions.AdminPermissions;
 import org.keycloak.sessions.AuthenticationSessionModel;
 import org.keycloak.util.TokenUtil;
@@ -122,7 +124,7 @@ public class TokenEndpoint {
     }
 
     @POST
-    public Response build() {
+    public Response processGrantRequest() {
         formParams = request.getDecodedFormParameters();
         grantType = formParams.getFirst(OIDCLoginProtocol.GRANT_TYPE_PARAM);
 
@@ -133,15 +135,15 @@ public class TokenEndpoint {
 
         switch (action) {
             case AUTHORIZATION_CODE:
-                return buildAuthorizationCodeAccessTokenResponse();
+                return codeToToken();
             case REFRESH_TOKEN:
-                return buildRefreshToken();
+                return refreshTokenGrant();
             case PASSWORD:
-                return buildResourceOwnerPasswordCredentialsGrant();
+                return resourceOwnerPasswordCredentialsGrant();
             case CLIENT_CREDENTIALS:
-                return buildClientCredentialsGrant();
+                return clientCredentialsGrant();
             case TOKEN_EXCHANGE:
-                return buildTokenExchange();
+                return tokenExchange();
         }
 
         throw new RuntimeException("Unknown action " + action);
@@ -216,7 +218,7 @@ public class TokenEndpoint {
         event.detail(Details.GRANT_TYPE, grantType);
     }
 
-    public Response buildAuthorizationCodeAccessTokenResponse() {
+    public Response codeToToken() {
         String code = formParams.getFirst(OAuth2Constants.CODE);
         if (code == null) {
             event.error(Errors.INVALID_CODE);
@@ -370,7 +372,7 @@ public class TokenEndpoint {
         return Cors.add(request, Response.ok(res).type(MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(uriInfo, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
     }
 
-    public Response buildRefreshToken() {
+    public Response refreshTokenGrant() {
         String refreshToken = formParams.getFirst(OAuth2Constants.REFRESH_TOKEN);
         if (refreshToken == null) {
             throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "No refresh token", Response.Status.BAD_REQUEST);
@@ -431,7 +433,7 @@ public class TokenEndpoint {
         }
     }
 
-    public Response buildResourceOwnerPasswordCredentialsGrant() {
+    public Response resourceOwnerPasswordCredentialsGrant() {
         event.detail(Details.AUTH_METHOD, "oauth_credentials");
 
         if (!client.isDirectAccessGrantsEnabled()) {
@@ -495,7 +497,7 @@ public class TokenEndpoint {
         return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(uriInfo, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
     }
 
-    public Response buildClientCredentialsGrant() {
+    public Response clientCredentialsGrant() {
         if (client.isBearerOnly()) {
             event.error(Errors.INVALID_CLIENT);
             throw new ErrorResponseException(OAuthErrorException.UNAUTHORIZED_CLIENT, "Bearer-only client not allowed to retrieve service account", Response.Status.UNAUTHORIZED);
@@ -564,34 +566,93 @@ public class TokenEndpoint {
         return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(uriInfo, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
     }
 
-    public Response buildTokenExchange() {
+    public Response tokenExchange() {
         event.detail(Details.AUTH_METHOD, "oauth_credentials");
+        event.client(client);
+
+        UserModel tokenUser = null;
+        UserSessionModel tokenSession = null;
+        AccessToken token = null;
 
         String subjectToken = formParams.getFirst(OAuth2Constants.SUBJECT_TOKEN);
-        String subjectTokenType = formParams.getFirst(OAuth2Constants.SUBJECT_TOKEN_TYPE);
-        if (subjectTokenType != null && !subjectTokenType.equals(OAuth2Constants.ACCESS_TOKEN_TYPE)) {
-            event.error(Errors.INVALID_TOKEN);
-            throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Invalid token type, must be access token", Response.Status.BAD_REQUEST);
+        if (subjectToken != null) {
+            String subjectTokenType = formParams.getFirst(OAuth2Constants.SUBJECT_TOKEN_TYPE);
+            if (subjectTokenType != null && !subjectTokenType.equals(OAuth2Constants.ACCESS_TOKEN_TYPE)) {
+                event.error(Errors.INVALID_TOKEN);
+                throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Invalid token type, must be access token", Response.Status.BAD_REQUEST);
+
+            }
+
+
+            AuthenticationManager.AuthResult authResult = AuthenticationManager.verifyIdentityToken(session, realm, uriInfo, clientConnection, true, true, false, subjectToken, headers);
+            if (authResult == null) {
+                event.error(Errors.INVALID_TOKEN);
+                throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Invalid token", Response.Status.BAD_REQUEST);
+            }
 
+            tokenUser = authResult.getUser();
+            tokenSession = authResult.getSession();
+            token = authResult.getToken();
         }
 
+        String requestedSubject = formParams.getFirst(OAuth2Constants.REQUESTED_SUBJECT);
+        if (requestedSubject != null) {
+            event.detail(Details.REQUESTED_SUBJECT, requestedSubject);
+            UserModel requestedUser = session.users().getUserByUsername(requestedSubject, realm);
+            if (requestedUser == null) {
+                requestedUser = session.users().getUserById(requestedSubject, realm);
+            }
 
-        AuthenticationManager.AuthResult authResult = AuthenticationManager.verifyIdentityToken(session, realm, uriInfo, clientConnection, true, true, false, subjectToken, headers);
-        if (authResult == null) {
-            event.error(Errors.INVALID_TOKEN);
-            throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Invalid token", Response.Status.BAD_REQUEST);
+            if (requestedUser == null) {
+                // We always returned access denied to avoid username fishing
+                logger.debug("Requested subject not found");
+                event.error(Errors.NOT_ALLOWED);
+                throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
+
+            }
+
+            if (token != null) {
+                event.detail(Details.IMPERSONATOR, tokenUser.getUsername());
+                // for this case, the user represented by the token, must have permission to impersonate.
+                AdminAuth auth = new AdminAuth(realm, token, tokenUser, client);
+                if (!AdminPermissions.evaluator(session, realm, auth).users().canImpersonate(requestedUser)) {
+                    logger.debug("Token user not allowed to exchange for requested subject");
+                    event.error(Errors.NOT_ALLOWED);
+                    throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
+                }
+
+            } else {
+                // no token is being exchanged, this is a direct exchange.  Client must be authenticated, not public, and must be allowed
+                // to impersonate
+                if (client.isPublicClient()) {
+                    logger.debug("Public clients cannot exchange tokens");
+                    event.error(Errors.NOT_ALLOWED);
+                    throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
+
+                }
+                if (!AdminPermissions.management(session, realm).users().canClientImpersonate(client, requestedUser)) {
+                    logger.debug("Client not allowed to exchange for requested subject");
+                    event.error(Errors.NOT_ALLOWED);
+                    throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
+                }
+            }
+
+            String sessionId = KeycloakModelUtils.generateId();
+            tokenUser = requestedUser;
+            tokenSession = session.sessions().createUserSession(sessionId, realm, requestedUser, requestedUser.getUsername(), clientConnection.getRemoteAddr(), "impersonate", false, null, null);
         }
 
         String requestedIssuer = formParams.getFirst(OAuth2Constants.REQUESTED_ISSUER);
 
         if (requestedIssuer == null) {
-            return exchangeClientToClient(authResult.getUser(), authResult.getSession());
+            return exchangeClientToClient(tokenUser, tokenSession);
         } else {
-            return exchangeToIdentityProvider(authResult, requestedIssuer);
+            return exchangeToIdentityProvider(tokenUser, tokenSession, requestedIssuer);
         }
     }
 
-    public Response exchangeToIdentityProvider(AuthenticationManager.AuthResult authResult, String requestedIssuer) {
+    public Response exchangeToIdentityProvider(UserModel targetUser, UserSessionModel targetUserSession, String requestedIssuer) {
+        event.detail(Details.REQUESTED_ISSUER, requestedIssuer);
         IdentityProviderModel providerModel = realm.getIdentityProviderByAlias(requestedIssuer);
         if (providerModel == null) {
             event.error(Errors.UNKNOWN_IDENTITY_PROVIDER);
@@ -608,7 +669,7 @@ public class TokenEndpoint {
             event.error(Errors.NOT_ALLOWED);
             throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
         }
-        Response response = ((ExchangeTokenToIdentityProviderToken)provider).exchangeFromToken(uriInfo, client, authResult.getSession(), authResult.getUser(), authResult.getToken(), formParams);
+        Response response = ((ExchangeTokenToIdentityProviderToken)provider).exchangeFromToken(uriInfo, client, targetUserSession, targetUser, formParams);
         return Cors.add(request, Response.fromResponse(response)).auth().allowedOrigins(uriInfo, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
 
     }
@@ -622,27 +683,17 @@ public class TokenEndpoint {
             throw new ErrorResponseException("unsupported_requested_token_type", "Unsupported requested token type", Response.Status.BAD_REQUEST);
 
         }
+        ClientModel targetClient = client;
         String audience = formParams.getFirst(OAuth2Constants.AUDIENCE);
-        if (audience == null) {
-            event.error(Errors.INVALID_REQUEST);
-            throw new ErrorResponseException("invalid_audience", "Audience parameter required", Response.Status.BAD_REQUEST);
-
-        }
-        ClientModel targetClient = null;
         if (audience != null) {
             targetClient = realm.getClientByClientId(audience);
         }
-        if (targetClient == null) {
-            event.error(Errors.INVALID_CLIENT);
-            throw new ErrorResponseException("invalid_client", "Client authentication ended, but client is null", Response.Status.BAD_REQUEST);
-        }
-
-        if (targetClient.isConsentRequired()) {
+       if (targetClient.isConsentRequired()) {
             event.error(Errors.CONSENT_DENIED);
             throw new ErrorResponseException(OAuthErrorException.INVALID_CLIENT, "Client requires user consent", Response.Status.BAD_REQUEST);
         }
 
-        if (!AdminPermissions.management(session, realm).clients().canExchangeTo(client, targetClient)) {
+        if (!targetClient.equals(client) && !AdminPermissions.management(session, realm).clients().canExchangeTo(client, targetClient)) {
             logger.debug("Client does not have exchange rights for target audience");
             event.error(Errors.NOT_ALLOWED);
             throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
@@ -678,6 +729,7 @@ public class TokenEndpoint {
         }
 
         AccessTokenResponse res = responseBuilder.build();
+        event.detail(Details.AUDIENCE, targetClient.getClientId());
 
         event.success();
 
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissionEvaluator.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissionEvaluator.java
index 40b1f8f..f0e2a86 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissionEvaluator.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissionEvaluator.java
@@ -49,6 +49,8 @@ public interface UserPermissionEvaluator {
 
     boolean canImpersonate(UserModel user);
 
+    boolean isImpersonatable(UserModel user);
+
     boolean canImpersonate();
 
     void requireImpersonate(UserModel user);
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissionManagement.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissionManagement.java
index d465378..0ac8431 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissionManagement.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissionManagement.java
@@ -18,7 +18,9 @@ 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.RoleModel;
+import org.keycloak.models.UserModel;
 
 import java.util.Map;
 
@@ -46,4 +48,8 @@ public interface UserPermissionManagement {
     Policy adminImpersonatingPermission();
 
     Policy userImpersonatedPermission();
+
+    boolean canClientImpersonate(ClientModel client, UserModel user);
+
+    boolean isImpersonatable(UserModel user);
 }
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissions.java
index 0078497..55b7fb4 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissions.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissions.java
@@ -18,13 +18,17 @@ 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.common.UserModelIdentity;
 import org.keycloak.authorization.identity.Identity;
 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.AdminRoles;
+import org.keycloak.models.ClientModel;
 import org.keycloak.models.GroupModel;
 import org.keycloak.models.ImpersonationConstants;
 import org.keycloak.models.KeycloakSession;
@@ -32,6 +36,8 @@ import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.services.ForbiddenException;
 
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
@@ -473,15 +479,33 @@ class UserPermissions implements UserPermissionEvaluator, UserPermissionManageme
     }
 
     @Override
+    public boolean canClientImpersonate(ClientModel client, UserModel user) {
+        ClientModelIdentity identity = new ClientModelIdentity(session, client);
+        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(client.getClientId()));
+                return attributes;
+            }
+
+        };
+        return canImpersonate(context) && isImpersonatable(user);
+
+    }
+
+    @Override
     public boolean canImpersonate(UserModel user) {
         if (!canImpersonate()) {
             return false;
         }
 
+        return isImpersonatable(user);
+    }
+
+    @Override
+    public boolean isImpersonatable(UserModel user) {
         Identity userIdentity = new UserModelIdentity(root.realm, user);
-        if (!root.isAdminSameRealm()) {
-            return true;
-        }
 
         ResourceServer server = root.realmResourceServer();
         if (server == null) return true;
@@ -502,17 +526,24 @@ class UserPermissions implements UserPermissionEvaluator, UserPermissionManageme
 
         Scope scope = root.realmScope(USER_IMPERSONATED_SCOPE);
         return root.evaluatePermission(resource, scope, server, userIdentity);
-
     }
 
     @Override
     public boolean canImpersonate() {
         if (root.hasOneAdminRole(ImpersonationConstants.IMPERSONATION_ROLE)) return true;
 
+        Identity identity = root.identity;
+
         if (!root.isAdminSameRealm()) {
             return false;
         }
 
+        EvaluationContext context = new DefaultEvaluationContext(identity, session);
+        return canImpersonate(context);
+    }
+
+    protected boolean canImpersonate(EvaluationContext context) {
+
         ResourceServer server = root.realmResourceServer();
         if (server == null) return false;
 
@@ -531,7 +562,7 @@ class UserPermissions implements UserPermissionEvaluator, UserPermissionManageme
         }
 
         Scope scope = root.realmScope(IMPERSONATE_SCOPE);
-        return root.evaluatePermission(resource, scope, server);
+        return root.evaluatePermission(resource, scope, server, context);
     }
 
     @Override
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 a3312d8..2b104fe 100755
--- a/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java
@@ -103,7 +103,7 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
     }
 
     @Override
-    public Response exchangeFromToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, org.keycloak.representations.AccessToken token, MultivaluedMap<String, String> params) {
+    public Response exchangeFromToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, MultivaluedMap<String, String> params) {
         String requestedType = params.getFirst(OAuth2Constants.REQUESTED_TOKEN_TYPE);
         if (requestedType != null && !requestedType.equals(TWITTER_TOKEN_TYPE)) {
             return exchangeUnsupportedRequiredType();
@@ -111,24 +111,24 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
         if (!getConfig().isStoreToken()) {
             String brokerId = tokenUserSession.getNote(Details.IDENTITY_PROVIDER);
             if (brokerId == null || !brokerId.equals(getConfig().getAlias())) {
-                return exchangeNotLinkedNoStore(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+                return exchangeNotLinkedNoStore(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
             }
-            return exchangeSessionToken(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+            return exchangeSessionToken(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
         } else {
-            return exchangeStoredToken(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+            return exchangeStoredToken(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
         }
     }
 
-    protected Response exchangeStoredToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, org.keycloak.representations.AccessToken token) {
+    protected Response exchangeStoredToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject) {
         FederatedIdentityModel model = session.users().getFederatedIdentity(tokenSubject, getConfig().getAlias(), authorizedClient.getRealm());
         if (model == null || model.getToken() == null) {
-            return exchangeNotLinked(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+            return exchangeNotLinked(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
         }
         String accessToken = model.getToken();
         if (accessToken == null) {
             model.setToken(null);
             session.users().updateFederatedIdentity(authorizedClient.getRealm(), tokenSubject, model);
-            return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+            return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
         }
         AccessTokenResponse tokenResponse = new AccessTokenResponse();
         tokenResponse.setToken(accessToken);
@@ -137,14 +137,14 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
         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));
+        tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession));
         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) {
+    protected Response exchangeSessionToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject) {
         String accessToken = tokenUserSession.getNote(AbstractOAuth2IdentityProvider.FEDERATED_ACCESS_TOKEN);
         if (accessToken == null) {
-            return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject, token);
+            return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
         }
         AccessTokenResponse tokenResponse = new AccessTokenResponse();
         tokenResponse.setToken(accessToken);
@@ -153,7 +153,7 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
         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));
+        tokenResponse.getOtherClaims().put(ACCOUNT_LINK_URL, getLinkingUrl(uriInfo, authorizedClient, tokenUserSession));
         return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
     }
 
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
index 07935bc..7a3a5b3 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
@@ -404,7 +404,7 @@ public class OAuthClient {
     }
 
     public AccessTokenResponse doTokenExchange(String realm, String token, String targetAudience,
-                                                         String clientId, String clientSecret) throws Exception {
+                                               String clientId, String clientSecret) throws Exception {
         CloseableHttpClient client = newCloseableHttpClient();
         try {
             HttpPost post = new HttpPost(getResourceOwnerPasswordCredentialGrantUrl(realm));
@@ -447,6 +447,40 @@ public class OAuthClient {
         }
     }
 
+    public AccessTokenResponse doTokenExchange(String realm, String clientId, String clientSecret, Map<String, String> params) throws Exception {
+        CloseableHttpClient client = newCloseableHttpClient();
+        try {
+            HttpPost post = new HttpPost(getResourceOwnerPasswordCredentialGrantUrl(realm));
+
+            List<NameValuePair> parameters = new LinkedList<NameValuePair>();
+            parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE));
+            for (Map.Entry<String, String> entry : params.entrySet()) {
+                parameters.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
+
+            }
+
+            if (clientSecret != null) {
+                String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
+                post.setHeader("Authorization", authorization);
+            } else {
+                parameters.add(new BasicNameValuePair("client_id", clientId));
+
+            }
+
+           UrlEncodedFormEntity formEntity;
+            try {
+                formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
+            } catch (UnsupportedEncodingException e) {
+                throw new RuntimeException(e);
+            }
+            post.setEntity(formEntity);
+
+            return new AccessTokenResponse(client.execute(post));
+        } finally {
+            closeClient(client);
+        }
+    }
+
 
     public JSONWebKeySet doCertsRequest(String realm) throws Exception {
         CloseableHttpClient client = new DefaultHttpClient();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractLinkAndExchangeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractLinkAndExchangeTest.java
index a382f27..162e7fd 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractLinkAndExchangeTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractLinkAndExchangeTest.java
@@ -45,6 +45,7 @@ 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.representations.idm.authorization.DecisionStrategy;
 import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement;
 import org.keycloak.services.resources.admin.permissions.AdminPermissions;
 import org.keycloak.testsuite.ActionURIUtils;
@@ -209,15 +210,38 @@ public abstract class AbstractLinkAndExchangeTest extends AbstractServletsAdapte
         IdentityProviderModel idp = realm.getIdentityProviderByAlias(PARENT_IDP);
         Assert.assertNotNull(idp);
 
+        ClientModel directExchanger = realm.addClient("direct-exchanger");
+        directExchanger.setClientId("direct-exchanger");
+        directExchanger.setPublicClient(false);
+        directExchanger.setDirectAccessGrantsEnabled(true);
+        directExchanger.setEnabled(true);
+        directExchanger.setSecret("secret");
+        directExchanger.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+        directExchanger.setFullScopeAllowed(false);
+
+
         AdminPermissionManagement management = AdminPermissions.management(session, realm);
         management.idps().setPermissionsEnabled(idp, true);
         ClientPolicyRepresentation clientRep = new ClientPolicyRepresentation();
         clientRep.setName("toIdp");
         clientRep.addClient(client.getId());
+        clientRep.addClient(directExchanger.getId());
         ResourceServer server = management.realmResourceServer();
         Policy clientPolicy = management.authz().getStoreFactory().getPolicyStore().create(clientRep, server);
         management.idps().exchangeToPermission(idp).addAssociatedPolicy(clientPolicy);
 
+
+        // permission for user impersonation for a client
+
+        ClientPolicyRepresentation clientImpersonateRep = new ClientPolicyRepresentation();
+        clientImpersonateRep.setName("clientImpersonators");
+        clientImpersonateRep.addClient(directExchanger.getId());
+        server = management.realmResourceServer();
+        Policy clientImpersonatePolicy = management.authz().getStoreFactory().getPolicyStore().create(clientImpersonateRep, server);
+        management.users().setPermissionsEnabled(true);
+        management.users().adminImpersonatingPermission().addAssociatedPolicy(clientImpersonatePolicy);
+        management.users().adminImpersonatingPermission().setDecisionStrategy(DecisionStrategy.AFFIRMATIVE);
+
     }
     public static void turnOffTokenStore(KeycloakSession session) {
         RealmModel realm = session.realms().getRealmByName(CHILD_IDP);
@@ -323,6 +347,21 @@ public abstract class AbstractLinkAndExchangeTest extends AbstractServletsAdapte
         response.close();
         Assert.assertNotEquals(externalToken, tokenResponse.getToken());
 
+        // test direct exchange
+        response = exchangeUrl.request()
+                .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("direct-exchanger", "secret"))
+                .post(Entity.form(
+                        new Form()
+                                .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
+                                .param(OAuth2Constants.REQUESTED_SUBJECT, "child")
+                                .param(OAuth2Constants.REQUESTED_ISSUER, PARENT_IDP)
+
+                ));
+        Assert.assertEquals(200, response.getStatus());
+        tokenResponse = response.readEntity(AccessTokenResponse.class);
+        response.close();
+        Assert.assertNotEquals(externalToken, tokenResponse.getToken());
+
 
         logoutAll();
 
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientTokenExchangeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientTokenExchangeTest.java
new file mode 100755
index 0000000..735d0db
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ClientTokenExchangeTest.java
@@ -0,0 +1,466 @@
+/*
+ * 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.oauth;
+
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.TokenVerifier;
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ImpersonationConstants;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.AccessTokenResponse;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.DecisionStrategy;
+import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement;
+import org.keycloak.services.resources.admin.permissions.AdminPermissions;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.Assert;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
+import org.keycloak.testsuite.util.OAuthClient;
+import org.keycloak.util.BasicAuthHelper;
+
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Form;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.keycloak.testsuite.auth.page.AuthRealm.TEST;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class ClientTokenExchangeTest extends AbstractKeycloakTest {
+
+    @Rule
+    public AssertEvents events = new AssertEvents(this);
+
+    @Deployment
+    public static WebArchive deploy() {
+        return RunOnServerDeployment.create(ClientTokenExchangeTest.class);
+    }
+
+    @Override
+    public void addTestRealms(List<RealmRepresentation> testRealms) {
+        RealmRepresentation testRealmRep = new RealmRepresentation();
+        testRealmRep.setId(TEST);
+        testRealmRep.setRealm(TEST);
+        testRealmRep.setEnabled(true);
+        testRealms.add(testRealmRep);
+    }
+
+    public static void setupRealm(KeycloakSession session) {
+        RealmModel realm = session.realms().getRealmByName(TEST);
+
+        RoleModel exampleRole = realm.addRole("example");
+
+        AdminPermissionManagement management = AdminPermissions.management(session, realm);
+
+        ClientModel target = realm.addClient("target");
+        target.setDirectAccessGrantsEnabled(true);
+        target.setEnabled(true);
+        target.setSecret("secret");
+        target.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+        target.setFullScopeAllowed(false);
+        target.addScopeMapping(exampleRole);
+
+
+        RoleModel impersonateRole = management.getRealmManagementClient().getRole(ImpersonationConstants.IMPERSONATION_ROLE);
+        Assert.assertNotNull(impersonateRole);
+
+        ClientModel clientExchanger = realm.addClient("client-exchanger");
+        clientExchanger.setClientId("client-exchanger");
+        clientExchanger.setPublicClient(false);
+        clientExchanger.setDirectAccessGrantsEnabled(true);
+        clientExchanger.setEnabled(true);
+        clientExchanger.setSecret("secret");
+        clientExchanger.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+        clientExchanger.setFullScopeAllowed(false);
+        clientExchanger.addScopeMapping(impersonateRole);
+
+
+        ClientModel directExchanger = realm.addClient("direct-exchanger");
+        directExchanger.setClientId("direct-exchanger");
+        directExchanger.setPublicClient(false);
+        directExchanger.setDirectAccessGrantsEnabled(true);
+        directExchanger.setEnabled(true);
+        directExchanger.setSecret("secret");
+        directExchanger.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+        directExchanger.setFullScopeAllowed(false);
+
+        ClientModel illegal = realm.addClient("illegal");
+        illegal.setClientId("illegal");
+        illegal.setPublicClient(false);
+        illegal.setDirectAccessGrantsEnabled(true);
+        illegal.setEnabled(true);
+        illegal.setSecret("secret");
+        illegal.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+        illegal.setFullScopeAllowed(false);
+
+        ClientModel legal = realm.addClient("legal");
+        legal.setClientId("legal");
+        legal.setPublicClient(false);
+        legal.setDirectAccessGrantsEnabled(true);
+        legal.setEnabled(true);
+        legal.setSecret("secret");
+        legal.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+        legal.setFullScopeAllowed(false);
+
+        ClientModel directLegal = realm.addClient("direct-legal");
+        directLegal.setClientId("direct-legal");
+        directLegal.setPublicClient(false);
+        directLegal.setDirectAccessGrantsEnabled(true);
+        directLegal.setEnabled(true);
+        directLegal.setSecret("secret");
+        directLegal.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+        directLegal.setFullScopeAllowed(false);
+
+        ClientModel directPublic = realm.addClient("direct-public");
+        directPublic.setClientId("direct-public");
+        directPublic.setPublicClient(true);
+        directPublic.setDirectAccessGrantsEnabled(true);
+        directPublic.setEnabled(true);
+        directPublic.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+        directPublic.setFullScopeAllowed(false);
+
+        ClientModel directNoSecret = realm.addClient("direct-no-secret");
+        directNoSecret.setClientId("direct-no-secret");
+        directNoSecret.setPublicClient(false);
+        directNoSecret.setDirectAccessGrantsEnabled(true);
+        directNoSecret.setEnabled(true);
+        directNoSecret.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+        directNoSecret.setFullScopeAllowed(false);
+
+
+        // permission for client to client exchange to "target" client
+        management.clients().setPermissionsEnabled(target, true);
+        ClientPolicyRepresentation clientRep = new ClientPolicyRepresentation();
+        clientRep.setName("to");
+        clientRep.addClient(clientExchanger.getId());
+        clientRep.addClient(legal.getId());
+        clientRep.addClient(directLegal.getId());
+        ResourceServer server = management.realmResourceServer();
+        Policy clientPolicy = management.authz().getStoreFactory().getPolicyStore().create(clientRep, server);
+        management.clients().exchangeToPermission(target).addAssociatedPolicy(clientPolicy);
+
+        // permission for user impersonation for a client
+
+        ClientPolicyRepresentation clientImpersonateRep = new ClientPolicyRepresentation();
+        clientImpersonateRep.setName("clientImpersonators");
+        clientImpersonateRep.addClient(directLegal.getId());
+        clientImpersonateRep.addClient(directExchanger.getId());
+        clientImpersonateRep.addClient(directPublic.getId());
+        clientImpersonateRep.addClient(directNoSecret.getId());
+        server = management.realmResourceServer();
+        Policy clientImpersonatePolicy = management.authz().getStoreFactory().getPolicyStore().create(clientImpersonateRep, server);
+        management.users().setPermissionsEnabled(true);
+        management.users().adminImpersonatingPermission().addAssociatedPolicy(clientImpersonatePolicy);
+        management.users().adminImpersonatingPermission().setDecisionStrategy(DecisionStrategy.AFFIRMATIVE);
+
+        UserModel user = session.users().addUser(realm, "user");
+        user.setEnabled(true);
+        session.userCredentialManager().updateCredential(realm, user, UserCredentialModel.password("password"));
+        user.grantRole(exampleRole);
+        user.grantRole(impersonateRole);
+
+        UserModel bad = session.users().addUser(realm, "bad-impersonator");
+        bad.setEnabled(true);
+        session.userCredentialManager().updateCredential(realm, bad, UserCredentialModel.password("password"));
+
+        UserModel impersonatedUser = session.users().addUser(realm, "impersonated-user");
+        impersonatedUser.setEnabled(true);
+        session.userCredentialManager().updateCredential(realm, impersonatedUser, UserCredentialModel.password("password"));
+        impersonatedUser.grantRole(exampleRole);
+
+    }
+
+    @Override
+    protected boolean isImportAfterEachMethod() {
+        return true;
+    }
+
+
+    @Test
+    public void testExchange() throws Exception {
+        testingClient.server().run(ClientTokenExchangeTest::setupRealm);
+
+        oauth.realm(TEST);
+        oauth.clientId("client-exchanger");
+
+        OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "user", "password");
+        String accessToken = response.getAccessToken();
+        TokenVerifier<AccessToken> accessTokenVerifier = TokenVerifier.create(accessToken, AccessToken.class);
+        AccessToken token = accessTokenVerifier.parse().getToken();
+        Assert.assertEquals(token.getPreferredUsername(), "user");
+        Assert.assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example"));
+
+        {
+            response = oauth.doTokenExchange(TEST, accessToken, "target", "client-exchanger", "secret");
+
+            String exchangedTokenString = response.getAccessToken();
+            TokenVerifier<AccessToken> verifier = TokenVerifier.create(exchangedTokenString, AccessToken.class);
+            AccessToken exchangedToken = verifier.parse().getToken();
+            Assert.assertEquals("client-exchanger", exchangedToken.getIssuedFor());
+            Assert.assertEquals("target", exchangedToken.getAudience()[0]);
+            Assert.assertEquals(exchangedToken.getPreferredUsername(), "user");
+            Assert.assertTrue(exchangedToken.getRealmAccess().isUserInRole("example"));
+        }
+
+        {
+            response = oauth.doTokenExchange(TEST, accessToken, "target", "legal", "secret");
+
+            String exchangedTokenString = response.getAccessToken();
+            TokenVerifier<AccessToken> verifier = TokenVerifier.create(exchangedTokenString, AccessToken.class);
+            AccessToken exchangedToken = verifier.parse().getToken();
+            Assert.assertEquals("legal", exchangedToken.getIssuedFor());
+            Assert.assertEquals("target", exchangedToken.getAudience()[0]);
+            Assert.assertEquals(exchangedToken.getPreferredUsername(), "user");
+            Assert.assertTrue(exchangedToken.getRealmAccess().isUserInRole("example"));
+        }
+        {
+            response = oauth.doTokenExchange(TEST, accessToken, "target", "illegal", "secret");
+            Assert.assertEquals(403, response.getStatusCode());
+        }
+    }
+    @Test
+    public void testImpersonation() throws Exception {
+        testingClient.server().run(ClientTokenExchangeTest::setupRealm);
+
+        oauth.realm(TEST);
+        oauth.clientId("client-exchanger");
+
+        Client httpClient = ClientBuilder.newClient();
+
+        WebTarget exchangeUrl = httpClient.target(OAuthClient.AUTH_SERVER_ROOT)
+                .path("/realms")
+                .path(TEST)
+                .path("protocol/openid-connect/token");
+        System.out.println("Exchange url: " + exchangeUrl.getUri().toString());
+
+        OAuthClient.AccessTokenResponse tokenResponse = oauth.doGrantAccessTokenRequest("secret", "user", "password");
+        String accessToken = tokenResponse.getAccessToken();
+        TokenVerifier<AccessToken> accessTokenVerifier = TokenVerifier.create(accessToken, AccessToken.class);
+        AccessToken token = accessTokenVerifier.parse().getToken();
+        Assert.assertEquals(token.getPreferredUsername(), "user");
+        Assert.assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example"));
+
+        // client-exchanger can impersonate from token "user" to user "impersonated-user"
+        {
+            Response response = exchangeUrl.request()
+                    .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("client-exchanger", "secret"))
+                    .post(Entity.form(
+                            new Form()
+                                    .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
+                                    .param(OAuth2Constants.SUBJECT_TOKEN, accessToken)
+                                    .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE)
+                                    .param(OAuth2Constants.REQUESTED_SUBJECT, "impersonated-user")
+
+                    ));
+            org.junit.Assert.assertEquals(200, response.getStatus());
+            AccessTokenResponse accessTokenResponse = response.readEntity(AccessTokenResponse.class);
+            response.close();
+
+            String exchangedTokenString = accessTokenResponse.getToken();
+            TokenVerifier<AccessToken> verifier = TokenVerifier.create(exchangedTokenString, AccessToken.class);
+            AccessToken exchangedToken = verifier.parse().getToken();
+            Assert.assertEquals("client-exchanger", exchangedToken.getIssuedFor());
+            Assert.assertEquals("client-exchanger", exchangedToken.getAudience()[0]);
+            Assert.assertEquals(exchangedToken.getPreferredUsername(), "impersonated-user");
+            Assert.assertNull(exchangedToken.getRealmAccess());
+        }
+
+        // client-exchanger can impersonate from token "user" to user "impersonated-user" and to "target" client
+        {
+            Response response = exchangeUrl.request()
+                    .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("client-exchanger", "secret"))
+                    .post(Entity.form(
+                            new Form()
+                                    .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
+                                    .param(OAuth2Constants.SUBJECT_TOKEN, accessToken)
+                                    .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE)
+                                    .param(OAuth2Constants.REQUESTED_SUBJECT, "impersonated-user")
+                                    .param(OAuth2Constants.AUDIENCE, "target")
+
+                    ));
+            org.junit.Assert.assertEquals(200, response.getStatus());
+            AccessTokenResponse accessTokenResponse = response.readEntity(AccessTokenResponse.class);
+            response.close();
+
+            String exchangedTokenString = accessTokenResponse.getToken();
+            TokenVerifier<AccessToken> verifier = TokenVerifier.create(exchangedTokenString, AccessToken.class);
+            AccessToken exchangedToken = verifier.parse().getToken();
+            Assert.assertEquals("client-exchanger", exchangedToken.getIssuedFor());
+            Assert.assertEquals("target", exchangedToken.getAudience()[0]);
+            Assert.assertEquals(exchangedToken.getPreferredUsername(), "impersonated-user");
+            Assert.assertTrue(exchangedToken.getRealmAccess().isUserInRole("example"));
+        }
+
+
+    }
+
+    @Test
+    public void testBadImpersonator() throws Exception {
+        testingClient.server().run(ClientTokenExchangeTest::setupRealm);
+
+        oauth.realm(TEST);
+        oauth.clientId("client-exchanger");
+
+        Client httpClient = ClientBuilder.newClient();
+
+        WebTarget exchangeUrl = httpClient.target(OAuthClient.AUTH_SERVER_ROOT)
+                .path("/realms")
+                .path(TEST)
+                .path("protocol/openid-connect/token");
+        System.out.println("Exchange url: " + exchangeUrl.getUri().toString());
+
+        OAuthClient.AccessTokenResponse tokenResponse = oauth.doGrantAccessTokenRequest("secret", "bad-impersonator", "password");
+        String accessToken = tokenResponse.getAccessToken();
+        TokenVerifier<AccessToken> accessTokenVerifier = TokenVerifier.create(accessToken, AccessToken.class);
+        AccessToken token = accessTokenVerifier.parse().getToken();
+        Assert.assertEquals(token.getPreferredUsername(), "bad-impersonator");
+        Assert.assertTrue(token.getRealmAccess() == null || !token.getRealmAccess().isUserInRole("example"));
+
+        // test that user does not have impersonator permission
+        {
+            Response response = exchangeUrl.request()
+                    .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("client-exchanger", "secret"))
+                    .post(Entity.form(
+                            new Form()
+                                    .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
+                                    .param(OAuth2Constants.SUBJECT_TOKEN, accessToken)
+                                    .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE)
+                                    .param(OAuth2Constants.REQUESTED_SUBJECT, "impersonated-user")
+
+                    ));
+            org.junit.Assert.assertEquals(403, response.getStatus());
+            response.close();
+        }
+
+
+    }
+
+    @Test
+    public void testDirectImpersonation() throws Exception {
+        testingClient.server().run(ClientTokenExchangeTest::setupRealm);
+        Client httpClient = ClientBuilder.newClient();
+
+        WebTarget exchangeUrl = httpClient.target(OAuthClient.AUTH_SERVER_ROOT)
+                .path("/realms")
+                .path(TEST)
+                .path("protocol/openid-connect/token");
+        System.out.println("Exchange url: " + exchangeUrl.getUri().toString());
+
+        // direct-exchanger can impersonate from token "user" to user "impersonated-user"
+        {
+            Response response = exchangeUrl.request()
+                    .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("direct-exchanger", "secret"))
+                    .post(Entity.form(
+                            new Form()
+                                    .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
+                                    .param(OAuth2Constants.REQUESTED_SUBJECT, "impersonated-user")
+
+                    ));
+            org.junit.Assert.assertEquals(200, response.getStatus());
+            AccessTokenResponse accessTokenResponse = response.readEntity(AccessTokenResponse.class);
+            response.close();
+
+            String exchangedTokenString = accessTokenResponse.getToken();
+            TokenVerifier<AccessToken> verifier = TokenVerifier.create(exchangedTokenString, AccessToken.class);
+            AccessToken exchangedToken = verifier.parse().getToken();
+            Assert.assertEquals("direct-exchanger", exchangedToken.getIssuedFor());
+            Assert.assertEquals("direct-exchanger", exchangedToken.getAudience()[0]);
+            Assert.assertEquals(exchangedToken.getPreferredUsername(), "impersonated-user");
+            Assert.assertNull(exchangedToken.getRealmAccess());
+        }
+
+        // direct-legal can impersonate from token "user" to user "impersonated-user" and to "target" client
+        {
+            Response response = exchangeUrl.request()
+                    .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("direct-legal", "secret"))
+                    .post(Entity.form(
+                            new Form()
+                                    .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
+                                    .param(OAuth2Constants.REQUESTED_SUBJECT, "impersonated-user")
+                                    .param(OAuth2Constants.AUDIENCE, "target")
+
+                    ));
+            org.junit.Assert.assertEquals(200, response.getStatus());
+            AccessTokenResponse accessTokenResponse = response.readEntity(AccessTokenResponse.class);
+            response.close();
+
+            String exchangedTokenString = accessTokenResponse.getToken();
+            TokenVerifier<AccessToken> verifier = TokenVerifier.create(exchangedTokenString, AccessToken.class);
+            AccessToken exchangedToken = verifier.parse().getToken();
+            Assert.assertEquals("direct-legal", exchangedToken.getIssuedFor());
+            Assert.assertEquals("target", exchangedToken.getAudience()[0]);
+            Assert.assertEquals(exchangedToken.getPreferredUsername(), "impersonated-user");
+            Assert.assertTrue(exchangedToken.getRealmAccess().isUserInRole("example"));
+        }
+
+        // direct-public fails impersonation
+        {
+            Response response = exchangeUrl.request()
+                    .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("direct-public", "secret"))
+                    .post(Entity.form(
+                            new Form()
+                                    .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
+                                    .param(OAuth2Constants.REQUESTED_SUBJECT, "impersonated-user")
+                                    .param(OAuth2Constants.AUDIENCE, "target")
+
+                    ));
+            org.junit.Assert.assertEquals(403, response.getStatus());
+            response.close();
+        }
+
+        // direct-no-secret fails impersonation
+        {
+            Response response = exchangeUrl.request()
+                    .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("direct-no-secret", "secret"))
+                    .post(Entity.form(
+                            new Form()
+                                    .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
+                                    .param(OAuth2Constants.REQUESTED_SUBJECT, "impersonated-user")
+                                    .param(OAuth2Constants.AUDIENCE, "target")
+
+                    ));
+            org.junit.Assert.assertTrue(response.getStatus() >= 400);
+            response.close();
+        }
+
+
+    }
+
+
+}