keycloak-aplcache

Details

diff --git a/common/src/main/java/org/keycloak/common/Profile.java b/common/src/main/java/org/keycloak/common/Profile.java
index 5a64c88..f068b39 100755
--- a/common/src/main/java/org/keycloak/common/Profile.java
+++ b/common/src/main/java/org/keycloak/common/Profile.java
@@ -35,11 +35,11 @@ import java.util.Set;
 public class Profile {
 
     public enum Feature {
-        AUTHORIZATION, IMPERSONATION, SCRIPTS, DOCKER, ACCOUNT2
+        AUTHORIZATION, IMPERSONATION, SCRIPTS, DOCKER, ACCOUNT2, TOKEN_EXCHANGE
     }
 
     private enum ProfileValue {
-        PRODUCT(Feature.AUTHORIZATION, Feature.SCRIPTS, Feature.DOCKER, Feature.ACCOUNT2),
+        PRODUCT(Feature.AUTHORIZATION, Feature.SCRIPTS, Feature.DOCKER, Feature.ACCOUNT2, Feature.TOKEN_EXCHANGE),
         PREVIEW(Feature.ACCOUNT2),
         COMMUNITY(Feature.DOCKER, Feature.ACCOUNT2);
 
diff --git a/core/src/main/java/org/keycloak/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java
index 244de98..098fdcd 100644
--- a/core/src/main/java/org/keycloak/OAuth2Constants.java
+++ b/core/src/main/java/org/keycloak/OAuth2Constants.java
@@ -101,9 +101,11 @@ public interface OAuth2Constants {
     String REQUESTED_TOKEN_TYPE="requested_token_type";
     String ISSUED_TOKEN_TYPE="issued_token_type";
     String REQUESTED_ISSUER="requested_issuer";
+    String SUBJECT_ISSUER="subject_issuer";
     String ACCESS_TOKEN_TYPE="urn:ietf:params:oauth:token-type:access_token";
     String REFRESH_TOKEN_TYPE="urn:ietf:params:oauth:token-type:refresh_token";
     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
new file mode 100644
index 0000000..a448d3d
--- /dev/null
+++ b/server-spi-private/src/main/java/org/keycloak/broker/provider/ExchangeExternalToken.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.broker.provider;
+
+import org.keycloak.broker.provider.BrokeredIdentityContext;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.models.UserSessionModel;
+
+import javax.ws.rs.core.MultivaluedMap;
+
+/**
+ * Exchange a token crafted by this provider for a local realm token.
+ *
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public interface ExchangeExternalToken {
+    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/events/Errors.java b/server-spi-private/src/main/java/org/keycloak/events/Errors.java
index ab954bc..632c21c 100755
--- a/server-spi-private/src/main/java/org/keycloak/events/Errors.java
+++ b/server-spi-private/src/main/java/org/keycloak/events/Errors.java
@@ -46,13 +46,16 @@ public interface Errors {
     String INVALID_REDIRECT_URI = "invalid_redirect_uri";
     String INVALID_CODE = "invalid_code";
     String INVALID_TOKEN = "invalid_token";
+    String INVALID_TOKEN_TYPE = "invalid_token_type";
     String INVALID_SAML_RESPONSE = "invalid_saml_response";
     String INVALID_SAML_AUTHN_REQUEST = "invalid_authn_request";
     String INVALID_SAML_LOGOUT_REQUEST = "invalid_logout_request";
     String INVALID_SAML_LOGOUT_RESPONSE = "invalid_logout_response";
     String INVALID_SIGNATURE = "invalid_signature";
     String INVALID_REGISTRATION = "invalid_registration";
+    String INVALID_ISSUER = "invalid_issuer";
     String INVALID_FORM = "invalid_form";
+    String INVALID_CONFIG = "invalid_config";
     String EXPIRED_CODE = "expired_code";
 
     String REGISTRATION_DISABLED = "registration_disabled";
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 4e2ea95..c3a26dc 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
@@ -149,11 +149,18 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
 
     @Override
     public Response exchangeFromToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject, MultivaluedMap<String, String> params) {
+        // check to see if we have a token exchange in session
+        // in other words check to see if this session was created by an external exchange
+        Response tokenResponse = hasExternalExchangeToken(tokenUserSession, params);
+        if (tokenResponse != null) return tokenResponse;
+
+        // going further we only support access token type?  Why?
         String requestedType = params.getFirst(OAuth2Constants.REQUESTED_TOKEN_TYPE);
         if (requestedType != null && !requestedType.equals(OAuth2Constants.ACCESS_TOKEN_TYPE)) {
             return exchangeUnsupportedRequiredType();
         }
         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);
             if (brokerId == null || !brokerId.equals(getConfig().getAlias())) {
                 return exchangeNotLinkedNoStore(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
@@ -164,6 +171,50 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
         }
     }
 
+    /**
+     * check to see if we have a token exchange in session
+     * in other words check to see if this session was created by an external exchange
+     * @param tokenUserSession
+     * @param params
+     * @return
+     */
+    protected Response hasExternalExchangeToken(UserSessionModel tokenUserSession, MultivaluedMap<String, String> params) {
+        if (getConfig().getAlias().equals(tokenUserSession.getNote(OIDCIdentityProvider.EXCHANGE_PROVIDER))) {
+
+            String requestedType = params.getFirst(OAuth2Constants.REQUESTED_TOKEN_TYPE);
+            if ((requestedType == null || requestedType.equals(OAuth2Constants.ACCESS_TOKEN_TYPE))) {
+                String accessToken = tokenUserSession.getNote(FEDERATED_ACCESS_TOKEN);
+                if (accessToken != null) {
+                    AccessTokenResponse tokenResponse = new AccessTokenResponse();
+                    tokenResponse.setToken(accessToken);
+                    tokenResponse.setIdToken(null);
+                    tokenResponse.setRefreshToken(null);
+                    tokenResponse.setRefreshExpiresIn(0);
+                    tokenResponse.setExpiresIn(0);
+                    tokenResponse.getOtherClaims().clear();
+                    tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE);
+                    return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
+                }
+            } else if (OAuth2Constants.ID_TOKEN_TYPE.equals(requestedType)) {
+                String idToken = tokenUserSession.getNote(OIDCIdentityProvider.FEDERATED_ID_TOKEN);
+                if (idToken != null) {
+                    AccessTokenResponse tokenResponse = new AccessTokenResponse();
+                    tokenResponse.setToken(null);
+                    tokenResponse.setIdToken(idToken);
+                    tokenResponse.setRefreshToken(null);
+                    tokenResponse.setRefreshExpiresIn(0);
+                    tokenResponse.setExpiresIn(0);
+                    tokenResponse.getOtherClaims().clear();
+                    tokenResponse.getOtherClaims().put(OAuth2Constants.ISSUED_TOKEN_TYPE, OAuth2Constants.ID_TOKEN_TYPE);
+                    return Response.ok(tokenResponse).type(MediaType.APPLICATION_JSON_TYPE).build();
+                }
+
+            }
+
+        }
+        return null;
+    }
+
     protected Response exchangeStoredToken(UriInfo uriInfo, ClientModel authorizedClient, UserSessionModel tokenUserSession, UserModel tokenSubject) {
         FederatedIdentityModel model = session.users().getFederatedIdentity(tokenSubject, getConfig().getAlias(), authorizedClient.getRealm());
         if (model == null || model.getToken() == null) {
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 1dbce4c..2963783 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
@@ -19,12 +19,15 @@ package org.keycloak.broker.oidc;
 import com.fasterxml.jackson.databind.JsonNode;
 import org.jboss.logging.Logger;
 import org.keycloak.OAuth2Constants;
+import org.keycloak.OAuthErrorException;
 import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
 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.util.SimpleHttp;
 import org.keycloak.common.util.Time;
+import org.keycloak.events.Details;
 import org.keycloak.events.Errors;
 import org.keycloak.events.EventBuilder;
 import org.keycloak.events.EventType;
@@ -38,11 +41,11 @@ import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.UserSessionModel;
-import org.keycloak.representations.AccessToken;
 import org.keycloak.representations.AccessTokenResponse;
 import org.keycloak.representations.IDToken;
 import org.keycloak.representations.JsonWebToken;
 import org.keycloak.services.ErrorPage;
+import org.keycloak.services.ErrorResponseException;
 import org.keycloak.services.managers.AuthenticationManager;
 import org.keycloak.services.messages.Messages;
 import org.keycloak.services.resources.IdentityBrokerService;
@@ -55,6 +58,7 @@ import javax.ws.rs.Path;
 import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriBuilder;
 import javax.ws.rs.core.UriInfo;
@@ -64,7 +68,7 @@ import java.security.PublicKey;
 /**
  * @author Pedro Igor
  */
-public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIdentityProviderConfig>  {
+public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIdentityProviderConfig> implements ExchangeExternalToken {
     protected static final Logger logger = Logger.getLogger(OIDCIdentityProvider.class);
 
     public static final String OAUTH2_PARAMETER_PROMPT = "prompt";
@@ -74,6 +78,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
     public static final String FEDERATED_ACCESS_TOKEN_RESPONSE = "FEDERATED_ACCESS_TOKEN_RESPONSE";
     public static final String VALIDATED_ID_TOKEN = "VALIDATED_ID_TOKEN";
     public static final String ACCESS_TOKEN_EXPIRATION = "accessTokenExpiration";
+    public static final String EXCHANGE_PROVIDER = "EXCHANGE_PROVIDER";
 
     public OIDCIdentityProvider(KeycloakSession session, OIDCIdentityProviderConfig config) {
         super(session, config);
@@ -96,7 +101,6 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
         }
 
 
-
         @GET
         @Path("logout_response")
         public Response logoutResponse(@Context UriInfo uriInfo,
@@ -123,7 +127,8 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
 
     @Override
     public void backchannelLogout(KeycloakSession session, UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) {
-        if (getConfig().getLogoutUrl() == null || getConfig().getLogoutUrl().trim().equals("") || !getConfig().isBackchannelSupported()) return;
+        if (getConfig().getLogoutUrl() == null || getConfig().getLogoutUrl().trim().equals("") || !getConfig().isBackchannelSupported())
+            return;
         String idToken = getIDTokenForLogout(session, userSession);
         if (idToken == null) return;
         backchannelLogout(userSession, idToken);
@@ -137,7 +142,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
         String url = logoutUri.build().toString();
         try {
             int status = SimpleHttp.doGet(url, session).asStatus();
-            boolean success = status >=200 && status < 400;
+            boolean success = status >= 200 && status < 400;
             if (!success) {
                 logger.warn("Failed backchannel broker logout to: " + url);
             }
@@ -233,7 +238,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
         try {
             String modelTokenString = model.getToken();
             AccessTokenResponse tokenResponse = JsonSerialization.readValue(modelTokenString, AccessTokenResponse.class);
-            Integer exp = (Integer)tokenResponse.getOtherClaims().get(ACCESS_TOKEN_EXPIRATION);
+            Integer exp = (Integer) tokenResponse.getOtherClaims().get(ACCESS_TOKEN_EXPIRATION);
             if (exp != null && exp < Time.currentTime()) {
                 if (tokenResponse.getRefreshToken() == null) {
                     return exchangeTokenExpired(uriInfo, authorizedClient, tokenUserSession, tokenSubject);
@@ -251,13 +256,13 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
                 }
                 AccessTokenResponse newResponse = JsonSerialization.readValue(response, AccessTokenResponse.class);
                 if (newResponse.getExpiresIn() > 0) {
-                    int accessTokenExpiration = Time.currentTime() + (int)newResponse.getExpiresIn();
+                    int accessTokenExpiration = Time.currentTime() + (int) newResponse.getExpiresIn();
                     newResponse.getOtherClaims().put(ACCESS_TOKEN_EXPIRATION, accessTokenExpiration);
                     response = JsonSerialization.writeValueAsString(newResponse);
                 }
                 String oldToken = tokenUserSession.getNote(FEDERATED_ACCESS_TOKEN);
                 if (oldToken != null && oldToken.equals(tokenResponse.getToken())) {
-                    int accessTokenExpiration = newResponse.getExpiresIn() > 0 ? Time.currentTime() + (int)newResponse.getExpiresIn() : 0;
+                    int accessTokenExpiration = newResponse.getExpiresIn() > 0 ? Time.currentTime() + (int) newResponse.getExpiresIn() : 0;
                     tokenUserSession.setNote(FEDERATED_TOKEN_EXPIRATION, Long.toString(accessTokenExpiration));
                     tokenUserSession.setNote(FEDERATED_REFRESH_TOKEN, newResponse.getRefreshToken());
                     tokenUserSession.setNote(FEDERATED_ACCESS_TOKEN, newResponse.getToken());
@@ -361,35 +366,33 @@ 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 email = (String)idToken.getOtherClaims().get(IDToken.EMAIL);
+        String name = (String) idToken.getOtherClaims().get(IDToken.NAME);
+        String preferredUsername = (String) idToken.getOtherClaims().get(getUsernameClaimName());
+        String email = (String) idToken.getOtherClaims().get(IDToken.EMAIL);
 
         if (!getConfig().isDisableUserInfoService()) {
             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();
-
-                id = getJsonProperty(userInfo, "sub");
-                name = getJsonProperty(userInfo, "name");
-                preferredUsername = getJsonProperty(userInfo, "preferred_username");
-                email = getJsonProperty(userInfo, "email");
-                AbstractJsonUserAttributeMapper.storeUserProfileForMapper(identity, userInfo, getConfig().getAlias());
+
+                if (accessToken != null) {
+                    JsonNode userInfo = SimpleHttp.doGet(userInfoUrl, session)
+                            .header("Authorization", "Bearer " + accessToken).asJson();
+
+                    id = getJsonProperty(userInfo, "sub");
+                    name = getJsonProperty(userInfo, "name");
+                    preferredUsername = getJsonProperty(userInfo, "preferred_username");
+                    email = getJsonProperty(userInfo, "email");
+                    AbstractJsonUserAttributeMapper.storeUserProfileForMapper(identity, userInfo, getConfig().getAlias());
+                }
             }
         }
-        identity.getContextData().put(FEDERATED_ACCESS_TOKEN_RESPONSE, tokenResponse);
         identity.getContextData().put(VALIDATED_ID_TOKEN, idToken);
-        processAccessTokenResponse(identity, tokenResponse);
 
         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;
@@ -400,6 +403,11 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
         }
 
         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;
     }
 
@@ -430,6 +438,12 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
     }
 
     protected JsonWebToken validateToken(String encodedToken) {
+        boolean ignoreAudience = false;
+
+        return validateToken(encodedToken, ignoreAudience);
+    }
+
+    protected JsonWebToken validateToken(String encodedToken, boolean ignoreAudience) {
         if (encodedToken == null) {
             throw new IdentityBrokerException("No token from server.");
         }
@@ -447,14 +461,14 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
 
         String iss = token.getIssuer();
 
-        if (!token.hasAudience(getConfig().getClientId())) {
-            throw new IdentityBrokerException("Wrong audience from token.");
-        }
-
         if (!token.isActive()) {
             throw new IdentityBrokerException("Token is no longer valid");
         }
 
+        if (!ignoreAudience && !token.hasAudience(getConfig().getClientId())) {
+            throw new IdentityBrokerException("Wrong audience from token.");
+        }
+
         String trustedIssuers = getConfig().getIssuer();
 
         if (trustedIssuers != null) {
@@ -468,12 +482,13 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
 
             throw new IdentityBrokerException("Wrong issuer from token. Got: " + iss + " expected: " + getConfig().getIssuer());
         }
+
         return token;
     }
 
     @Override
-    public void authenticationFinished(AuthenticationSessionModel authSession, BrokeredIdentityContext context)  {
-        AccessTokenResponse tokenResponse = (AccessTokenResponse)context.getContextData().get(FEDERATED_ACCESS_TOKEN_RESPONSE);
+    public void authenticationFinished(AuthenticationSessionModel authSession, BrokeredIdentityContext context) {
+        AccessTokenResponse tokenResponse = (AccessTokenResponse) context.getContextData().get(FEDERATED_ACCESS_TOKEN_RESPONSE);
         int currentTime = Time.currentTime();
         long expiration = tokenResponse.getExpiresIn() > 0 ? tokenResponse.getExpiresIn() + currentTime : 0;
         authSession.setUserSessionNote(FEDERATED_TOKEN_EXPIRATION, Long.toString(expiration));
@@ -486,4 +501,107 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
     protected String getDefaultScopes() {
         return "openid";
     }
+
+    protected boolean isIssuer(MultivaluedMap<String, String> params) {
+        String requestedIssuer = params.getFirst(OAuth2Constants.SUBJECT_ISSUER);
+        if (requestedIssuer == null) return true;
+        if (requestedIssuer.equals(getConfig().getAlias())) return true;
+
+        String[] issuers = getConfig().getIssuer().split(",");
+
+        for (String trustedIssuer : issuers) {
+            if (requestedIssuer.equals(trustedIssuer.trim())) {
+                return true;
+            }
+        }
+        return false;
+
+    }
+
+    @Override
+    public BrokeredIdentityContext exchangeExternal(EventBuilder event, MultivaluedMap<String, String> params) {
+        if (!isIssuer(params)) {
+            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) {
+            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);
+        }
+        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 (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);
+        }
+        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);
+                throw new ErrorResponseException(Errors.INVALID_CONFIG, "Invalid server config", Response.Status.BAD_REQUEST);
+            }
+        } else if (getConfig().getPublicKeySignatureVerifier() == null) {
+            event.detail(Details.REASON, "public key unset");
+            event.error(Errors.INVALID_CONFIG);
+            throw new ErrorResponseException(Errors.INVALID_CONFIG, "Invalid server config", Response.Status.BAD_REQUEST);
+        }
+
+        JsonWebToken parsedToken = null;
+        try {
+            parsedToken = validateToken(subjectToken, true);
+        } catch (IdentityBrokerException e) {
+            logger.debug("Unable to validate token for exchange", e);
+            event.detail(Details.REASON, "token validation failure");
+            event.error(Errors.INVALID_TOKEN);
+            throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token", Response.Status.BAD_REQUEST);
+        }
+
+        try {
+
+            BrokeredIdentityContext context = extractIdentity(null, idTokenType ? null : subjectToken, parsedToken);
+            if (context == null) {
+                logger.debug("Failed to extractIdentity() from id token.  Disabling User Info service might fix this");
+                throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token", Response.Status.BAD_REQUEST);
+
+            }
+            if (!idTokenType) {
+                context.getContextData().put(VALIDATED_ID_TOKEN, subjectToken);
+            }
+            if (jwtAccessTokenType) {
+                context.getContextData().put(KeycloakOIDCIdentityProvider.VALIDATED_ACCESS_TOKEN, parsedToken);
+            }
+            context.getContextData().put(EXCHANGE_PROVIDER, getConfig().getAlias());
+            context.setIdp(this);
+            context.setIdpConfig(getConfig());
+            return context;
+        } catch (IOException e) {
+            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());
+
+    }
 }
diff --git a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderConfig.java b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderConfig.java
index fd49f3e..ce0ccff 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderConfig.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderConfig.java
@@ -23,9 +23,10 @@ import org.keycloak.models.IdentityProviderModel;
  */
 public class OIDCIdentityProviderConfig extends OAuth2IdentityProviderConfig {
 
-    private static final String JWKS_URL = "jwksUrl";
+    public static final String JWKS_URL = "jwksUrl";
 
-    private static final String USE_JWKS_URL = "useJwksUrl";
+    public static final String USE_JWKS_URL = "useJwksUrl";
+    public static final String VALIDATE_SIGNATURE = "validateSignature";
 
 
     public OIDCIdentityProviderConfig(IdentityProviderModel identityProviderModel) {
@@ -73,7 +74,7 @@ public class OIDCIdentityProviderConfig extends OAuth2IdentityProviderConfig {
     }
 
     public void setValidateSignature(boolean validateSignature) {
-        getConfig().put("validateSignature", String.valueOf(validateSignature));
+        getConfig().put(VALIDATE_SIGNATURE, String.valueOf(validateSignature));
     }
 
     public boolean isUseJwksUrl() {
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 056932d..30e52be 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
@@ -23,9 +23,16 @@ import org.jboss.resteasy.spi.ResteasyProviderFactory;
 import org.keycloak.OAuth2Constants;
 import org.keycloak.OAuthErrorException;
 import org.keycloak.authentication.AuthenticationProcessor;
+import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
+import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
+import org.keycloak.broker.provider.BrokeredIdentityContext;
+import org.keycloak.broker.provider.ExchangeExternalToken;
 import org.keycloak.broker.provider.IdentityProvider;
 import org.keycloak.broker.provider.ExchangeTokenToIdentityProviderToken;
+import org.keycloak.broker.provider.IdentityProviderFactory;
+import org.keycloak.broker.provider.IdentityProviderMapper;
 import org.keycloak.common.ClientConnection;
+import org.keycloak.common.Profile;
 import org.keycloak.common.constants.ServiceAccountConstants;
 import org.keycloak.common.util.Base64Url;
 import org.keycloak.constants.AdapterConstants;
@@ -36,8 +43,12 @@ import org.keycloak.events.EventType;
 import org.keycloak.models.AuthenticationFlowModel;
 import org.keycloak.models.AuthenticatedClientSessionModel;
 import org.keycloak.models.ClientModel;
+import org.keycloak.models.Constants;
+import org.keycloak.models.FederatedIdentityModel;
+import org.keycloak.models.IdentityProviderMapperModel;
 import org.keycloak.models.IdentityProviderModel;
 import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.UserSessionModel;
@@ -47,20 +58,26 @@ 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.services.ErrorPage;
 import org.keycloak.services.ErrorResponseException;
 import org.keycloak.services.ServicesLogger;
 import org.keycloak.services.Urls;
 import org.keycloak.services.managers.AuthenticationManager;
 import org.keycloak.services.managers.AuthenticationSessionManager;
+import org.keycloak.services.managers.BruteForceProtector;
 import org.keycloak.services.managers.ClientManager;
 import org.keycloak.services.managers.ClientSessionCode;
 import org.keycloak.services.managers.RealmManager;
+import org.keycloak.services.messages.Messages;
 import org.keycloak.services.resources.Cors;
 import org.keycloak.services.resources.IdentityBrokerService;
+import org.keycloak.services.resources.LoginActionsService;
 import org.keycloak.services.resources.admin.AdminAuth;
 import org.keycloak.services.resources.admin.permissions.AdminPermissions;
+import org.keycloak.services.validation.Validation;
 import org.keycloak.sessions.AuthenticationSessionModel;
 import org.keycloak.util.TokenUtil;
+import org.keycloak.utils.ProfileHelper;
 
 import javax.ws.rs.OPTIONS;
 import javax.ws.rs.POST;
@@ -71,8 +88,11 @@ import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriInfo;
+import java.net.URI;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.security.MessageDigest;
@@ -567,7 +587,9 @@ public class TokenEndpoint {
     }
 
     public Response tokenExchange() {
-        event.detail(Details.AUTH_METHOD, "oauth_credentials");
+        ProfileHelper.requireFeature(Profile.Feature.TOKEN_EXCHANGE);
+
+        event.detail(Details.AUTH_METHOD, "token_exchange");
         event.client(client);
 
         UserModel tokenUser = null;
@@ -576,6 +598,14 @@ public class TokenEndpoint {
 
         String subjectToken = formParams.getFirst(OAuth2Constants.SUBJECT_TOKEN);
         if (subjectToken != null) {
+            String subjectIssuer = formParams.getFirst(OAuth2Constants.SUBJECT_ISSUER);
+            String realmIssuerUrl = Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName());
+            if (subjectIssuer != null && !realmIssuerUrl.equals(subjectIssuer)) {
+                event.detail(OAuth2Constants.SUBJECT_ISSUER, subjectIssuer);
+                return exchangeExternalToken();
+
+            }
+
             String subjectTokenType = formParams.getFirst(OAuth2Constants.SUBJECT_TOKEN_TYPE);
             if (subjectTokenType != null && !subjectTokenType.equals(OAuth2Constants.ACCESS_TOKEN_TYPE)) {
                 event.error(Errors.INVALID_TOKEN);
@@ -583,7 +613,6 @@ public class TokenEndpoint {
 
             }
 
-
             AuthenticationManager.AuthResult authResult = AuthenticationManager.verifyIdentityToken(session, realm, uriInfo, clientConnection, true, true, false, subjectToken, headers);
             if (authResult == null) {
                 event.error(Errors.INVALID_TOKEN);
@@ -688,7 +717,8 @@ public class TokenEndpoint {
         if (audience != null) {
             targetClient = realm.getClientByClientId(audience);
         }
-       if (targetClient.isConsentRequired()) {
+
+        if (targetClient.isConsentRequired()) {
             event.error(Errors.CONSENT_DENIED);
             throw new ErrorResponseException(OAuthErrorException.INVALID_CLIENT, "Client requires user consent", Response.Status.BAD_REQUEST);
         }
@@ -736,6 +766,143 @@ 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;
+
+        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;
+            }
+        }
+        if (context == 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())) {
+            logger.debug("Client not allowed to exchange for linked token");
+            event.error(Errors.NOT_ALLOWED);
+            throw new ErrorResponseException(OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN);
+        }
+
+        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);
+        return exchangeClientToClient(user, userSession);
+
+
+    }
+
+    protected UserModel importUserFromExternalIdentity(BrokeredIdentityContext context) {
+        IdentityProviderModel identityProviderConfig = context.getIdpConfig();
+
+        String providerId = identityProviderConfig.getAlias();
+
+        // do we need this?
+        //AuthenticationSessionModel authenticationSession = clientCode.getClientSession();
+        //context.setAuthenticationSession(authenticationSession);
+        //session.getContext().setClient(authenticationSession.getClient());
+
+        context.getIdp().preprocessFederatedIdentity(session, realm, context);
+        Set<IdentityProviderMapperModel> mappers = realm.getIdentityProviderMappersByAlias(context.getIdpConfig().getAlias());
+        if (mappers != null) {
+            KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
+            for (IdentityProviderMapperModel mapper : mappers) {
+                IdentityProviderMapper target = (IdentityProviderMapper)sessionFactory.getProviderFactory(IdentityProviderMapper.class, mapper.getIdentityProviderMapper());
+                target.preprocessFederatedIdentity(session, realm, mapper, context);
+            }
+        }
+
+        FederatedIdentityModel federatedIdentityModel = new FederatedIdentityModel(providerId, context.getId(),
+                context.getUsername(), context.getToken());
+
+        UserModel user = this.session.users().getUserByFederatedIdentity(federatedIdentityModel, realm);
+
+        if (user == null) {
+
+            logger.debugf("Federated user not found for provider '%s' and broker username '%s'.", providerId, context.getUsername());
+
+            String username = context.getModelUsername();
+            if (username == null) {
+                if (this.realm.isRegistrationEmailAsUsername() && !Validation.isBlank(context.getEmail())) {
+                    username = context.getEmail();
+                } else if (context.getUsername() == null) {
+                    username = context.getIdpConfig().getAlias() + "." + context.getId();
+                } else {
+                    username = context.getUsername();
+                }
+            }
+            username = username.trim();
+            context.setModelUsername(username);
+            if (context.getEmail() != null && !realm.isDuplicateEmailsAllowed()) {
+                UserModel existingUser = session.users().getUserByEmail(context.getEmail(), realm);
+                if (existingUser != null) {
+                    event.error(Errors.FEDERATED_IDENTITY_EXISTS);
+                    throw new ErrorResponseException(Errors.INVALID_TOKEN, "User already exists", Response.Status.BAD_REQUEST);
+                }
+            }
+
+            UserModel existingUser = session.users().getUserByUsername(username, realm);
+            if (existingUser != null) {
+                event.error(Errors.FEDERATED_IDENTITY_EXISTS);
+                throw new ErrorResponseException(Errors.INVALID_TOKEN, "User already exists", Response.Status.BAD_REQUEST);
+            }
+
+            // don't allow user that already exists
+            // firstBroker login
+
+            user = session.users().addUser(realm, username);
+            user.setEnabled(true);
+            user.setEmail(context.getEmail());
+            user.setFirstName(context.getFirstName());
+            user.setLastName(context.getLastName());
+
+
+            federatedIdentityModel = new FederatedIdentityModel(context.getIdpConfig().getAlias(), context.getId(),
+                    context.getUsername(), context.getToken());
+            session.users().addFederatedIdentity(realm, user, federatedIdentityModel);
+
+            context.getIdp().importNewUser(session, realm, user, context);
+            if (mappers != null) {
+                KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
+                for (IdentityProviderMapperModel mapper : mappers) {
+                    IdentityProviderMapper target = (IdentityProviderMapper)sessionFactory.getProviderFactory(IdentityProviderMapper.class, mapper.getIdentityProviderMapper());
+                    target.importNewUser(session, realm, user, mapper, context);
+                }
+            }
+
+            if (context.getIdpConfig().isTrustEmail() && !Validation.isBlank(user.getEmail())) {
+                logger.debugf("Email verified automatically after registration of user '%s' through Identity provider '%s' ", user.getUsername(), context.getIdpConfig().getAlias());
+                user.setEmailVerified(true);
+            }
+        } else {
+            if (!user.isEnabled()) {
+                event.error(Errors.USER_DISABLED);
+                throw new ErrorResponseException(Errors.INVALID_TOKEN, "Invalid Token", Response.Status.BAD_REQUEST);
+            }
+            if (realm.isBruteForceProtected()) {
+                if (session.getProvider(BruteForceProtector.class).isTemporarilyDisabled(session, realm, user)) {
+                    event.error(Errors.USER_TEMPORARILY_DISABLED);
+                    throw new ErrorResponseException(Errors.INVALID_TOKEN, "Invalid Token", Response.Status.BAD_REQUEST);
+                }
+            }
+
+            context.getIdp().updateBrokeredUser(session, realm, user, context);
+            if (mappers != null) {
+                KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
+                for (IdentityProviderMapperModel mapper : mappers) {
+                    IdentityProviderMapper target = (IdentityProviderMapper)sessionFactory.getProviderFactory(IdentityProviderMapper.class, mapper.getIdentityProviderMapper());
+                    target.updateBrokeredUser(session, realm, user, mapper, context);
+                }
+            }
+        }
+        return user;
+    }
+
 
     // https://tools.ietf.org/html/rfc7636#section-4.1
     private boolean isValidPkceCodeVerifier(String codeVerifier) {
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 2ef481e..78fadf5 100755
--- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
+++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
@@ -1143,7 +1143,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
         throw new IdentityBrokerException("Identity Provider [" + alias + "] not found.");
     }
 
-    private static IdentityProviderFactory getIdentityProviderFactory(KeycloakSession session, IdentityProviderModel model) {
+    public static IdentityProviderFactory getIdentityProviderFactory(KeycloakSession session, IdentityProviderModel model) {
         Map<String, IdentityProviderFactory> availableProviders = new HashMap<String, IdentityProviderFactory>();
         List<ProviderFactory> allProviders = new ArrayList<ProviderFactory>();