keycloak-uncached

Merge pull request #4562 from patriot1burke/master KEYCLOAK-5683,

10/13/2017 7:43:48 PM

Changes

Details

diff --git a/core/src/main/java/org/keycloak/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java
index 098fdcd..4098439 100644
--- a/core/src/main/java/org/keycloak/OAuth2Constants.java
+++ b/core/src/main/java/org/keycloak/OAuth2Constants.java
@@ -105,7 +105,6 @@ public interface OAuth2Constants {
     String ACCESS_TOKEN_TYPE="urn:ietf:params:oauth:token-type:access_token";
     String REFRESH_TOKEN_TYPE="urn:ietf:params:oauth:token-type:refresh_token";
     String JWT_TOKEN_TYPE="urn:ietf:params:oauth:token-type:jwt";
-    String JWT_ACCESS_TOKEN_TYPE="urn:ietf:params:oauth:token-type:jwt:access_token";
     String ID_TOKEN_TYPE="urn:ietf:params:oauth:token-type:id_token";
 
 
diff --git a/server-spi-private/src/main/java/org/keycloak/broker/provider/ExchangeExternalToken.java b/server-spi-private/src/main/java/org/keycloak/broker/provider/ExchangeExternalToken.java
index a448d3d..2b3ef3b 100644
--- a/server-spi-private/src/main/java/org/keycloak/broker/provider/ExchangeExternalToken.java
+++ b/server-spi-private/src/main/java/org/keycloak/broker/provider/ExchangeExternalToken.java
@@ -29,6 +29,7 @@ import javax.ws.rs.core.MultivaluedMap;
  * @version $Revision: 1 $
  */
 public interface ExchangeExternalToken {
+    boolean isIssuer(String issuer, MultivaluedMap<String, String> params);
     BrokeredIdentityContext exchangeExternal(EventBuilder event, MultivaluedMap<String, String> params);
 
     void exchangeExternalComplete(UserSessionModel userSession, BrokeredIdentityContext context, MultivaluedMap<String, String> params);
diff --git a/server-spi-private/src/main/java/org/keycloak/broker/provider/IdentityProvider.java b/server-spi-private/src/main/java/org/keycloak/broker/provider/IdentityProvider.java
index c4f1c5c..6564118 100755
--- a/server-spi-private/src/main/java/org/keycloak/broker/provider/IdentityProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/broker/provider/IdentityProvider.java
@@ -34,6 +34,9 @@ import javax.ws.rs.core.UriInfo;
  */
 public interface IdentityProvider<C extends IdentityProviderModel> extends Provider {
 
+    String EXTERNAL_IDENTITY_PROVIDER = "EXTERNAL_IDENTITY_PROVIDER";
+    String FEDERATED_ACCESS_TOKEN = "FEDERATED_ACCESS_TOKEN";
+
     interface AuthenticationCallback {
         /**
          * This method should be called by provider after the JAXRS callback endpoint has finished authentication
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 414cbc4..98d4e34 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
@@ -20,11 +20,14 @@ import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import org.jboss.logging.Logger;
 import org.keycloak.OAuth2Constants;
+import org.keycloak.OAuthErrorException;
 import org.keycloak.broker.provider.AbstractIdentityProvider;
 import org.keycloak.broker.provider.AuthenticationRequest;
 import org.keycloak.broker.provider.BrokeredIdentityContext;
+import org.keycloak.broker.provider.ExchangeExternalToken;
 import org.keycloak.broker.provider.IdentityBrokerException;
 import org.keycloak.broker.provider.ExchangeTokenToIdentityProviderToken;
+import org.keycloak.broker.provider.IdentityProvider;
 import org.keycloak.broker.provider.util.SimpleHttp;
 import org.keycloak.common.ClientConnection;
 import org.keycloak.events.Details;
@@ -40,6 +43,7 @@ import org.keycloak.models.UserSessionModel;
 import org.keycloak.representations.AccessTokenResponse;
 import org.keycloak.protocol.oidc.OIDCLoginProtocol;
 import org.keycloak.services.ErrorPage;
+import org.keycloak.services.ErrorResponseException;
 import org.keycloak.services.messages.Messages;
 import org.keycloak.sessions.AuthenticationSessionModel;
 
@@ -61,12 +65,12 @@ import java.util.regex.Pattern;
 /**
  * @author Pedro Igor
  */
-public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityProviderConfig> extends AbstractIdentityProvider<C> implements ExchangeTokenToIdentityProviderToken {
+public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityProviderConfig> extends AbstractIdentityProvider<C> implements ExchangeTokenToIdentityProviderToken, ExchangeExternalToken {
     protected static final Logger logger = Logger.getLogger(AbstractOAuth2IdentityProvider.class);
 
     public static final String OAUTH2_GRANT_TYPE_REFRESH_TOKEN = "refresh_token";
     public static final String OAUTH2_GRANT_TYPE_AUTHORIZATION_CODE = "authorization_code";
-    public static final String FEDERATED_ACCESS_TOKEN = "FEDERATED_ACCESS_TOKEN";
+
     public static final String FEDERATED_REFRESH_TOKEN = "FEDERATED_REFRESH_TOKEN";
     public static final String FEDERATED_TOKEN_EXPIRATION = "FEDERATED_TOKEN_EXPIRATION";
     public static final String ACCESS_DENIED = "access_denied";
@@ -163,6 +167,7 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
         if (!getConfig().isStoreToken()) {
             // if token isn't stored, we need to see if this session has been linked
             String brokerId = tokenUserSession.getNote(Details.IDENTITY_PROVIDER);
+            brokerId = brokerId == null ? tokenUserSession.getNote(IdentityProvider.EXTERNAL_IDENTITY_PROVIDER) : brokerId;
             if (brokerId == null || !brokerId.equals(getConfig().getAlias())) {
                 event.detail(Details.REASON, "requested_issuer has not linked");
                 event.error(Errors.INVALID_REQUEST);
@@ -411,4 +416,113 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
                     .param(OAUTH2_PARAMETER_GRANT_TYPE, OAUTH2_GRANT_TYPE_AUTHORIZATION_CODE);
         }
     }
+
+    protected String getProfileEndpointForValidation(EventBuilder event) {
+        event.detail(Details.REASON, "exchange unsupported");
+        event.error(Errors.INVALID_TOKEN);
+        throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token", Response.Status.BAD_REQUEST);
+    }
+
+    protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, JsonNode node) {
+        return null;
+    }
+
+    protected BrokeredIdentityContext validateExternalTokenThroughUserInfo(EventBuilder event, String subjectToken, String subjectTokenType) {
+        event.detail("validation_method", "user info");
+        SimpleHttp.Response response = null;
+        int status = 0;
+        try {
+            String userInfoUrl = getProfileEndpointForValidation(event);
+            response = buildUserInfoRequest(subjectToken, userInfoUrl).asResponse();
+            status = response.getStatus();
+        } catch (IOException e) {
+            logger.debug("Failed to invoke user info for external exchange", e);
+        }
+        if (status != 200) {
+            logger.debug("Failed to invoke user info status: " + status);
+            event.detail(Details.REASON, "user info call failure");
+            event.error(Errors.INVALID_TOKEN);
+            throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token", Response.Status.BAD_REQUEST);
+        }
+        JsonNode profile = null;
+        try {
+            profile = response.asJson();
+        } catch (IOException e) {
+            event.detail(Details.REASON, "user info call failure");
+            event.error(Errors.INVALID_TOKEN);
+            throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token", Response.Status.BAD_REQUEST);
+        }
+        BrokeredIdentityContext context = extractIdentityFromProfile(event, profile);
+        if (context.getId() == null) {
+            event.detail(Details.REASON, "user info call failure");
+            event.error(Errors.INVALID_TOKEN);
+            throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token", Response.Status.BAD_REQUEST);
+        }
+        return context;
+    }
+
+    protected SimpleHttp buildUserInfoRequest(String subjectToken, String userInfoUrl) {
+        return SimpleHttp.doGet(userInfoUrl, session)
+                  .header("Authorization", "Bearer " + subjectToken);
+    }
+
+
+    protected boolean supportsExternalExchange() {
+        return false;
+    }
+
+    @Override
+    public boolean isIssuer(String issuer, MultivaluedMap<String, String> params) {
+        if (!supportsExternalExchange()) return false;
+        String requestedIssuer = params.getFirst(OAuth2Constants.SUBJECT_ISSUER);
+        if (requestedIssuer == null) requestedIssuer = issuer;
+        return requestedIssuer.equals(getConfig().getAlias());
+    }
+
+
+    final public BrokeredIdentityContext exchangeExternal(EventBuilder event, MultivaluedMap<String, String> params) {
+        if (!supportsExternalExchange()) return null;
+        BrokeredIdentityContext context = exchangeExternalImpl(event, params);
+        if (context != null) {
+            context.setIdp(this);
+            context.setIdpConfig(getConfig());
+        }
+        return context;
+    }
+
+    protected BrokeredIdentityContext exchangeExternalImpl(EventBuilder event, MultivaluedMap<String, String> params) {
+        return exchangeExternalUserInfoValidationOnly(event, params);
+
+    }
+
+    protected BrokeredIdentityContext exchangeExternalUserInfoValidationOnly(EventBuilder event, MultivaluedMap<String, String> params) {
+        String subjectToken = params.getFirst(OAuth2Constants.SUBJECT_TOKEN);
+        if (subjectToken == null) {
+            event.detail(Details.REASON, OAuth2Constants.SUBJECT_TOKEN + " param unset");
+            event.error(Errors.INVALID_TOKEN);
+            throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "token not set", Response.Status.BAD_REQUEST);
+        }
+        String subjectTokenType = params.getFirst(OAuth2Constants.SUBJECT_TOKEN_TYPE);
+        if (subjectTokenType == null) {
+            subjectTokenType = OAuth2Constants.ACCESS_TOKEN_TYPE;
+        }
+        if (!OAuth2Constants.ACCESS_TOKEN_TYPE.equals(subjectTokenType)) {
+            event.detail(Details.REASON, OAuth2Constants.SUBJECT_TOKEN_TYPE + " invalid");
+            event.error(Errors.INVALID_TOKEN_TYPE);
+            throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token type", Response.Status.BAD_REQUEST);
+        }
+        return validateExternalTokenThroughUserInfo(event, subjectToken, subjectTokenType);
+    }
+
+    @Override
+    public void exchangeExternalComplete(UserSessionModel userSession, BrokeredIdentityContext context, MultivaluedMap<String, String> params) {
+        if (context.getContextData().containsKey(OIDCIdentityProvider.VALIDATED_ID_TOKEN))
+            userSession.setNote(FEDERATED_ACCESS_TOKEN, params.getFirst(OAuth2Constants.SUBJECT_TOKEN));
+        if (context.getContextData().containsKey(OIDCIdentityProvider.VALIDATED_ID_TOKEN))
+            userSession.setNote(OIDCIdentityProvider.FEDERATED_ID_TOKEN, params.getFirst(OAuth2Constants.SUBJECT_TOKEN));
+        userSession.setNote(OIDCIdentityProvider.EXCHANGE_PROVIDER, getConfig().getAlias());
+
+    }
+
+
 }
diff --git a/services/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProvider.java b/services/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProvider.java
index 1f2871b..4e3d160 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProvider.java
@@ -17,9 +17,13 @@
 
 package org.keycloak.broker.oidc;
 
+import org.keycloak.OAuth2Constants;
+import org.keycloak.OAuthErrorException;
 import org.keycloak.broker.provider.BrokeredIdentityContext;
 import org.keycloak.broker.provider.util.SimpleHttp;
 import org.keycloak.constants.AdapterConstants;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
 import org.keycloak.events.EventBuilder;
 import org.keycloak.jose.jws.JWSInput;
 import org.keycloak.jose.jws.JWSInputException;
@@ -30,11 +34,13 @@ import org.keycloak.representations.AccessTokenResponse;
 import org.keycloak.representations.JsonWebToken;
 import org.keycloak.representations.adapters.action.AdminAction;
 import org.keycloak.representations.adapters.action.LogoutAction;
+import org.keycloak.services.ErrorResponseException;
 import org.keycloak.services.managers.AuthenticationManager;
 import org.keycloak.util.JsonSerialization;
 
 import javax.ws.rs.POST;
 import javax.ws.rs.Path;
+import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.core.Response;
 import java.io.IOException;
 import java.security.PublicKey;
@@ -134,5 +140,21 @@ public class KeycloakOIDCIdentityProvider extends OIDCIdentityProvider {
 
     }
 
+    @Override
+    protected BrokeredIdentityContext exchangeExternalImpl(EventBuilder event, MultivaluedMap<String, String> params) {
+        String subjectToken = params.getFirst(OAuth2Constants.SUBJECT_TOKEN);
+        if (subjectToken == null) {
+            event.detail(Details.REASON, OAuth2Constants.SUBJECT_TOKEN + " param unset");
+            event.error(Errors.INVALID_TOKEN);
+            throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "token not set", Response.Status.BAD_REQUEST);
+        }
+        String subjectTokenType = params.getFirst(OAuth2Constants.SUBJECT_TOKEN_TYPE);
+        if (subjectTokenType == null) {
+            subjectTokenType = OAuth2Constants.ACCESS_TOKEN_TYPE;
+        }
+        return validateJwt(event, subjectToken, subjectTokenType);
+    }
+
+
 
 }
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 6f878b0..014d835 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
@@ -195,7 +195,8 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
     }
 
     private String getIDTokenForLogout(KeycloakSession session, UserSessionModel userSession) {
-        long exp = Long.parseLong(userSession.getNote(FEDERATED_TOKEN_EXPIRATION));
+        String tokenExpirationString = userSession.getNote(FEDERATED_TOKEN_EXPIRATION);
+        long exp = tokenExpirationString == null ? 0 : Long.parseLong(tokenExpirationString);
         int currentTime = Time.currentTime();
         if (exp > 0 && currentTime > exp) {
             String response = refreshTokenForLogout(session, userSession);
@@ -379,11 +380,12 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
         }
     }
 
+
     protected BrokeredIdentityContext extractIdentity(AccessTokenResponse tokenResponse, String accessToken, JsonWebToken idToken) throws IOException {
         String id = idToken.getSubject();
         BrokeredIdentityContext identity = new BrokeredIdentityContext(id);
         String name = (String) idToken.getOtherClaims().get(IDToken.NAME);
-        String preferredUsername = (String) idToken.getOtherClaims().get(getUsernameClaimName());
+        String preferredUsername = (String) idToken.getOtherClaims().get(getusernameClaimNameForIdToken());
         String email = (String) idToken.getOtherClaims().get(IDToken.EMAIL);
 
         if (!getConfig().isDisableUserInfoService()) {
@@ -391,12 +393,24 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
             if (userInfoUrl != null && !userInfoUrl.isEmpty() && (id == null || name == null || preferredUsername == null || email == null)) {
 
                 if (accessToken != null) {
-                    JsonNode userInfo = SimpleHttp.doGet(userInfoUrl, session)
-                            .header("Authorization", "Bearer " + accessToken).asJson();
+                    SimpleHttp.Response response = SimpleHttp.doGet(userInfoUrl, session)
+                            .header("Authorization", "Bearer " + accessToken).asResponse();
+                    if (response.getStatus() != 200) {
+                        String msg = "failed to invoke user info url";
+                        try {
+                            String tmp = response.asString();
+                            if (tmp != null) msg = tmp;
+
+                        } catch (IOException e) {
+
+                        }
+                        throw new IdentityBrokerException("Failed to invoke on user info url: " + msg);
+                    }
+                    JsonNode userInfo = response.asJson();
 
                     id = getJsonProperty(userInfo, "sub");
                     name = getJsonProperty(userInfo, "name");
-                    preferredUsername = getJsonProperty(userInfo, "preferred_username");
+                    preferredUsername = getUsernameFromUserInfo(userInfo);
                     email = getJsonProperty(userInfo, "email");
                     AbstractJsonUserAttributeMapper.storeUserProfileForMapper(identity, userInfo, getConfig().getAlias());
                 }
@@ -427,7 +441,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
         return identity;
     }
 
-    protected String getUsernameClaimName() {
+    protected String getusernameClaimNameForIdToken() {
         return IDToken.PREFERRED_USERNAME;
     }
 
@@ -518,9 +532,11 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
         return "openid";
     }
 
-    protected boolean isIssuer(MultivaluedMap<String, String> params) {
+    @Override
+    public boolean isIssuer(String issuer, MultivaluedMap<String, String> params) {
+        if (!supportsExternalExchange()) return false;
         String requestedIssuer = params.getFirst(OAuth2Constants.SUBJECT_ISSUER);
-        if (requestedIssuer == null) return true;
+        if (requestedIssuer == null) requestedIssuer = issuer;
         if (requestedIssuer.equals(getConfig().getAlias())) return true;
 
         String[] issuers = getConfig().getIssuer().split(",");
@@ -534,38 +550,65 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
 
     }
 
+    protected boolean supportsExternalExchange() {
+        return true;
+    }
+
     @Override
-    public BrokeredIdentityContext exchangeExternal(EventBuilder event, MultivaluedMap<String, String> params) {
-        if (!isIssuer(params)) {
-            return null;
+    protected String getProfileEndpointForValidation(EventBuilder event) {
+        String userInfoUrl = getUserInfoUrl();
+        if (getConfig().isDisableUserInfoService() || userInfoUrl == null || userInfoUrl.isEmpty()) {
+            event.detail(Details.REASON, "user info service disabled");
+            event.error(Errors.INVALID_TOKEN);
+            throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token", Response.Status.BAD_REQUEST);
+
         }
-        String subjectToken = params.getFirst(OAuth2Constants.SUBJECT_TOKEN);
-        if (subjectToken == null) {
-            event.detail(Details.REASON, OAuth2Constants.SUBJECT_TOKEN + " param unset");
+        return userInfoUrl;
+    }
+
+    @Override
+    protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, JsonNode userInfo) {
+        String id = getJsonProperty(userInfo, "sub");
+        if (id == null) {
+            event.detail(Details.REASON, "sub claim is null from user info json");
             event.error(Errors.INVALID_TOKEN);
-            throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "token not set", Response.Status.BAD_REQUEST);
+            throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token", Response.Status.BAD_REQUEST);
         }
-        String subjectTokenType = params.getFirst(OAuth2Constants.SUBJECT_TOKEN_TYPE);
-        if (subjectTokenType == null) {
-            event.detail(Details.REASON, OAuth2Constants.SUBJECT_TOKEN_TYPE + " param unset");
-            event.error(Errors.INVALID_TOKEN_TYPE);
-            throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "token type unset", Response.Status.BAD_REQUEST);
+        BrokeredIdentityContext identity = new BrokeredIdentityContext(id);
+
+        String name = getJsonProperty(userInfo, "name");
+        String preferredUsername = getUsernameFromUserInfo(userInfo);
+        String email = getJsonProperty(userInfo, "email");
+        AbstractJsonUserAttributeMapper.storeUserProfileForMapper(identity, userInfo, getConfig().getAlias());
+
+        identity.setId(id);
+        identity.setName(name);
+        identity.setEmail(email);
+
+        identity.setBrokerUserId(getConfig().getAlias() + "." + id);
+
+        if (preferredUsername == null) {
+            preferredUsername = email;
         }
-        boolean jwtAccessTokenType = subjectTokenType.equals(OAuth2Constants.JWT_ACCESS_TOKEN_TYPE);
-        boolean idTokenType = subjectTokenType.equals(OAuth2Constants.ID_TOKEN_TYPE);
-        if (!jwtAccessTokenType && !idTokenType) {
-            event.error(Errors.INVALID_TOKEN_TYPE);
-            throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token type", Response.Status.BAD_REQUEST);
+
+        if (preferredUsername == null) {
+            preferredUsername = id;
         }
 
+        identity.setUsername(preferredUsername);
+        return identity;
+    }
 
-        if (getConfig().isValidateSignature() == false) {
-            event.detail(Details.REASON, "validate signature unset");
-            event.error(Errors.INVALID_CONFIG);
-            throw new ErrorResponseException(Errors.INVALID_CONFIG, "Invalid server config", Response.Status.BAD_REQUEST);
+    protected String getUsernameFromUserInfo(JsonNode userInfo) {
+        return getJsonProperty(userInfo, "preferred_username");
+    }
+
+    final protected BrokeredIdentityContext validateJwt(EventBuilder event, String subjectToken, String subjectTokenType) {
+        if (!getConfig().isValidateSignature()) {
+            return validateExternalTokenThroughUserInfo(event, subjectToken, subjectTokenType);
         }
+        event.detail("validation_method", "signature");
         if (getConfig().isUseJwksUrl()) {
-            logger.debug("using jwks url to validate token exchange");
             if (getConfig().getJwksUrl() == null) {
                 event.detail(Details.REASON, "jwks url unset");
                 event.error(Errors.INVALID_CONFIG);
@@ -589,6 +632,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
 
         try {
 
+            boolean idTokenType = OAuth2Constants.ID_TOKEN_TYPE.equals(subjectTokenType);
             BrokeredIdentityContext context = extractIdentity(null, idTokenType ? null : subjectToken, parsedToken);
             if (context == null) {
                 event.detail(Details.REASON, "Failed to extract identity from token");
@@ -596,10 +640,9 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
                 throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token", Response.Status.BAD_REQUEST);
 
             }
-            if (!idTokenType) {
+            if (idTokenType) {
                 context.getContextData().put(VALIDATED_ID_TOKEN, subjectToken);
-            }
-            if (jwtAccessTokenType) {
+            } else {
                 context.getContextData().put(KeycloakOIDCIdentityProvider.VALIDATED_ACCESS_TOKEN, parsedToken);
             }
             context.getContextData().put(EXCHANGE_PROVIDER, getConfig().getAlias());
@@ -610,15 +653,31 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
             logger.debug("Unable to extract identity from identity token", e);
             throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token", Response.Status.BAD_REQUEST);
         }
+
+
     }
 
     @Override
-    public void exchangeExternalComplete(UserSessionModel userSession, BrokeredIdentityContext context, MultivaluedMap<String, String> params) {
-        if (context.getContextData().containsKey(VALIDATED_ID_TOKEN))
-            userSession.setNote(FEDERATED_ACCESS_TOKEN, params.getFirst(OAuth2Constants.SUBJECT_TOKEN));
-        if (context.getContextData().containsKey(VALIDATED_ID_TOKEN))
-            userSession.setNote(FEDERATED_ID_TOKEN, params.getFirst(OAuth2Constants.SUBJECT_TOKEN));
-        userSession.setNote(EXCHANGE_PROVIDER, getConfig().getAlias());
-
+    protected BrokeredIdentityContext exchangeExternalImpl(EventBuilder event, MultivaluedMap<String, String> params) {
+        if (!supportsExternalExchange()) return null;
+        String subjectToken = params.getFirst(OAuth2Constants.SUBJECT_TOKEN);
+        if (subjectToken == null) {
+            event.detail(Details.REASON, OAuth2Constants.SUBJECT_TOKEN + " param unset");
+            event.error(Errors.INVALID_TOKEN);
+            throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "token not set", Response.Status.BAD_REQUEST);
+        }
+        String subjectTokenType = params.getFirst(OAuth2Constants.SUBJECT_TOKEN_TYPE);
+        if (subjectTokenType == null) {
+            subjectTokenType = OAuth2Constants.ACCESS_TOKEN_TYPE;
+        }
+        if (OAuth2Constants.JWT_TOKEN_TYPE.equals(subjectTokenType) || OAuth2Constants.ID_TOKEN_TYPE.equals(subjectTokenType)) {
+            return validateJwt(event, subjectToken, subjectTokenType);
+        } else if (OAuth2Constants.ACCESS_TOKEN_TYPE.equals(subjectTokenType)) {
+            return validateExternalTokenThroughUserInfo(event, subjectToken, subjectTokenType);
+        } else {
+            event.detail(Details.REASON, OAuth2Constants.SUBJECT_TOKEN_TYPE + " invalid");
+            event.error(Errors.INVALID_TOKEN_TYPE);
+            throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token type", Response.Status.BAD_REQUEST);
+        }
     }
 }
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 42b42d9..cf85aa4 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
@@ -40,6 +40,8 @@ import org.keycloak.events.Details;
 import org.keycloak.events.Errors;
 import org.keycloak.events.EventBuilder;
 import org.keycloak.events.EventType;
+import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.jose.jws.JWSInputException;
 import org.keycloak.models.AuthenticationFlowModel;
 import org.keycloak.models.AuthenticatedClientSessionModel;
 import org.keycloak.models.ClientModel;
@@ -58,6 +60,7 @@ import org.keycloak.protocol.oidc.TokenManager;
 import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
 import org.keycloak.representations.AccessToken;
 import org.keycloak.representations.AccessTokenResponse;
+import org.keycloak.representations.JsonWebToken;
 import org.keycloak.services.ErrorPage;
 import org.keycloak.services.ErrorResponseException;
 import org.keycloak.services.ServicesLogger;
@@ -590,15 +593,29 @@ public class TokenEndpoint {
 
         String subjectToken = formParams.getFirst(OAuth2Constants.SUBJECT_TOKEN);
         if (subjectToken != null) {
-            String subjectIssuer = formParams.getFirst(OAuth2Constants.SUBJECT_ISSUER);
+            String subjectTokenType = formParams.getFirst(OAuth2Constants.SUBJECT_TOKEN_TYPE);
             String realmIssuerUrl = Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName());
+            String subjectIssuer = formParams.getFirst(OAuth2Constants.SUBJECT_ISSUER);
+
+            if (subjectIssuer == null && OAuth2Constants.JWT_TOKEN_TYPE.equals(subjectTokenType)) {
+                try {
+                    JWSInput jws = new JWSInput(subjectToken);
+                    JsonWebToken jwt = jws.readJsonContent(JsonWebToken.class);
+                    subjectIssuer = jwt.getIssuer();
+                } catch (JWSInputException e) {
+                    event.detail(Details.REASON, "unable to parse jwt subject_token");
+                    event.error(Errors.INVALID_TOKEN);
+                    throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Invalid token type, must be access token", Response.Status.BAD_REQUEST);
+
+                }
+            }
+
             if (subjectIssuer != null && !realmIssuerUrl.equals(subjectIssuer)) {
                 event.detail(OAuth2Constants.SUBJECT_ISSUER, subjectIssuer);
-                return exchangeExternalToken();
+                return exchangeExternalToken(subjectIssuer, subjectToken);
 
             }
 
-            String subjectTokenType = formParams.getFirst(OAuth2Constants.SUBJECT_TOKEN_TYPE);
             if (subjectTokenType != null && !subjectTokenType.equals(OAuth2Constants.ACCESS_TOKEN_TYPE)) {
                 event.detail(Details.REASON, "subject_token supports access tokens only");
                 event.error(Errors.INVALID_TOKEN);
@@ -670,8 +687,18 @@ public class TokenEndpoint {
         if (requestedIssuer == null) {
             return exchangeClientToClient(tokenUser, tokenSession);
         } else {
-            return exchangeToIdentityProvider(tokenUser, tokenSession, requestedIssuer);
-        }
+            try {
+                return exchangeToIdentityProvider(tokenUser, tokenSession, requestedIssuer);
+            } finally {
+                if (subjectToken == null) { // we are naked! So need to clean up user session
+                    try {
+                        session.sessions().removeUserSession(realm, tokenSession);
+                    } catch (Exception ignore) {
+
+                    }
+                }
+            }
+         }
     }
 
     public Response exchangeToIdentityProvider(UserModel targetUser, UserSessionModel targetUserSession, String requestedIssuer) {
@@ -764,32 +791,49 @@ 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 exchangeExternalToken() {
-        BrokeredIdentityContext context = null;
+    public Response exchangeExternalToken(String issuer, String subjectToken) {
+        ExchangeExternalToken externalIdp = null;
+        IdentityProviderModel externalIdpModel = null;
 
         for (IdentityProviderModel idpModel : realm.getIdentityProviders()) {
             IdentityProviderFactory factory = IdentityBrokerService.getIdentityProviderFactory(session, idpModel);
             IdentityProvider idp = factory.create(session, idpModel);
             if (idp instanceof ExchangeExternalToken) {
-                context = ((ExchangeExternalToken)idp).exchangeExternal(event, formParams);
-                break;
+                ExchangeExternalToken external = (ExchangeExternalToken) idp;
+                if (idpModel.getAlias().equals(issuer) || external.isIssuer(issuer, formParams)) {
+                    externalIdp = external;
+                    externalIdpModel = idpModel;
+                    break;
+                }
             }
         }
-        if (context == null) {
+
+
+        if (externalIdp == null) {
             event.error(Errors.INVALID_ISSUER);
             throw new ErrorResponseException(Errors.INVALID_ISSUER, "Invalid " + OAuth2Constants.SUBJECT_ISSUER + " parameter", Response.Status.BAD_REQUEST);
         }
-        if (!AdminPermissions.management(session, realm).idps().canExchangeTo(client, context.getIdpConfig())) {
+        if (!AdminPermissions.management(session, realm).idps().canExchangeTo(client, externalIdpModel)) {
             event.detail(Details.REASON, "client not allowed to exchange subject_issuer");
             event.error(Errors.NOT_ALLOWED);
             throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
         }
+        BrokeredIdentityContext context = externalIdp.exchangeExternal(event, formParams);
+        if (context == null) {
+            event.error(Errors.INVALID_ISSUER);
+            throw new ErrorResponseException(Errors.INVALID_ISSUER, "Invalid " + OAuth2Constants.SUBJECT_ISSUER + " parameter", Response.Status.BAD_REQUEST);
+        }
 
         UserModel user = importUserFromExternalIdentity(context);
 
         String sessionId = KeycloakModelUtils.generateId();
         UserSessionModel userSession = session.sessions().createUserSession(sessionId, realm, user, user.getUsername(), clientConnection.getRemoteAddr(), "external-exchange", false, null, null);
-        ((ExchangeExternalToken)context.getIdp()).exchangeExternalComplete(userSession, context, formParams);
+        externalIdp.exchangeExternalComplete(userSession, context, formParams);
+
+        // this must exist so that we can obtain access token from user session if idp's store tokens is off
+        userSession.setNote(IdentityProvider.EXTERNAL_IDENTITY_PROVIDER, externalIdpModel.getAlias());
+        userSession.setNote(IdentityProvider.FEDERATED_ACCESS_TOKEN, subjectToken);
+
         return exchangeClientToClient(user, userSession);
 
 
diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
index 078cca7..ac99833 100755
--- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
+++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
@@ -371,6 +371,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
                 return response;
             }
         } catch (IdentityBrokerException e) {
+            e.printStackTrace();
             return redirectToErrorPage(Messages.COULD_NOT_SEND_AUTHENTICATION_REQUEST, e, providerId);
         } catch (Exception e) {
             return redirectToErrorPage(Messages.UNEXPECTED_ERROR_HANDLING_REQUEST, e, providerId);
diff --git a/services/src/main/java/org/keycloak/social/bitbucket/BitbucketIdentityProvider.java b/services/src/main/java/org/keycloak/social/bitbucket/BitbucketIdentityProvider.java
index bb7aa64..b2a62ff 100755
--- a/services/src/main/java/org/keycloak/social/bitbucket/BitbucketIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/bitbucket/BitbucketIdentityProvider.java
@@ -18,6 +18,7 @@
 package org.keycloak.social.bitbucket;
 
 import com.fasterxml.jackson.databind.JsonNode;
+import org.keycloak.OAuthErrorException;
 import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider;
 import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
 import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
@@ -25,7 +26,14 @@ import org.keycloak.broker.provider.BrokeredIdentityContext;
 import org.keycloak.broker.provider.IdentityBrokerException;
 import org.keycloak.broker.provider.util.SimpleHttp;
 import org.keycloak.broker.social.SocialIdentityProvider;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.events.EventBuilder;
 import org.keycloak.models.KeycloakSession;
+import org.keycloak.services.ErrorResponseException;
+
+import javax.ws.rs.core.Response;
+import java.io.IOException;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -35,6 +43,7 @@ public class BitbucketIdentityProvider extends AbstractOAuth2IdentityProvider im
 	public static final String AUTH_URL = "https://bitbucket.org/site/oauth2/authorize";
 	public static final String TOKEN_URL = "https://bitbucket.org/site/oauth2/access_token";
 	public static final String USER_URL = "https://api.bitbucket.org/2.0/user";
+	public static final String USER_EMAIL_URL = "https://api.bitbucket.org/2.0/user/emails";
 	public static final String EMAIL_SCOPE = "email";
 	public static final String ACCOUNT_SCOPE = "account";
 	public static final String DEFAULT_SCOPE = ACCOUNT_SCOPE;
@@ -46,11 +55,116 @@ public class BitbucketIdentityProvider extends AbstractOAuth2IdentityProvider im
 		String defaultScope = config.getDefaultScope();
 
 		if (defaultScope ==  null || defaultScope.trim().equals("")) {
-			config.setDefaultScope(ACCOUNT_SCOPE);
+			config.setDefaultScope(ACCOUNT_SCOPE + " " + EMAIL_SCOPE);
 		}
 	}
 
 	@Override
+	protected boolean supportsExternalExchange() {
+		return true;
+	}
+
+	@Override
+	protected String getProfileEndpointForValidation(EventBuilder event) {
+		return USER_URL;
+	}
+
+	@Override
+	protected BrokeredIdentityContext validateExternalTokenThroughUserInfo(EventBuilder event, String subjectToken, String subjectTokenType) {
+		event.detail("validation_method", "user info");
+		SimpleHttp.Response response = null;
+		int status = 0;
+		try {
+			String userInfoUrl = getProfileEndpointForValidation(event);
+			response = buildUserInfoRequest(subjectToken, userInfoUrl).asResponse();
+			status = response.getStatus();
+		} catch (IOException e) {
+			logger.debug("Failed to invoke user info for external exchange", e);
+		}
+		if (status != 200) {
+			logger.debug("Failed to invoke user info status: " + status);
+			event.detail(Details.REASON, "user info call failure");
+			event.error(Errors.INVALID_TOKEN);
+			throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token", Response.Status.BAD_REQUEST);
+		}
+		JsonNode profile = null;
+		try {
+			profile = response.asJson();
+		} catch (IOException e) {
+			event.detail(Details.REASON, "user info call failure");
+			event.error(Errors.INVALID_TOKEN);
+			throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token", Response.Status.BAD_REQUEST);
+		}
+		String type = getJsonProperty(profile, "type");
+		if (type == null) {
+			event.detail(Details.REASON, "no type data in user info response");
+			event.error(Errors.INVALID_TOKEN);
+			throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token", Response.Status.BAD_REQUEST);
+
+		}
+		if (type.equals("error")) {
+			JsonNode errorNode = profile.get("error");
+			if (errorNode != null) {
+				String errorMsg = getJsonProperty(errorNode, "message");
+				event.detail(Details.REASON, "user info call failure: " + errorMsg);
+				event.error(Errors.INVALID_TOKEN);
+				throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token", Response.Status.BAD_REQUEST);
+			} else {
+				event.detail(Details.REASON, "user info call failure");
+				event.error(Errors.INVALID_TOKEN);
+				throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token", Response.Status.BAD_REQUEST);
+			}
+		}
+		if (!type.equals("user")) {
+			event.detail(Details.REASON, "no user info in response");
+			event.error(Errors.INVALID_TOKEN);
+			throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token", Response.Status.BAD_REQUEST);
+
+		}
+		String id = getJsonProperty(profile, "account_id");
+		if (id == null) {
+				event.detail(Details.REASON, "user info call failure");
+				event.error(Errors.INVALID_TOKEN);
+				throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token", Response.Status.BAD_REQUEST);
+
+		}
+		return extractUserInfo(subjectToken, profile);
+	}
+
+	private BrokeredIdentityContext extractUserInfo(String subjectToken, JsonNode profile) {
+		BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "account_id"));
+
+
+		String username = getJsonProperty(profile, "username");
+		user.setUsername(username);
+		user.setName(getJsonProperty(profile, "display_name"));
+		user.setIdpConfig(getConfig());
+		user.setIdp(this);
+
+		AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
+
+		try {
+			JsonNode emails = SimpleHttp.doGet(USER_EMAIL_URL, session).header("Authorization", "Bearer " + subjectToken).asJson();
+
+			// {"pagelen":10,"values":[{"is_primary":true,"is_confirmed":true,"type":"email","email":"bburke@redhat.com","links":{"self":{"href":"https://api.bitbucket.org/2.0/user/emails/bburke@redhat.com"}}}],"page":1,"size":1}
+			JsonNode emailJson = emails.get("values");
+			if (emailJson != null) {
+                if (emailJson.isArray()) {
+                    emailJson = emailJson.get(0);
+                }
+                if (emailJson != null && "email".equals(getJsonProperty(emailJson, "type"))) {
+                    user.setEmail(getJsonProperty(emailJson, "email"));
+
+                }
+            }
+		} catch (Exception ignore) {
+			logger.debug("failed to get email from BitBucket", ignore);
+
+		}
+		return user;
+	}
+
+	@Override
 	protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
 		try {
 			JsonNode profile = SimpleHttp.doGet(USER_URL, session).header("Authorization", "Bearer " + accessToken).asJson();
@@ -74,16 +188,7 @@ public class BitbucketIdentityProvider extends AbstractOAuth2IdentityProvider im
 				throw new IdentityBrokerException("Could not obtain account information from bitbucket.");
 
 			}
-			BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "account_id"));
-
-			String username = getJsonProperty(profile, "username");
-			user.setUsername(username);
-			user.setIdpConfig(getConfig());
-			user.setIdp(this);
-
-			AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
-
-			return user;
+			return extractUserInfo(accessToken, profile);
 		} catch (Exception e) {
 			if (e instanceof IdentityBrokerException) throw (IdentityBrokerException)e;
 			throw new IdentityBrokerException("Could not obtain user profile from github.", e);
diff --git a/services/src/main/java/org/keycloak/social/facebook/FacebookIdentityProvider.java b/services/src/main/java/org/keycloak/social/facebook/FacebookIdentityProvider.java
index 54be72c..57c0d03 100755
--- a/services/src/main/java/org/keycloak/social/facebook/FacebookIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/facebook/FacebookIdentityProvider.java
@@ -18,6 +18,7 @@
 package org.keycloak.social.facebook;
 
 import com.fasterxml.jackson.databind.JsonNode;
+import org.keycloak.OAuthErrorException;
 import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider;
 import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
 import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
@@ -25,7 +26,14 @@ import org.keycloak.broker.provider.BrokeredIdentityContext;
 import org.keycloak.broker.provider.IdentityBrokerException;
 import org.keycloak.broker.provider.util.SimpleHttp;
 import org.keycloak.broker.social.SocialIdentityProvider;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.events.EventBuilder;
 import org.keycloak.models.KeycloakSession;
+import org.keycloak.services.ErrorResponseException;
+
+import javax.ws.rs.core.Response;
+import java.io.IOException;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -48,45 +56,60 @@ public class FacebookIdentityProvider extends AbstractOAuth2IdentityProvider imp
 		try {
 			JsonNode profile = SimpleHttp.doGet(PROFILE_URL, session).header("Authorization", "Bearer " + accessToken).asJson();
 
-			String id = getJsonProperty(profile, "id");
+			return extractIdentityFromProfile(null, profile);
+		} catch (Exception e) {
+			throw new IdentityBrokerException("Could not obtain user profile from facebook.", e);
+		}
+	}
+
+	@Override
+	protected boolean supportsExternalExchange() {
+		return true;
+	}
+
+	@Override
+	protected String getProfileEndpointForValidation(EventBuilder event) {
+		return PROFILE_URL;
+	}
+
+	@Override
+	protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, JsonNode profile) {
+		String id = getJsonProperty(profile, "id");
 
-			BrokeredIdentityContext user = new BrokeredIdentityContext(id);
+		BrokeredIdentityContext user = new BrokeredIdentityContext(id);
 
-			String email = getJsonProperty(profile, "email");
+		String email = getJsonProperty(profile, "email");
 
-			user.setEmail(email);
+		user.setEmail(email);
 
-			String username = getJsonProperty(profile, "username");
+		String username = getJsonProperty(profile, "username");
 
-			if (username == null) {
-				if (email != null) {
-					username = email;
-				} else {
-					username = id;
-				}
-			}
+		if (username == null) {
+            if (email != null) {
+                username = email;
+            } else {
+                username = id;
+            }
+        }
 
-			user.setUsername(username);
+		user.setUsername(username);
 
-			String firstName = getJsonProperty(profile, "first_name");
-			String lastName = getJsonProperty(profile, "last_name");
+		String firstName = getJsonProperty(profile, "first_name");
+		String lastName = getJsonProperty(profile, "last_name");
 
-			if (lastName == null) {
-				lastName = "";
-			} else {
-				lastName = " " + lastName;
-			}
+		if (lastName == null) {
+            lastName = "";
+        } else {
+            lastName = " " + lastName;
+        }
 
-			user.setName(firstName + lastName);
-			user.setIdpConfig(getConfig());
-			user.setIdp(this);
+		user.setName(firstName + lastName);
+		user.setIdpConfig(getConfig());
+		user.setIdp(this);
 
-			AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
+		AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
 
-			return user;
-		} catch (Exception e) {
-			throw new IdentityBrokerException("Could not obtain user profile from facebook.", e);
-		}
+		return user;
 	}
 
 	@Override
diff --git a/services/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java b/services/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java
index 4120e43..9b04b76 100755
--- a/services/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java
@@ -25,6 +25,7 @@ import org.keycloak.broker.provider.BrokeredIdentityContext;
 import org.keycloak.broker.provider.IdentityBrokerException;
 import org.keycloak.broker.provider.util.SimpleHttp;
 import org.keycloak.broker.social.SocialIdentityProvider;
+import org.keycloak.events.EventBuilder;
 import org.keycloak.models.KeycloakSession;
 
 /**
@@ -45,22 +46,39 @@ public class GitHubIdentityProvider extends AbstractOAuth2IdentityProvider imple
 	}
 
 	@Override
-	protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
-		try {
-			JsonNode profile = SimpleHttp.doGet(PROFILE_URL, session).header("Authorization", "Bearer " + accessToken).asJson();
+	protected boolean supportsExternalExchange() {
+		return true;
+	}
+
+	@Override
+	protected String getProfileEndpointForValidation(EventBuilder event) {
+		return PROFILE_URL;
+	}
+
+	@Override
+	protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, JsonNode profile) {
+		BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "id"));
 
-			BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "id"));
+		String username = getJsonProperty(profile, "login");
+		user.setUsername(username);
+		user.setName(getJsonProperty(profile, "name"));
+		user.setEmail(getJsonProperty(profile, "email"));
+		user.setIdpConfig(getConfig());
+		user.setIdp(this);
 
-			String username = getJsonProperty(profile, "login");
-			user.setUsername(username);
-			user.setName(getJsonProperty(profile, "name"));
-			user.setEmail(getJsonProperty(profile, "email"));
-			user.setIdpConfig(getConfig());
-			user.setIdp(this);
+		AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
 
-			AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
+		return user;
+
+	}
+
+
+	@Override
+	protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
+		try {
+			JsonNode profile = SimpleHttp.doGet(PROFILE_URL, session).header("Authorization", "Bearer " + accessToken).asJson();
 
-			return user;
+			return extractIdentityFromProfile(null, profile);
 		} catch (Exception e) {
 			throw new IdentityBrokerException("Could not obtain user profile from github.", e);
 		}
diff --git a/services/src/main/java/org/keycloak/social/gitlab/GitLabIdentityProvider.java b/services/src/main/java/org/keycloak/social/gitlab/GitLabIdentityProvider.java
index f700d45..b781cd4 100755
--- a/services/src/main/java/org/keycloak/social/gitlab/GitLabIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/gitlab/GitLabIdentityProvider.java
@@ -18,19 +18,26 @@
 package org.keycloak.social.gitlab;
 
 import com.fasterxml.jackson.databind.JsonNode;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.OAuthErrorException;
 import org.keycloak.broker.oidc.OIDCIdentityProvider;
 import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
 import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
 import org.keycloak.broker.provider.BrokeredIdentityContext;
+import org.keycloak.broker.provider.IdentityBrokerException;
 import org.keycloak.broker.provider.util.SimpleHttp;
 import org.keycloak.broker.social.SocialIdentityProvider;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
 import org.keycloak.events.EventBuilder;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.representations.AccessTokenResponse;
 import org.keycloak.representations.IDToken;
 import org.keycloak.representations.JsonWebToken;
+import org.keycloak.services.ErrorResponseException;
 
 import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
 import java.io.IOException;
 
 /**
@@ -56,37 +63,62 @@ public class GitLabIdentityProvider extends OIDCIdentityProvider  implements Soc
 		}
 	}
 
-	protected BrokeredIdentityContext extractIdentity(AccessTokenResponse tokenResponse, String accessToken, JsonWebToken idToken) throws IOException {
-		String id = idToken.getSubject();
-		BrokeredIdentityContext identity = new BrokeredIdentityContext(id);
-		String name = (String)idToken.getOtherClaims().get(IDToken.NAME);
-		String preferredUsername = (String)idToken.getOtherClaims().get(IDToken.NICKNAME);
-		String email = (String)idToken.getOtherClaims().get(IDToken.EMAIL);
-
-		if (getConfig().getDefaultScope().contains(API_SCOPE)) {
-			String userInfoUrl = getUserInfoUrl();
-			if (userInfoUrl != null && !userInfoUrl.isEmpty() && (id == null || name == null || preferredUsername == null || email == null)) {
-				JsonNode userInfo = SimpleHttp.doGet(userInfoUrl, session)
-						.header("Authorization", "Bearer " + accessToken).asJson();
-
-				name = getJsonProperty(userInfo, "name");
-				preferredUsername = getJsonProperty(userInfo, "username");
-				email = getJsonProperty(userInfo, "email");
-				AbstractJsonUserAttributeMapper.storeUserProfileForMapper(identity, userInfo, getConfig().getAlias());
-			}
+	protected String getUsernameFromUserInfo(JsonNode userInfo) {
+		return getJsonProperty(userInfo, "username");
+	}
+
+	protected String getusernameClaimNameForIdToken() {
+		return IDToken.NICKNAME;
+	}
+
+	@Override
+	protected boolean supportsExternalExchange() {
+		return true;
+	}
+
+	@Override
+	protected String getProfileEndpointForValidation(EventBuilder event) {
+		return getUserInfoUrl();
+	}
+
+	@Override
+	public boolean isIssuer(String issuer, MultivaluedMap<String, String> params) {
+		String requestedIssuer = params.getFirst(OAuth2Constants.SUBJECT_ISSUER);
+		if (requestedIssuer == null) requestedIssuer = issuer;
+		return requestedIssuer.equals(getConfig().getAlias());
+	}
+
+
+	@Override
+	protected BrokeredIdentityContext exchangeExternalImpl(EventBuilder event, MultivaluedMap<String, String> params) {
+		return exchangeExternalUserInfoValidationOnly(event, params);
+	}
+
+	@Override
+	protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, JsonNode profile) {
+		String id = getJsonProperty(profile, "id");
+		if (id == null) {
+			event.detail(Details.REASON, "id claim is null from user info json");
+			event.error(Errors.INVALID_TOKEN);
+			throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token", Response.Status.BAD_REQUEST);
 		}
-		identity.getContextData().put(FEDERATED_ACCESS_TOKEN_RESPONSE, tokenResponse);
-		identity.getContextData().put(VALIDATED_ID_TOKEN, idToken);
-		processAccessTokenResponse(identity, tokenResponse);
+		return gitlabExtractFromProfile(profile);
+	}
+
+	private BrokeredIdentityContext gitlabExtractFromProfile(JsonNode profile) {
+		String id = getJsonProperty(profile, "id");
+		BrokeredIdentityContext identity = new BrokeredIdentityContext(id);
+
+		String name = getJsonProperty(profile, "name");
+		String preferredUsername = getJsonProperty(profile, "username");
+		String email = getJsonProperty(profile, "email");
+		AbstractJsonUserAttributeMapper.storeUserProfileForMapper(identity, profile, getConfig().getAlias());
 
 		identity.setId(id);
 		identity.setName(name);
 		identity.setEmail(email);
 
 		identity.setBrokerUserId(getConfig().getAlias() + "." + id);
-		if (tokenResponse.getSessionState() != null) {
-			identity.setBrokerSessionId(getConfig().getAlias() + "." + tokenResponse.getSessionState());
-		}
 
 		if (preferredUsername == null) {
 			preferredUsername = email;
@@ -100,13 +132,54 @@ public class GitLabIdentityProvider extends OIDCIdentityProvider  implements Soc
 		return identity;
 	}
 
-	@Override
-	public BrokeredIdentityContext exchangeExternal(EventBuilder event, MultivaluedMap<String, String> params) {
-		return null;
+
+	protected BrokeredIdentityContext extractIdentity(AccessTokenResponse tokenResponse, String accessToken, JsonWebToken idToken) throws IOException {
+
+		SimpleHttp.Response response = null;
+		int status = 0;
+
+		for (int i = 0; i < 10; i++) {
+			try {
+				String userInfoUrl = getUserInfoUrl();
+				response = SimpleHttp.doGet(userInfoUrl, session)
+						.header("Authorization", "Bearer " + accessToken).asResponse();
+				status = response.getStatus();
+			} catch (IOException e) {
+				logger.debug("Failed to invoke user info for external exchange", e);
+			}
+			if (status == 200) break;
+			response.close();
+			try {
+				Thread.sleep(200);
+			} catch (InterruptedException e) {
+				throw new RuntimeException(e);
+			}
+		}
+		if (status != 200) {
+			logger.debug("Failed to invoke user info status: " + status);
+			throw new IdentityBrokerException("Gitlab user info call failure");
+		}
+		JsonNode profile = null;
+		try {
+			profile = response.asJson();
+		} catch (IOException e) {
+			throw new IdentityBrokerException("Gitlab user info call failure");
+		}
+		String id = getJsonProperty(profile, "id");
+		if (id == null) {
+			throw new IdentityBrokerException("Gitlab id claim is null from user info json");
+		}
+		BrokeredIdentityContext identity = gitlabExtractFromProfile(profile);
+		identity.getContextData().put(FEDERATED_ACCESS_TOKEN_RESPONSE, tokenResponse);
+		identity.getContextData().put(VALIDATED_ID_TOKEN, idToken);
+		processAccessTokenResponse(identity, tokenResponse);
+
+		return identity;
 	}
 
 
 
 
 
+
 }
diff --git a/services/src/main/java/org/keycloak/social/google/GoogleIdentityProvider.java b/services/src/main/java/org/keycloak/social/google/GoogleIdentityProvider.java
index 10b29db..f3e4990 100755
--- a/services/src/main/java/org/keycloak/social/google/GoogleIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/google/GoogleIdentityProvider.java
@@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.JsonNode;
 import org.jboss.resteasy.spi.ResteasyProviderFactory;
 import org.keycloak.OAuth2Constants;
 import org.keycloak.OAuthErrorException;
+import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider;
 import org.keycloak.broker.oidc.KeycloakOIDCIdentityProvider;
 import org.keycloak.broker.oidc.OIDCIdentityProvider;
 import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
@@ -79,48 +80,23 @@ public class GoogleIdentityProvider extends OIDCIdentityProvider implements Soci
         return uri;
     }
 
-    protected BrokeredIdentityContext extractIdentity(AccessTokenResponse tokenResponse, String accessToken, JsonWebToken idToken) throws IOException {
-        String id = idToken.getSubject();
-        BrokeredIdentityContext identity = new BrokeredIdentityContext(id);
-        String name = (String) idToken.getOtherClaims().get(IDToken.NAME);
-        String preferredUsername = (String) idToken.getOtherClaims().get(getUsernameClaimName());
-        String email = (String) idToken.getOtherClaims().get(IDToken.EMAIL);
-
-         identity.getContextData().put(VALIDATED_ID_TOKEN, idToken);
-
-        if (!getConfig().isDisableUserInfoService() && accessToken != null && name == null) {
-            JsonNode userInfo = SimpleHttp.doGet(getUserInfoUrl(), session)
-                    .header("Authorization", "Bearer " + accessToken).asJson();
-            name = getJsonProperty(userInfo, "name");
-        }
-
-        identity.setId(id);
-        identity.setName(name);
-        identity.setEmail(email);
-
-        identity.setBrokerUserId(getConfig().getAlias() + "." + id);
-
-        if (preferredUsername == null) {
-            preferredUsername = email;
-        }
+    @Override
+    protected boolean supportsExternalExchange() {
+        return true;
+    }
 
-        if (preferredUsername == null) {
-            preferredUsername = id;
-        }
 
-        identity.setUsername(preferredUsername);
-        if (tokenResponse != null && tokenResponse.getSessionState() != null) {
-            identity.setBrokerSessionId(getConfig().getAlias() + "." + tokenResponse.getSessionState());
-        }
-        if (tokenResponse != null) identity.getContextData().put(FEDERATED_ACCESS_TOKEN_RESPONSE, tokenResponse);
-        if (tokenResponse != null) processAccessTokenResponse(identity, tokenResponse);
-        return identity;
+    @Override
+    public boolean isIssuer(String issuer, MultivaluedMap<String, String> params) {
+        String requestedIssuer = params.getFirst(OAuth2Constants.SUBJECT_ISSUER);
+        if (requestedIssuer == null) requestedIssuer = issuer;
+        return requestedIssuer.equals(getConfig().getAlias());
     }
 
 
     @Override
-    public BrokeredIdentityContext exchangeExternal(EventBuilder event, MultivaluedMap<String, String> params) {
-        return null;
+    protected BrokeredIdentityContext exchangeExternalImpl(EventBuilder event, MultivaluedMap<String, String> params) {
+        return exchangeExternalUserInfoValidationOnly(event, params);
     }
 
 
diff --git a/services/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProvider.java b/services/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProvider.java
index e25bcfa..c30db0c 100755
--- a/services/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/linkedin/LinkedInIdentityProvider.java
@@ -25,6 +25,7 @@ import org.keycloak.broker.provider.BrokeredIdentityContext;
 import org.keycloak.broker.provider.IdentityBrokerException;
 import org.keycloak.broker.provider.util.SimpleHttp;
 import org.keycloak.broker.social.SocialIdentityProvider;
+import org.keycloak.events.EventBuilder;
 import org.keycloak.models.KeycloakSession;
 
 import java.net.MalformedURLException;
@@ -53,23 +54,39 @@ public class LinkedInIdentityProvider extends AbstractOAuth2IdentityProvider imp
 	}
 
 	@Override
-	protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
-		log.debug("doGetFederatedIdentity()");
-		try {
-			JsonNode profile = SimpleHttp.doGet(PROFILE_URL, session).header("Authorization", "Bearer " + accessToken).asJson();
+	protected boolean supportsExternalExchange() {
+		return true;
+	}
+
+	@Override
+	protected String getProfileEndpointForValidation(EventBuilder event) {
+		return PROFILE_URL;
+	}
+
+	@Override
+	protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, JsonNode profile) {
+		BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "id"));
+
+		String username = extractUsernameFromProfileURL(getJsonProperty(profile, "publicProfileUrl"));
+		user.setUsername(username);
+		user.setName(getJsonProperty(profile, "formattedName"));
+		user.setEmail(getJsonProperty(profile, "emailAddress"));
+		user.setIdpConfig(getConfig());
+		user.setIdp(this);
 
-			BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "id"));
+		AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
 
-			String username = extractUsernameFromProfileURL(getJsonProperty(profile, "publicProfileUrl"));
-			user.setUsername(username);
-			user.setName(getJsonProperty(profile, "formattedName"));
-			user.setEmail(getJsonProperty(profile, "emailAddress"));
-			user.setIdpConfig(getConfig());
-			user.setIdp(this);
+		return user;
 
-			AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
+	}
 
-			return user;
+
+	@Override
+	protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
+		log.debug("doGetFederatedIdentity()");
+		try {
+			JsonNode profile = SimpleHttp.doGet(PROFILE_URL, session).header("Authorization", "Bearer " + accessToken).asJson();
+			return extractIdentityFromProfile(null, profile);
 		} catch (Exception e) {
 			throw new IdentityBrokerException("Could not obtain user profile from linkedIn.", e);
 		}
diff --git a/services/src/main/java/org/keycloak/social/microsoft/MicrosoftIdentityProvider.java b/services/src/main/java/org/keycloak/social/microsoft/MicrosoftIdentityProvider.java
index 17dde5e..5df1ce9 100755
--- a/services/src/main/java/org/keycloak/social/microsoft/MicrosoftIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/microsoft/MicrosoftIdentityProvider.java
@@ -19,6 +19,7 @@ package org.keycloak.social.microsoft;
 
 import com.fasterxml.jackson.databind.JsonNode;
 import org.jboss.logging.Logger;
+import org.keycloak.OAuthErrorException;
 import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider;
 import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
 import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
@@ -27,8 +28,15 @@ import org.keycloak.broker.provider.IdentityBrokerException;
 import org.keycloak.broker.provider.util.SimpleHttp;
 import org.keycloak.broker.social.SocialIdentityProvider;
 
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.events.EventBuilder;
 import org.keycloak.models.KeycloakSession;
+import org.keycloak.services.ErrorResponseException;
 
+import javax.ws.rs.core.Response;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
 import java.net.URLEncoder;
 
 /**
@@ -54,6 +62,27 @@ public class MicrosoftIdentityProvider extends AbstractOAuth2IdentityProvider im
     }
 
     @Override
+    protected boolean supportsExternalExchange() {
+        return true;
+    }
+
+    @Override
+    protected String getProfileEndpointForValidation(EventBuilder event) {
+        return PROFILE_URL;
+    }
+
+    @Override
+    protected SimpleHttp buildUserInfoRequest(String subjectToken, String userInfoUrl) {
+        String URL = null;
+        try {
+            URL = PROFILE_URL + "?access_token=" + URLEncoder.encode(subjectToken, "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException(e);
+        }
+        return SimpleHttp.doGet(URL, session);
+    }
+
+    @Override
     protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
         try {
             String URL = PROFILE_URL + "?access_token=" + URLEncoder.encode(accessToken, "UTF-8");
@@ -62,29 +91,34 @@ public class MicrosoftIdentityProvider extends AbstractOAuth2IdentityProvider im
             }
             JsonNode profile = SimpleHttp.doGet(URL, session).asJson();
 
-            String id = getJsonProperty(profile, "id");
+            return extractIdentityFromProfile(null, profile);
+        } catch (Exception e) {
+            throw new IdentityBrokerException("Could not obtain user profile from Microsoft Live ID.", e);
+        }
+    }
 
-            String email = null;
-            if (profile.has("emails")) {
-                email = getJsonProperty(profile.get("emails"), "preferred");
-            }
+    @Override
+    protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, JsonNode profile) {
+        String id = getJsonProperty(profile, "id");
 
-            BrokeredIdentityContext user = new BrokeredIdentityContext(id);
+        String email = null;
+        if (profile.has("emails")) {
+            email = getJsonProperty(profile.get("emails"), "preferred");
+        }
 
-            user.setUsername(email != null ? email : id);
-            user.setFirstName(getJsonProperty(profile, "first_name"));
-            user.setLastName(getJsonProperty(profile, "last_name"));
-            if (email != null)
-                user.setEmail(email);
-            user.setIdpConfig(getConfig());
-            user.setIdp(this);
+        BrokeredIdentityContext user = new BrokeredIdentityContext(id);
 
-            AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
+        user.setUsername(email != null ? email : id);
+        user.setFirstName(getJsonProperty(profile, "first_name"));
+        user.setLastName(getJsonProperty(profile, "last_name"));
+        if (email != null)
+            user.setEmail(email);
+        user.setIdpConfig(getConfig());
+        user.setIdp(this);
 
-            return user;
-        } catch (Exception e) {
-            throw new IdentityBrokerException("Could not obtain user profile from Microsoft Live ID.", e);
-        }
+        AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
+
+        return user;
     }
 
     @Override
diff --git a/services/src/main/java/org/keycloak/social/openshift/OpenshiftV3IdentityProvider.java b/services/src/main/java/org/keycloak/social/openshift/OpenshiftV3IdentityProvider.java
index fafa425..fa58386 100644
--- a/services/src/main/java/org/keycloak/social/openshift/OpenshiftV3IdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/openshift/OpenshiftV3IdentityProvider.java
@@ -1,15 +1,22 @@
 package org.keycloak.social.openshift;
 
 import com.fasterxml.jackson.databind.JsonNode;
+import org.keycloak.OAuthErrorException;
 import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider;
 import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
 import org.keycloak.broker.provider.BrokeredIdentityContext;
 import org.keycloak.broker.provider.IdentityBrokerException;
 import org.keycloak.broker.provider.util.SimpleHttp;
 import org.keycloak.broker.social.SocialIdentityProvider;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.events.EventBuilder;
 import org.keycloak.models.KeycloakSession;
+import org.keycloak.services.ErrorResponseException;
 
+import javax.ws.rs.core.Response;
 import java.io.IOException;
+import java.net.URLEncoder;
 import java.util.Optional;
 
 /**
@@ -63,4 +70,21 @@ public class OpenshiftV3IdentityProvider extends AbstractOAuth2IdentityProvider<
                              .asJson();
     }
 
+    @Override
+    protected boolean supportsExternalExchange() {
+        return true;
+    }
+
+    @Override
+    protected String getProfileEndpointForValidation(EventBuilder event) {
+        return getConfig().getUserInfoUrl();
+    }
+
+    @Override
+    protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, JsonNode profile) {
+        final BrokeredIdentityContext user = extractUserContext(profile.get("metadata"));
+        AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
+        return user;
+    }
+
 }
diff --git a/services/src/main/java/org/keycloak/social/paypal/PayPalIdentityProvider.java b/services/src/main/java/org/keycloak/social/paypal/PayPalIdentityProvider.java
index a3f4602..17a8353 100644
--- a/services/src/main/java/org/keycloak/social/paypal/PayPalIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/paypal/PayPalIdentityProvider.java
@@ -25,6 +25,7 @@ import org.keycloak.broker.provider.BrokeredIdentityContext;
 import org.keycloak.broker.provider.IdentityBrokerException;
 import org.keycloak.broker.provider.util.SimpleHttp;
 import org.keycloak.broker.social.SocialIdentityProvider;
+import org.keycloak.events.EventBuilder;
 import org.keycloak.models.KeycloakSession;
 
 /**
@@ -46,21 +47,36 @@ public class PayPalIdentityProvider extends AbstractOAuth2IdentityProvider<PayPa
 	}
 
 	@Override
-	protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
-		try {
-			JsonNode profile = SimpleHttp.doGet(getConfig().getUserInfoUrl(), session).header("Authorization", "Bearer " + accessToken).asJson();
+	protected boolean supportsExternalExchange() {
+		return true;
+	}
 
-			BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "user_id"));
+	@Override
+	protected String getProfileEndpointForValidation(EventBuilder event) {
+		return getConfig().getUserInfoUrl();
+	}
+
+	@Override
+	protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, JsonNode profile) {
+		BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "user_id"));
 
-			user.setUsername(getJsonProperty(profile, "email"));
-			user.setName(getJsonProperty(profile, "name"));
-			user.setEmail(getJsonProperty(profile, "email"));
-			user.setIdpConfig(getConfig());
-			user.setIdp(this);
+		user.setUsername(getJsonProperty(profile, "email"));
+		user.setName(getJsonProperty(profile, "name"));
+		user.setEmail(getJsonProperty(profile, "email"));
+		user.setIdpConfig(getConfig());
+		user.setIdp(this);
 
-			AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
+		AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
+		return user;
+	}
+
+
+	@Override
+	protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
+		try {
+			JsonNode profile = SimpleHttp.doGet(getConfig().getUserInfoUrl(), session).header("Authorization", "Bearer " + accessToken).asJson();
 
-			return user;
+			return extractIdentityFromProfile(null, profile);
 		} catch (Exception e) {
 			throw new IdentityBrokerException("Could not obtain user profile from paypal.", e);
 		}
diff --git a/services/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProvider.java b/services/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProvider.java
index 9a0992a..b44de94 100755
--- a/services/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProvider.java
@@ -24,12 +24,15 @@ import org.keycloak.broker.provider.BrokeredIdentityContext;
 import org.keycloak.broker.provider.IdentityBrokerException;
 import org.keycloak.broker.provider.util.SimpleHttp;
 import org.keycloak.broker.social.SocialIdentityProvider;
+import org.keycloak.events.EventBuilder;
 import org.keycloak.models.KeycloakSession;
 
 import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.net.URLDecoder;
+import java.net.URLEncoder;
 import java.util.HashMap;
 
 /**
@@ -54,6 +57,41 @@ public class StackoverflowIdentityProvider extends AbstractOAuth2IdentityProvide
 	}
 
 	@Override
+	protected boolean supportsExternalExchange() {
+		return true;
+	}
+
+	@Override
+	protected String getProfileEndpointForValidation(EventBuilder event) {
+		return PROFILE_URL;
+	}
+
+	@Override
+	protected SimpleHttp buildUserInfoRequest(String subjectToken, String userInfoUrl) {
+		String URL = PROFILE_URL + "&access_token=" + subjectToken + "&key=" + getConfig().getKey();
+		return SimpleHttp.doGet(URL, session);
+	}
+
+	@Override
+	protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, JsonNode node) {
+		JsonNode profile = node.get("items").get(0);
+
+		BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "user_id"));
+
+		String username = extractUsernameFromProfileURL(getJsonProperty(profile, "link"));
+		user.setUsername(username);
+		user.setName(unescapeHtml3(getJsonProperty(profile, "display_name")));
+		// email is not provided
+		// user.setEmail(getJsonProperty(profile, "email"));
+		user.setIdpConfig(getConfig());
+		user.setIdp(this);
+
+		AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
+
+		return user;
+	}
+
+	@Override
 	protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
 		log.debug("doGetFederatedIdentity()");
 		try {
@@ -62,21 +100,7 @@ public class StackoverflowIdentityProvider extends AbstractOAuth2IdentityProvide
 			if (log.isDebugEnabled()) {
 				log.debug("StackOverflow profile request to: " + URL);
 			}
-			JsonNode profile = SimpleHttp.doGet(URL, session).asJson().get("items").get(0);
-
-			BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "user_id"));
-
-			String username = extractUsernameFromProfileURL(getJsonProperty(profile, "link"));
-			user.setUsername(username);
-			user.setName(unescapeHtml3(getJsonProperty(profile, "display_name")));
-			// email is not provided
-			// user.setEmail(getJsonProperty(profile, "email"));
-			user.setIdpConfig(getConfig());
-			user.setIdp(this);
-
-			AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
-
-			return user;
+			return extractIdentityFromProfile(null, SimpleHttp.doGet(URL, session).asJson());
 		} catch (Exception e) {
 			throw new IdentityBrokerException("Could not obtain user profile from Stackoverflow: " + e.getMessage(), e);
 		}
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 afcbf4e..3212f36 100755
--- a/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java
@@ -18,13 +18,13 @@ 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.ExchangeTokenToIdentityProviderToken;
+import org.keycloak.broker.provider.IdentityProvider;
 import org.keycloak.broker.social.SocialIdentityProvider;
 import org.keycloak.common.ClientConnection;
 import org.keycloak.events.Details;
@@ -142,7 +142,7 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
     }
 
     protected Response exchangeSessionToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject) {
-        String accessToken = tokenUserSession.getNote(AbstractOAuth2IdentityProvider.FEDERATED_ACCESS_TOKEN);
+        String accessToken = tokenUserSession.getNote(IdentityProvider.FEDERATED_ACCESS_TOKEN);
         if (accessToken == null) {
             return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
         }
@@ -226,7 +226,7 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
                 if (getConfig().isStoreToken()) {
                     identity.setToken(token);
                 }
-                identity.getContextData().put(AbstractOAuth2IdentityProvider.FEDERATED_ACCESS_TOKEN, token);
+                identity.getContextData().put(IdentityProvider.FEDERATED_ACCESS_TOKEN, token);
 
                 identity.setIdpConfig(getConfig());
                 identity.setCode(state);
@@ -256,7 +256,7 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
 
     @Override
     public void authenticationFinished(AuthenticationSessionModel authSession, BrokeredIdentityContext context) {
-        authSession.setUserSessionNote(AbstractOAuth2IdentityProvider.FEDERATED_ACCESS_TOKEN, (String)context.getContextData().get(AbstractOAuth2IdentityProvider.FEDERATED_ACCESS_TOKEN));
+        authSession.setUserSessionNote(IdentityProvider.FEDERATED_ACCESS_TOKEN, (String)context.getContextData().get(IdentityProvider.FEDERATED_ACCESS_TOKEN));
 
     }
 
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/social/BitbucketLoginPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/social/BitbucketLoginPage.java
new file mode 100644
index 0000000..4f1392d
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/social/BitbucketLoginPage.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.pages.social;
+
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+
+/**
+ * @author Vaclav Muzikar <vmuzikar@redhat.com>
+ */
+public class BitbucketLoginPage extends AbstractSocialLoginPage {
+    @FindBy(name = "username")
+    private WebElement usernameInput;
+
+    @FindBy(name = "password")
+    private WebElement passwordInput;
+
+    @FindBy(name = "commit")
+    private WebElement loginButton;
+
+    @Override
+    public void login(String user, String password) {
+        usernameInput.sendKeys(user);
+        passwordInput.sendKeys(password);
+        passwordInput.sendKeys(Keys.RETURN);
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/social/GitLabLoginPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/social/GitLabLoginPage.java
new file mode 100644
index 0000000..04b91e2
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/social/GitLabLoginPage.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.pages.social;
+
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.NoSuchElementException;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+
+/**
+ * @author Vaclav Muzikar <vmuzikar@redhat.com>
+ */
+public class GitLabLoginPage extends AbstractSocialLoginPage {
+    @FindBy(id = "user_login")
+    //@FindBy(name = "user[login]")
+    private WebElement usernameInput;
+
+    @FindBy(id = "user_password")
+    //@FindBy(name = "user[password]")
+    private WebElement passwordInput;
+
+    @FindBy(name = "commit")
+    private WebElement loginButton;
+
+    @Override
+    public void login(String user, String password) {
+        usernameInput.sendKeys(user);
+        passwordInput.sendKeys(password);
+        passwordInput.sendKeys(Keys.RETURN);
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/GreenMailRule.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/GreenMailRule.java
index 7ebaa1d..efc82f3 100755
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/GreenMailRule.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/GreenMailRule.java
@@ -35,9 +35,20 @@ public class GreenMailRule extends ExternalResource {
 
     private GreenMail greenMail;
 
+    private int port = 3025;
+    private String host = "localhost";
+
+    public GreenMailRule() {
+    }
+
+    public GreenMailRule(int port, String host) {
+        this.port = port;
+        this.host = host;
+    }
+
     @Override
     protected void before() throws Throwable {
-        ServerSetup setup = new ServerSetup(3025, "localhost", "smtp");
+        ServerSetup setup = new ServerSetup(port, host, "smtp");
 
         greenMail = new GreenMail(setup);
         greenMail.start();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractBrokerLinkAndTokenExchangeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractBrokerLinkAndTokenExchangeTest.java
index be579e2..9bc9519 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractBrokerLinkAndTokenExchangeTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractBrokerLinkAndTokenExchangeTest.java
@@ -464,40 +464,58 @@ public abstract class AbstractBrokerLinkAndTokenExchangeTest extends AbstractSer
             IdentityProviderRepresentation rep = adminClient.realm(CHILD_IDP).identityProviders().get(PARENT_IDP).toRepresentation();
             rep.getConfig().put(OIDCIdentityProviderConfig.VALIDATE_SIGNATURE, String.valueOf(false));
             adminClient.realm(CHILD_IDP).identityProviders().get(PARENT_IDP).update(rep);
-            // test failure that validate signatures not set up yet.
+            // test user info validation.
             Response response = exchangeUrl.request()
                     .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(ClientApp.DEPLOYMENT_NAME, "password"))
                     .post(Entity.form(
                             new Form()
                                     .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
                                     .param(OAuth2Constants.SUBJECT_TOKEN, accessToken)
-                                    .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.JWT_ACCESS_TOKEN_TYPE)
+                                    .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.JWT_TOKEN_TYPE)
                                     .param(OAuth2Constants.SUBJECT_ISSUER, PARENT_IDP)
 
                     ));
-            Assert.assertEquals(400, response.getStatus());
-            String json = response.readEntity(String.class);
-            System.out.println(json);
-            Assert.assertTrue(json.contains("Invalid server config"));
+            Assert.assertEquals(200, response.getStatus());
+            AccessTokenResponse tokenResponse = response.readEntity(AccessTokenResponse.class);
+            String exchangedAccessToken = tokenResponse.getToken();
+            Assert.assertNotNull(exchangedAccessToken);
+            response.close();
+
+            Assert.assertEquals(1, adminClient.realm(CHILD_IDP).getClientSessionStats().size());
+
+            // test logout
+            response = childLogoutWebTarget(httpClient)
+                    .queryParam("id_token_hint", exchangedAccessToken)
+                    .request()
+                    .get();
+            response.close();
+
+            Assert.assertEquals(0, adminClient.realm(CHILD_IDP).getClientSessionStats().size());
+
         }
         IdentityProviderRepresentation rep = adminClient.realm(CHILD_IDP).identityProviders().get(PARENT_IDP).toRepresentation();
         rep.getConfig().put(OIDCIdentityProviderConfig.VALIDATE_SIGNATURE, String.valueOf(true));
         rep.getConfig().put(OIDCIdentityProviderConfig.USE_JWKS_URL, String.valueOf(true));
         rep.getConfig().put(OIDCIdentityProviderConfig.JWKS_URL, parentJwksUrl());
+        String parentIssuer = UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT)
+                .path("/realms")
+                .path(PARENT_IDP)
+                .build().toString();
+        rep.getConfig().put("issuer", parentIssuer);
         adminClient.realm(CHILD_IDP).identityProviders().get(PARENT_IDP).update(rep);
 
         String exchangedUserId = null;
         String exchangedUsername = null;
 
         {
-            // valid exchange
+            // test signature validation
             Response response = exchangeUrl.request()
                     .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(ClientApp.DEPLOYMENT_NAME, "password"))
                     .post(Entity.form(
                             new Form()
                                     .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
                                     .param(OAuth2Constants.SUBJECT_TOKEN, accessToken)
-                                    .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.JWT_ACCESS_TOKEN_TYPE)
+                                    .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.JWT_TOKEN_TYPE)
                                     .param(OAuth2Constants.SUBJECT_ISSUER, PARENT_IDP)
 
                     ));
@@ -554,7 +572,7 @@ public abstract class AbstractBrokerLinkAndTokenExchangeTest extends AbstractSer
                             new Form()
                                     .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
                                     .param(OAuth2Constants.SUBJECT_TOKEN, accessToken)
-                                    .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.JWT_ACCESS_TOKEN_TYPE)
+                                    .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.JWT_TOKEN_TYPE)
                                     .param(OAuth2Constants.SUBJECT_ISSUER, PARENT_IDP)
 
                     ));
