KEYCLOAK-14023 Instagram User Endpoint change
Co-authored-by: Jean-Baptiste PIN <jibet.pin@gmail.com>
This commit is contained in:
parent
1db1deb066
commit
7087c081f0
11 changed files with 331 additions and 21 deletions
|
@ -34,6 +34,7 @@ import java.util.Map;
|
|||
public class BrokeredIdentityContext {
|
||||
|
||||
private String id;
|
||||
private String legacyId;
|
||||
private String username;
|
||||
private String modelUsername;
|
||||
private String email;
|
||||
|
@ -64,6 +65,19 @@ public class BrokeredIdentityContext {
|
|||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* ID from older API version. For API migrations.
|
||||
*
|
||||
* @return legacy ID
|
||||
*/
|
||||
public String getLegacyId() {
|
||||
return legacyId;
|
||||
}
|
||||
|
||||
public void setLegacyId(String legacyId) {
|
||||
this.legacyId = legacyId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Username in remote idp
|
||||
*
|
||||
|
|
|
@ -38,6 +38,13 @@ public class FederatedIdentityModel {
|
|||
this.token = token;
|
||||
}
|
||||
|
||||
public FederatedIdentityModel(FederatedIdentityModel originalIdentity, String userId) {
|
||||
identityProvider = originalIdentity.getIdentityProvider();
|
||||
this.userId = userId;
|
||||
userName = originalIdentity.getUserName();
|
||||
token = originalIdentity.getToken();
|
||||
}
|
||||
|
||||
public String getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
|
|
@ -546,6 +546,13 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
|||
.detail(Details.IDENTITY_PROVIDER_USERNAME, context.getUsername());
|
||||
|
||||
UserModel federatedUser = this.session.users().getUserByFederatedIdentity(federatedIdentityModel, this.realmModel);
|
||||
boolean shouldMigrateId = false;
|
||||
// try to find the user using legacy ID
|
||||
if (federatedUser == null && context.getLegacyId() != null) {
|
||||
federatedIdentityModel = new FederatedIdentityModel(federatedIdentityModel, context.getLegacyId());
|
||||
federatedUser = this.session.users().getUserByFederatedIdentity(federatedIdentityModel, this.realmModel);
|
||||
shouldMigrateId = true;
|
||||
}
|
||||
|
||||
// Check if federatedUser is already authenticated (this means linking social into existing federatedUser account)
|
||||
UserSessionModel userSession = new AuthenticationSessionManager(session).getUserSession(authenticationSession);
|
||||
|
@ -608,6 +615,9 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
|||
}
|
||||
|
||||
updateFederatedIdentity(context, federatedUser);
|
||||
if (shouldMigrateId) {
|
||||
migrateFederatedIdentityId(context, federatedUser);
|
||||
}
|
||||
authenticationSession.setAuthenticatedUser(federatedUser);
|
||||
|
||||
return finishOrRedirectToPostBrokerLogin(authenticationSession, context, false, parsedCode.clientSessionCode);
|
||||
|
@ -1006,6 +1016,16 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
|||
|
||||
}
|
||||
|
||||
private void migrateFederatedIdentityId(BrokeredIdentityContext context, UserModel federatedUser) {
|
||||
FederatedIdentityModel identityModel = this.session.users().getFederatedIdentity(federatedUser, context.getIdpConfig().getAlias(), this.realmModel);
|
||||
FederatedIdentityModel migratedIdentityModel = new FederatedIdentityModel(identityModel, context.getId());
|
||||
|
||||
// since ID is a partial key we need to recreate the identity
|
||||
session.users().removeFederatedIdentity(realmModel, federatedUser, identityModel.getIdentityProvider());
|
||||
session.users().addFederatedIdentity(realmModel, federatedUser, migratedIdentityModel);
|
||||
logger.debugf("Federated user ID was migrated from %s to %s", identityModel.getUserId(), migratedIdentityModel.getUserId());
|
||||
}
|
||||
|
||||
private void updateToken(BrokeredIdentityContext context, UserModel federatedUser, FederatedIdentityModel federatedIdentityModel) {
|
||||
if (context.getIdpConfig().isStoreToken() && !ObjectUtil.isEqualOrBothNull(context.getToken(), federatedIdentityModel.getToken())) {
|
||||
federatedIdentityModel.setToken(context.getToken());
|
||||
|
|
|
@ -27,6 +27,8 @@ import org.keycloak.broker.provider.IdentityBrokerException;
|
|||
import org.keycloak.broker.social.SocialIdentityProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
|
@ -34,9 +36,11 @@ public class InstagramIdentityProvider extends AbstractOAuth2IdentityProvider im
|
|||
|
||||
public static final String AUTH_URL = "https://api.instagram.com/oauth/authorize";
|
||||
public static final String TOKEN_URL = "https://api.instagram.com/oauth/access_token";
|
||||
public static final String PROFILE_URL = "https://api.instagram.com/v1/users/self";
|
||||
public static final String DEFAULT_SCOPE = "basic";
|
||||
|
||||
public static final String PROFILE_URL = "https://graph.instagram.com/me";
|
||||
public static final String PROFILE_FIELDS = "id,username";
|
||||
public static final String DEFAULT_SCOPE = "user_profile";
|
||||
public static final String LEGACY_ID_FIELD = "ig_id";
|
||||
|
||||
public InstagramIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) {
|
||||
super(session, config);
|
||||
config.setAuthorizationUrl(AUTH_URL);
|
||||
|
@ -46,25 +50,29 @@ public class InstagramIdentityProvider extends AbstractOAuth2IdentityProvider im
|
|||
|
||||
protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
|
||||
try {
|
||||
JsonNode raw = SimpleHttp.doGet(PROFILE_URL,session).param("access_token", accessToken).asJson();
|
||||
|
||||
JsonNode profile = raw.get("data");
|
||||
// try to get the profile incl. legacy Instagram ID to allow existing users to log in
|
||||
JsonNode profile = fetchUserProfile(accessToken, true);
|
||||
// ig_id field will get deprecated in the future and eventually might stop working (returning error)
|
||||
if (!profile.has("id")) {
|
||||
logger.debugf("Could not fetch user profile from instagram. Trying without %s.", LEGACY_ID_FIELD);
|
||||
profile = fetchUserProfile(accessToken, false);
|
||||
}
|
||||
|
||||
logger.debug(profile.toString());
|
||||
|
||||
String id = getJsonProperty(profile, "id");
|
||||
// it's not documented whether the new ID system can or cannot have conflicts with the legacy system, therefore
|
||||
// we're using a custom prefix just to be sure
|
||||
String id = "graph_" + getJsonProperty(profile, "id");
|
||||
String username = getJsonProperty(profile, "username");
|
||||
String legacyId = getJsonProperty(profile, LEGACY_ID_FIELD);
|
||||
|
||||
BrokeredIdentityContext user = new BrokeredIdentityContext(id);
|
||||
|
||||
String username = getJsonProperty(profile, "username");
|
||||
|
||||
user.setUsername(username);
|
||||
|
||||
String full_name = getJsonProperty(profile, "full_name");
|
||||
|
||||
user.setName(full_name);
|
||||
user.setIdpConfig(getConfig());
|
||||
user.setIdp(this);
|
||||
if (legacyId != null && !legacyId.isEmpty()) {
|
||||
user.setLegacyId(legacyId);
|
||||
}
|
||||
|
||||
AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
|
||||
|
||||
|
@ -74,6 +82,18 @@ public class InstagramIdentityProvider extends AbstractOAuth2IdentityProvider im
|
|||
}
|
||||
}
|
||||
|
||||
protected JsonNode fetchUserProfile(String accessToken, boolean includeIgId) throws IOException {
|
||||
String fields = PROFILE_FIELDS;
|
||||
if (includeIgId) {
|
||||
fields += "," + LEGACY_ID_FIELD;
|
||||
}
|
||||
|
||||
return SimpleHttp.doGet(PROFILE_URL,session)
|
||||
.param("access_token", accessToken)
|
||||
.param("fields", fields)
|
||||
.asJson();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getDefaultScopes() {
|
||||
return DEFAULT_SCOPE;
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright 2020 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.broker.oidc;
|
||||
|
||||
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProvider;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
/**
|
||||
* @author Vaclav Muzikar <vmuzikar@redhat.com>
|
||||
*/
|
||||
public class LegacyIdIdentityProvider extends KeycloakOIDCIdentityProvider {
|
||||
public static final String LEGACY_ID = "3.14159265359";
|
||||
|
||||
public LegacyIdIdentityProvider(KeycloakSession session, OIDCIdentityProviderConfig config) {
|
||||
super(session, config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BrokeredIdentityContext getFederatedIdentity(String response) {
|
||||
BrokeredIdentityContext user = super.getFederatedIdentity(response);
|
||||
user.setLegacyId(LEGACY_ID);
|
||||
return user;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright 2020 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.broker.oidc;
|
||||
|
||||
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProvider;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
/**
|
||||
* @author Vaclav Muzikar <vmuzikar@redhat.com>
|
||||
*/
|
||||
public class LegacyIdIdentityProviderFactory extends OIDCIdentityProviderFactory {
|
||||
|
||||
public static final String PROVIDER_ID = "legacy-id-idp";
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeycloakOIDCIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
|
||||
return new LegacyIdIdentityProvider(session, new OIDCIdentityProviderConfig(model));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
#
|
||||
# Copyright 2020 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.
|
||||
#
|
||||
|
||||
org.keycloak.testsuite.broker.oidc.LegacyIdIdentityProviderFactory
|
|
@ -18,9 +18,12 @@
|
|||
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;
|
||||
|
||||
import static org.keycloak.testsuite.util.WaitUtils.pause;
|
||||
|
||||
/**
|
||||
* @author Vaclav Muzikar <vmuzikar@redhat.com>
|
||||
*/
|
||||
|
@ -31,11 +34,42 @@ public class InstagramLoginPage extends AbstractSocialLoginPage {
|
|||
@FindBy(name = "password")
|
||||
private WebElement passwordInput;
|
||||
|
||||
@FindBy(xpath = "//button[text()='Save Info']")
|
||||
private WebElement saveInfoBtn;
|
||||
|
||||
@FindBy(xpath = "//button[text()='Authorize']")
|
||||
private WebElement authorizeBtn;
|
||||
|
||||
@FindBy(xpath = "//button[text()='Continue']")
|
||||
private WebElement continueBtn;
|
||||
|
||||
@Override
|
||||
public void login(String user, String password) {
|
||||
usernameInput.clear();
|
||||
usernameInput.sendKeys(user);
|
||||
passwordInput.sendKeys(password);
|
||||
passwordInput.sendKeys(Keys.RETURN);
|
||||
try {
|
||||
usernameInput.clear();
|
||||
usernameInput.sendKeys(user);
|
||||
passwordInput.sendKeys(password);
|
||||
passwordInput.sendKeys(Keys.RETURN);
|
||||
pause(2000); // wait for the login screen a bit
|
||||
|
||||
try {
|
||||
saveInfoBtn.click();
|
||||
}
|
||||
catch (NoSuchElementException e) {
|
||||
log.info("'Save Info' button not found, ignoring");
|
||||
pause(2000); // wait for the login screen a bit
|
||||
}
|
||||
}
|
||||
catch (NoSuchElementException e) {
|
||||
log.info("Instagram is already logged in, just confirmation is expected");
|
||||
}
|
||||
|
||||
try {
|
||||
continueBtn.click();
|
||||
}
|
||||
catch (NoSuchElementException e) {
|
||||
log.info("'Continue' button not found, trying 'Authorize'...");
|
||||
authorizeBtn.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -316,10 +316,14 @@ public abstract class AbstractBaseBrokerTest extends AbstractKeycloakTest {
|
|||
|
||||
|
||||
protected void assertLoggedInAccountManagement() {
|
||||
assertLoggedInAccountManagement(bc.getUserLogin(), bc.getUserEmail());
|
||||
}
|
||||
|
||||
protected void assertLoggedInAccountManagement(String username, String email) {
|
||||
waitForAccountManagementTitle();
|
||||
Assert.assertTrue(accountUpdateProfilePage.isCurrent());
|
||||
Assert.assertEquals(accountUpdateProfilePage.getUsername(), bc.getUserLogin());
|
||||
Assert.assertEquals(accountUpdateProfilePage.getEmail(), bc.getUserEmail());
|
||||
Assert.assertEquals(accountUpdateProfilePage.getUsername(), username);
|
||||
Assert.assertEquals(accountUpdateProfilePage.getEmail(), email);
|
||||
}
|
||||
|
||||
protected void waitForAccountManagementTitle() {
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* Copyright 2020 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.broker;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.models.IdentityProviderSyncMode;
|
||||
import org.keycloak.representations.idm.FederatedIdentityRepresentation;
|
||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.broker.oidc.LegacyIdIdentityProviderFactory;
|
||||
import org.keycloak.testsuite.util.FederatedIdentityBuilder;
|
||||
import org.keycloak.testsuite.util.UserBuilder;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.keycloak.testsuite.admin.ApiUtil.createUserWithAdminClient;
|
||||
import static org.keycloak.testsuite.broker.BrokerTestConstants.IDP_OIDC_ALIAS;
|
||||
import static org.keycloak.testsuite.broker.BrokerTestTools.getConsumerRoot;
|
||||
import static org.keycloak.testsuite.broker.BrokerTestTools.getProviderRoot;
|
||||
import static org.keycloak.testsuite.broker.oidc.LegacyIdIdentityProvider.LEGACY_ID;
|
||||
|
||||
/**
|
||||
* @author Vaclav Muzikar <vmuzikar@redhat.com>
|
||||
*/
|
||||
public class BrokerWithLegacyIdTest extends AbstractInitializedBaseBrokerTest {
|
||||
private static final UserRepresentation consumerUser = UserBuilder.create()
|
||||
.username("anakin")
|
||||
.firstName("Darth")
|
||||
.lastName("Vader")
|
||||
.email("anakin@skywalker.tatooine")
|
||||
.password("Come to the Dark Side. We have cookies")
|
||||
.build();
|
||||
private UserResource consumerUserResource;
|
||||
|
||||
@Override
|
||||
protected BrokerConfiguration getBrokerConfiguration() {
|
||||
return new KcOidcBrokerConfiguration() {
|
||||
@Override
|
||||
public IdentityProviderRepresentation setUpIdentityProvider(IdentityProviderSyncMode syncMode) {
|
||||
IdentityProviderRepresentation idp = super.setUpIdentityProvider(syncMode);
|
||||
idp.setProviderId(LegacyIdIdentityProviderFactory.PROVIDER_ID);
|
||||
return idp;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeBrokerTest() {
|
||||
super.beforeBrokerTest();
|
||||
RealmResource consumerRealm = realmsResouce().realm(bc.consumerRealmName());
|
||||
|
||||
String consumerUserId = createUserWithAdminClient(consumerRealm, consumerUser);
|
||||
|
||||
FederatedIdentityRepresentation identity = FederatedIdentityBuilder.create()
|
||||
.userId(LEGACY_ID)
|
||||
.userName(bc.getUserLogin())
|
||||
.identityProvider(IDP_OIDC_ALIAS)
|
||||
.build();
|
||||
|
||||
consumerUserResource = consumerRealm.users().get(consumerUserId);
|
||||
consumerUserResource.addFederatedIdentity(IDP_OIDC_ALIAS, identity);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loginWithLegacyId() {
|
||||
assertEquals(LEGACY_ID, getFederatedIdentity().getUserId());
|
||||
// login as existing user with legacy ID (from e.g. a deprecated API)
|
||||
logInAsUserInIDP();
|
||||
// id should be migrated to new one
|
||||
assertEquals(userId, getFederatedIdentity().getUserId());
|
||||
assertLoggedInAccountManagement(consumerUser.getUsername(), consumerUser.getEmail());
|
||||
|
||||
logoutFromRealm(getProviderRoot(), bc.providerRealmName());
|
||||
logoutFromRealm(getConsumerRoot(), bc.consumerRealmName());
|
||||
|
||||
// try to login again to double check the new ID works
|
||||
logInAsUserInIDP();
|
||||
assertEquals(userId, getFederatedIdentity().getUserId());
|
||||
assertLoggedInAccountManagement(consumerUser.getUsername(), consumerUser.getEmail());
|
||||
}
|
||||
|
||||
private FederatedIdentityRepresentation getFederatedIdentity() {
|
||||
List<FederatedIdentityRepresentation> identities = consumerUserResource.getFederatedIdentity();
|
||||
assertEquals(1, identities.size());
|
||||
return identities.get(0);
|
||||
}
|
||||
}
|
|
@ -333,7 +333,7 @@ public class SocialLoginTest extends AbstractKeycloakTest {
|
|||
public void instagramLogin() throws InterruptedException {
|
||||
setTestProvider(INSTAGRAM);
|
||||
performLogin();
|
||||
assertUpdateProfile(false, false, true);
|
||||
assertUpdateProfile(true, true, true);
|
||||
assertAccount();
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue