keycloak-uncached
Changes
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/social/BitbucketLoginPage.java 43(+43 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/social/GitLabLoginPage.java 46(+46 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/GreenMailRule.java 13(+12 -1)
Details
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 5d6e94a..98d4e34 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
@@ -27,6 +27,7 @@ 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;
@@ -70,7 +71,6 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
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";
@@ -167,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);
@@ -426,7 +427,7 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
return null;
}
- final protected BrokeredIdentityContext validateExternalTokenThroughUserInfo(EventBuilder event, String subjectToken, String subjectTokenType) {
+ protected BrokeredIdentityContext validateExternalTokenThroughUserInfo(EventBuilder event, String subjectToken, String subjectTokenType) {
event.detail("validation_method", "user info");
SimpleHttp.Response response = null;
int status = 0;
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 5007971..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);
@@ -392,8 +393,20 @@ 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");
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 9d1359c..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
@@ -612,7 +612,7 @@ public class TokenEndpoint {
if (subjectIssuer != null && !realmIssuerUrl.equals(subjectIssuer)) {
event.detail(OAuth2Constants.SUBJECT_ISSUER, subjectIssuer);
- return exchangeExternalToken(subjectIssuer);
+ return exchangeExternalToken(subjectIssuer, subjectToken);
}
@@ -687,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) {
@@ -781,7 +791,7 @@ public class TokenEndpoint {
return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(uriInfo, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
}
- public Response exchangeExternalToken(String issuer) {
+ public Response exchangeExternalToken(String issuer, String subjectToken) {
ExchangeExternalToken externalIdp = null;
IdentityProviderModel externalIdpModel = null;
@@ -790,7 +800,7 @@ public class TokenEndpoint {
IdentityProvider idp = factory.create(session, idpModel);
if (idp instanceof ExchangeExternalToken) {
ExchangeExternalToken external = (ExchangeExternalToken) idp;
- if (idpModel.getAlias().equals(issuer) || externalIdp.isIssuer(issuer, formParams)) {
+ if (idpModel.getAlias().equals(issuer) || external.isIssuer(issuer, formParams)) {
externalIdp = external;
externalIdpModel = idpModel;
break;
@@ -819,6 +829,11 @@ public class TokenEndpoint {
String sessionId = KeycloakModelUtils.generateId();
UserSessionModel userSession = session.sessions().createUserSession(sessionId, realm, user, user.getUsername(), clientConnection.getRemoteAddr(), "external-exchange", false, null, null);
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 4b894fa..b2a62ff 100755
--- a/services/src/main/java/org/keycloak/social/bitbucket/BitbucketIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/bitbucket/BitbucketIdentityProvider.java
@@ -33,6 +33,7 @@ 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>
@@ -42,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;
@@ -53,7 +55,7 @@ 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);
}
}
@@ -68,7 +70,31 @@ public class BitbucketIdentityProvider extends AbstractOAuth2IdentityProvider im
}
@Override
- protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, JsonNode profile) {
+ 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");
@@ -95,15 +121,46 @@ public class BitbucketIdentityProvider extends AbstractOAuth2IdentityProvider im
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;
}
@@ -131,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/gitlab/GitLabIdentityProvider.java b/services/src/main/java/org/keycloak/social/gitlab/GitLabIdentityProvider.java
index adf2e05..b781cd4 100755
--- a/services/src/main/java/org/keycloak/social/gitlab/GitLabIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/gitlab/GitLabIdentityProvider.java
@@ -24,6 +24,7 @@ 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;
@@ -93,37 +94,31 @@ public class GitLabIdentityProvider extends OIDCIdentityProvider implements Soc
return exchangeExternalUserInfoValidationOnly(event, params);
}
- 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());
- }
+ @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;
@@ -138,6 +133,51 @@ public class GitLabIdentityProvider extends OIDCIdentityProvider implements Soc
}
+ 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/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 d3b3258..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
@@ -497,6 +497,11 @@ public abstract class AbstractBrokerLinkAndTokenExchangeTest extends AbstractSer
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;
@@ -599,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();
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();
+ }
+
}