@@ -586,6 +604,45 @@ public abstract class AbstractBrokerLinkAndTokenExchangeTest extends AbstractSer
             List<FederatedIdentityRepresentation> links = childRealm.users().get(exchangedUserId).getFederatedIdentity();
             Assert.assertEquals(1, links.size());
         }
+        {
+            // check that we can exchange without specifying an SUBJECT_ISSUER
+            Response response = exchangeUrl.request()
+                    .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(ClientApp.DEPLOYMENT_NAME, "password"))
+                    .post(Entity.form(
+                            new Form()
+                                    .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
+                                    .param(OAuth2Constants.SUBJECT_TOKEN, accessToken)
+                                    .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.JWT_TOKEN_TYPE)
+
+                    ));
+            Assert.assertEquals(200, response.getStatus());
+            AccessTokenResponse tokenResponse = response.readEntity(AccessTokenResponse.class);
+            String exchangedAccessToken = tokenResponse.getToken();
+            JWSInput jws = new JWSInput(tokenResponse.getToken());
+            AccessToken token = jws.readJsonContent(AccessToken.class);
+            response.close();
+
+            String exchanged2UserId = token.getSubject();
+            String exchanged2Username = token.getPreferredUsername();
+
+            // assert that we get the same linked account as was previously imported
+
+            Assert.assertEquals(exchangedUserId, exchanged2UserId);
+            Assert.assertEquals(exchangedUsername, exchanged2Username);
+
+            // test logout
+            response = childLogoutWebTarget(httpClient)
+                    .queryParam("id_token_hint", exchangedAccessToken)
+                    .request()
+                    .get();
+            response.close();
+
+            Assert.assertEquals(0, adminClient.realm(CHILD_IDP).getClientSessionStats().size());
+
+
+            List<FederatedIdentityRepresentation> links = childRealm.users().get(exchangedUserId).getFederatedIdentity();
+            Assert.assertEquals(1, links.size());
+        }
         // cleanup  remove the user
         childRealm.users().get(exchangedUserId).remove();
 
@@ -597,7 +654,7 @@ public abstract class AbstractBrokerLinkAndTokenExchangeTest extends AbstractSer
                             new Form()
                                     .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
                                     .param(OAuth2Constants.SUBJECT_TOKEN, accessToken)
-                                    .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.JWT_ACCESS_TOKEN_TYPE)
+                                    .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.JWT_TOKEN_TYPE)
                                     .param(OAuth2Constants.SUBJECT_ISSUER, PARENT_IDP)
 
                     ));
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SocialLoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SocialLoginTest.java
index e34dd78..66eaea2 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SocialLoginTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SocialLoginTest.java
@@ -3,20 +3,36 @@ package org.keycloak.testsuite.broker;
 import org.jboss.arquillian.graphene.Graphene;
 import org.jboss.arquillian.graphene.page.Page;
 import org.junit.After;
+import org.junit.Assert;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Ignore;
 import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.representations.AccessTokenResponse;
 import org.keycloak.representations.idm.IdentityProviderRepresentation;
 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.social.openshift.OpenshiftV3IdentityProvider;
 import org.keycloak.representations.idm.UserRepresentation;
 import org.keycloak.testsuite.AbstractKeycloakTest;
 import org.keycloak.testsuite.auth.page.login.UpdateAccount;
 import org.keycloak.testsuite.pages.LoginPage;
 import org.keycloak.testsuite.pages.social.AbstractSocialLoginPage;
+import org.keycloak.testsuite.pages.social.BitbucketLoginPage;
 import org.keycloak.testsuite.pages.social.FacebookLoginPage;
 import org.keycloak.testsuite.pages.social.GitHubLoginPage;
+import org.keycloak.testsuite.pages.social.GitLabLoginPage;
 import org.keycloak.testsuite.pages.social.GoogleLoginPage;
 import org.keycloak.testsuite.pages.social.LinkedInLoginPage;
 import org.keycloak.testsuite.pages.social.MicrosoftLoginPage;
@@ -24,12 +40,21 @@ import org.keycloak.testsuite.pages.social.PayPalLoginPage;
 import org.keycloak.testsuite.pages.social.StackOverflowLoginPage;
 import org.keycloak.testsuite.pages.social.TwitterLoginPage;
 import org.keycloak.testsuite.util.IdentityProviderBuilder;
+import org.keycloak.testsuite.util.OAuthClient;
 import org.keycloak.testsuite.util.RealmBuilder;
 import org.keycloak.testsuite.util.URLUtils;
 import org.keycloak.testsuite.util.WaitUtils;
+import org.keycloak.util.BasicAuthHelper;
 import org.openqa.selenium.By;
 import org.openqa.selenium.support.ui.ExpectedConditions;
 
+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.io.FileInputStream;
 import java.util.LinkedList;
 import java.util.List;
@@ -38,8 +63,10 @@ import java.util.Properties;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
+import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.BITBUCKET;
 import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.FACEBOOK;
 import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GITHUB;
+import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GITLAB;
 import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GOOGLE;
 import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.LINKEDIN;
 import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.MICROSOFT;
@@ -56,6 +83,7 @@ public class SocialLoginTest extends AbstractKeycloakTest {
 
     public static final String SOCIAL_CONFIG = "social.config";
     public static final String REALM = "social";
+    public static final String EXCHANGE_CLIENT = "exchange-client";
 
     private static Properties config = new Properties();
 
@@ -74,7 +102,9 @@ public class SocialLoginTest extends AbstractKeycloakTest {
         MICROSOFT("microsoft", MicrosoftLoginPage.class),
         PAYPAL("paypal", PayPalLoginPage.class),
         STACKOVERFLOW("stackoverflow", StackOverflowLoginPage.class),
-        OPENSHIFT("openshift-v3", null);
+        OPENSHIFT("openshift-v3", null),
+        GITLAB("gitlab", GitLabLoginPage.class),
+        BITBUCKET("bitbucket", BitbucketLoginPage.class);
 
         private String id;
         private Class<? extends AbstractSocialLoginPage> pageObjectClazz;
@@ -95,11 +125,15 @@ public class SocialLoginTest extends AbstractKeycloakTest {
 
     private Provider currentTestProvider;
 
+    private static final boolean localConfig = false;
+
     @BeforeClass
     public static void loadConfig() throws Exception {
-        assumeTrue(System.getProperties().containsKey(SOCIAL_CONFIG));
-
-        config.load(new FileInputStream(System.getProperty(SOCIAL_CONFIG)));
+        if (localConfig) {
+        } else {
+            assumeTrue(System.getProperties().containsKey(SOCIAL_CONFIG));
+            config.load(new FileInputStream(System.getProperty(SOCIAL_CONFIG)));
+        }
     }
     
     @Before
@@ -133,6 +167,34 @@ public class SocialLoginTest extends AbstractKeycloakTest {
         testRealms.add(rep);
     }
 
+    public static void setupClientExchangePermissions(KeycloakSession session) {
+        RealmModel realm = session.realms().getRealmByName(REALM);
+        ClientModel client = session.realms().getClientByClientId(EXCHANGE_CLIENT, realm);
+        // lazy init
+        if (client != null) return;
+        client = realm.addClient(EXCHANGE_CLIENT);
+        client.setSecret("secret");
+        client.setPublicClient(false);
+        client.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+        client.setEnabled(true);
+        client.setDirectAccessGrantsEnabled(true);
+
+        ClientPolicyRepresentation clientPolicyRep = new ClientPolicyRepresentation();
+        clientPolicyRep.setName("client-policy");
+        clientPolicyRep.addClient(client.getId());
+        AdminPermissionManagement management = AdminPermissions.management(session, realm);
+        management.users().setPermissionsEnabled(true);
+        ResourceServer server = management.realmResourceServer();
+        Policy clientPolicy = management.authz().getStoreFactory().getPolicyStore().create(clientPolicyRep, server);
+        management.users().adminImpersonatingPermission().addAssociatedPolicy(clientPolicy);
+        management.users().adminImpersonatingPermission().setDecisionStrategy(DecisionStrategy.AFFIRMATIVE);
+        for (IdentityProviderModel idp : realm.getIdentityProviders()) {
+            management.idps().setPermissionsEnabled(idp, true);
+            management.idps().exchangeToPermission(idp).addAssociatedPolicy(clientPolicy);
+        }
+
+    }
+
     @Test
     @Ignore
     // TODO: Fix and revamp this test
@@ -155,20 +217,57 @@ public class SocialLoginTest extends AbstractKeycloakTest {
         currentTestProvider = GOOGLE;
         performLogin();
         assertAccount();
+        testTokenExchange();
+    }
+
+    @Test
+    public void bitbucketLogin() throws InterruptedException {
+        currentTestProvider = BITBUCKET;
+        performLogin();
+        assertAccount();
+        testTokenExchange();
+    }
+
+    // disabled as I can't get this to work with automated login
+    //@Test
+    public void gitLabLogin() throws InterruptedException {
+        currentTestProvider = GITLAB;
+        // I can't get automated login to work.  inspected elements in browser, are not found in the GitLabLoginPage.
+        performLogin();
+        assertAccount();
+        testTokenExchange();
+    }
+
+    protected void manualLogin() throws InterruptedException {
+        System.out.println("****** START MANUAL LOGIN ******");
+        System.out.println("****** START MANUAL LOGIN ******");
+        System.out.println("****** START MANUAL LOGIN ******");
+        Thread.sleep(2000);
+        for (int i = 0; i < 60; i++) {
+            List<UserRepresentation> users = adminClient.realm(REALM).users().search(null, null, null);
+            if (users.size() > 0) return;
+            System.out.println("....waiting");
+            Thread.sleep(1000);
+        }
+
     }
 
     @Test
-    public void facebookLogin() {
+    public void facebookLogin() throws InterruptedException {
         currentTestProvider = FACEBOOK;
         performLogin();
         assertAccount();
+        testTokenExchange();
     }
 
+
     @Test
-    public void githubLogin() {
+    public void githubLogin() throws InterruptedException {
+        //Thread.sleep(100000000);
         currentTestProvider = GITHUB;
         performLogin();
         assertAccount();
+        testTokenExchange();
     }
 
     @Test
@@ -201,7 +300,7 @@ public class SocialLoginTest extends AbstractKeycloakTest {
     }
 
     @Test
-    public void stackoverflowLogin() {
+    public void stackoverflowLogin() throws InterruptedException {
         currentTestProvider = STACKOVERFLOW;
         performLogin();
         assertUpdateProfile(false, false, true);
@@ -211,6 +310,7 @@ public class SocialLoginTest extends AbstractKeycloakTest {
     private IdentityProviderRepresentation buildIdp(Provider provider) {
         IdentityProviderRepresentation idp = IdentityProviderBuilder.create().alias(provider.id()).providerId(provider.id()).build();
         idp.setEnabled(true);
+        idp.setStoreToken(true);
         idp.getConfig().put("clientId", getConfig(provider, "clientId"));
         idp.getConfig().put("clientSecret", getConfig(provider, "clientSecret"));
         if (provider == STACKOVERFLOW) {
@@ -289,4 +389,119 @@ public class SocialLoginTest extends AbstractKeycloakTest {
 
         updateAccountPage.submit();
     }
+
+    protected void testTokenExchange() {
+        testingClient.server().run(SocialLoginTest::setupClientExchangePermissions);
+
+        List<UserRepresentation> users = adminClient.realm(REALM).users().search(null, null, null);
+        Assert.assertEquals(1, users.size());
+        String username = users.get(0).getUsername();
+        Client httpClient = ClientBuilder.newClient();
+
+        WebTarget exchangeUrl = httpClient.target(OAuthClient.AUTH_SERVER_ROOT)
+                .path("/realms")
+                .path(REALM)
+                .path("protocol/openid-connect/token");
+
+        // obtain social token
+        Response response = exchangeUrl.request()
+                .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(EXCHANGE_CLIENT, "secret"))
+                .post(Entity.form(
+                        new Form()
+                                .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
+                                .param(OAuth2Constants.REQUESTED_SUBJECT, username)
+                                .param(OAuth2Constants.REQUESTED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE)
+                                .param(OAuth2Constants.REQUESTED_ISSUER, currentTestProvider.id())
+
+                ));
+        Assert.assertEquals(200, response.getStatus());
+        AccessTokenResponse tokenResponse = response.readEntity(AccessTokenResponse.class);
+        response.close();
+
+        String socialToken = tokenResponse.getToken();
+        Assert.assertNotNull(socialToken);
+
+        // remove all users
+        removeUser();
+
+        users = adminClient.realm(REALM).users().search(null, null, null);
+        Assert.assertEquals(0, users.size());
+
+        // now try external exchange where we trust social provider and import the external token.
+        response = exchangeUrl.request()
+                .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(EXCHANGE_CLIENT, "secret"))
+                .post(Entity.form(
+                        new Form()
+                                .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
+                                .param(OAuth2Constants.SUBJECT_TOKEN, socialToken)
+                                .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE)
+                                .param(OAuth2Constants.SUBJECT_ISSUER, currentTestProvider.id())
+
+                ));
+        Assert.assertEquals(200, response.getStatus());
+        tokenResponse = response.readEntity(AccessTokenResponse.class);
+        response.close();
+
+        users = adminClient.realm(REALM).users().search(null, null, null);
+        Assert.assertEquals(1, users.size());
+
+        Assert.assertEquals(username, users.get(0).getUsername());
+
+        // remove all users
+        removeUser();
+
+        users = adminClient.realm(REALM).users().search(null, null, null);
+        Assert.assertEquals(0, users.size());
+
+        ///// Test that we can update social token from session with stored tokens turned off.
+
+        // turn off store token
+        IdentityProviderRepresentation idp = adminClient.realm(REALM).identityProviders().get(currentTestProvider.id).toRepresentation();
+        idp.setStoreToken(false);
+        adminClient.realm(REALM).identityProviders().get(idp.getAlias()).update(idp);
+
+
+        // first exchange social token to get a user session that should store the social token there
+        response = exchangeUrl.request()
+                .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(EXCHANGE_CLIENT, "secret"))
+                .post(Entity.form(
+                        new Form()
+                                .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
+                                .param(OAuth2Constants.SUBJECT_TOKEN, socialToken)
+                                .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE)
+                                .param(OAuth2Constants.SUBJECT_ISSUER, currentTestProvider.id())
+
+                ));
+        Assert.assertEquals(200, response.getStatus());
+        tokenResponse = response.readEntity(AccessTokenResponse.class);
+        String keycloakToken = tokenResponse.getToken();
+        response.close();
+
+        // now take keycloak token and make sure it can get back the social token from the user session since stored tokens are off
+        response = exchangeUrl.request()
+                .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(EXCHANGE_CLIENT, "secret"))
+                .post(Entity.form(
+                        new Form()
+                                .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
+                                .param(OAuth2Constants.SUBJECT_TOKEN, keycloakToken)
+                                .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE)
+                                .param(OAuth2Constants.REQUESTED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE)
+                                .param(OAuth2Constants.REQUESTED_ISSUER, currentTestProvider.id())
+
+                ));
+        Assert.assertEquals(200, response.getStatus());
+        tokenResponse = response.readEntity(AccessTokenResponse.class);
+        response.close();
+
+        Assert.assertEquals(socialToken, tokenResponse.getToken());
+
+
+         // turn on store token
+        idp = adminClient.realm(REALM).identityProviders().get(currentTestProvider.id).toRepresentation();
+        idp.setStoreToken(true);
+        adminClient.realm(REALM).identityProviders().get(idp.getAlias()).update(idp);
+
+        httpClient.close();
+    }
+
 }