KEYCLOAK-4544 Detect existing user before granting user autolink

This commit is contained in:
diodfr 2021-01-27 18:51:20 +01:00 committed by Marek Posolda
parent b1a16e4654
commit cb12fed96e
8 changed files with 339 additions and 1 deletions

View file

@ -0,0 +1,92 @@
/*
* 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.authentication.authenticators.broker;
import org.jboss.logging.Logger;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.authenticators.broker.util.ExistingUserInfo;
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.messages.Messages;
import javax.ws.rs.core.Response;
public class IdpDetectExistingBrokerUserAuthenticator extends IdpCreateUserIfUniqueAuthenticator {
private static final Logger logger = Logger.getLogger(IdpDetectExistingBrokerUserAuthenticator.class);
@Override
protected void authenticateImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {
RealmModel realm = context.getRealm();
if (context.getAuthenticationSession().getAuthNote(EXISTING_USER_INFO) != null) {
context.attempted();
return;
}
String username = getUsername(context, serializedCtx, brokerContext);
if (username == null) {
ServicesLogger.LOGGER.resetFlow(realm.isRegistrationEmailAsUsername() ? "Email" : "Username");
context.getAuthenticationSession().setAuthNote(ENFORCE_UPDATE_PROFILE, "true");
context.resetFlow();
return;
}
ExistingUserInfo duplication = checkExistingUser(context, username, serializedCtx, brokerContext);
if (duplication == null) {
logger.errorf("The user %s should be already registered in the realm to login %s",username, realm.getName());
Response challengeResponse = context.form()
.setError(Messages.FEDERATED_IDENTITY_UNAVAILABLE, username, brokerContext.getIdpConfig().getAlias())
.createErrorPage(Response.Status.UNAUTHORIZED);
context.challenge(challengeResponse);
context.getEvent()
.detail("authenticator", "DetectExistingBrokerUser")
.removeDetail(Details.AUTH_METHOD)
.removeDetail(Details.AUTH_TYPE)
.error(Errors.USER_NOT_FOUND);
} else {
logger.debugf("Duplication detected. There is already existing user with %s '%s' .",
duplication.getDuplicateAttributeName(), duplication.getDuplicateAttributeValue());
// Set duplicated user, so next authenticators can deal with it
context.getAuthenticationSession().setAuthNote(EXISTING_USER_INFO, duplication.serialize());
context.success();
}
}
@Override
public boolean requiresUser() {
return false;
}
@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return true;
}
}

View file

@ -0,0 +1,99 @@
/*
* 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.authentication.authenticators.broker;
import org.keycloak.Config;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class IdpDetectExistingBrokerUserAuthenticatorFactory implements AuthenticatorFactory {
public static final String PROVIDER_ID = "idp-detect-existing-broker-user";
private static final IdpDetectExistingBrokerUserAuthenticator SINGLETON = new IdpDetectExistingBrokerUserAuthenticator();
@Override
public Authenticator create(KeycloakSession session) {
return SINGLETON;
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public String getReferenceCategory() {
return "detectExistingBrokerUser";
}
@Override
public boolean isConfigurable() {
return false;
}
@Override
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
return new AuthenticationExecutionModel.Requirement[] {
AuthenticationExecutionModel.Requirement.REQUIRED,
AuthenticationExecutionModel.Requirement.DISABLED
};
}
@Override
public String getDisplayType() {
return "Detect existing broker user";
}
@Override
public String getHelpText() {
return "Detect if there is an existing Keycloak account with same email like identity provider. If no, throw an error.";
}
@Override
public boolean isUserSetupAllowed() {
return false;
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return Collections.emptyList();
}
}

View file

@ -78,6 +78,8 @@ public class Messages {
public static final String FEDERATED_IDENTITY_EXISTS = "federatedIdentityExistsMessage"; public static final String FEDERATED_IDENTITY_EXISTS = "federatedIdentityExistsMessage";
public static final String FEDERATED_IDENTITY_UNAVAILABLE = "federatedIdentityUnavailableMessage";
public static final String FEDERATED_IDENTITY_CONFIRM_LINK_MESSAGE = "federatedIdentityConfirmLinkMessage"; public static final String FEDERATED_IDENTITY_CONFIRM_LINK_MESSAGE = "federatedIdentityConfirmLinkMessage";
public static final String FEDERATED_IDENTITY_CONFIRM_REAUTHENTICATE_MESSAGE = "federatedIdentityConfirmReauthenticateMessage"; public static final String FEDERATED_IDENTITY_CONFIRM_REAUTHENTICATE_MESSAGE = "federatedIdentityConfirmReauthenticateMessage";

View file

@ -34,6 +34,7 @@ org.keycloak.authentication.authenticators.resetcred.ResetOTP
org.keycloak.authentication.authenticators.resetcred.ResetPassword org.keycloak.authentication.authenticators.resetcred.ResetPassword
org.keycloak.authentication.authenticators.broker.IdpReviewProfileAuthenticatorFactory org.keycloak.authentication.authenticators.broker.IdpReviewProfileAuthenticatorFactory
org.keycloak.authentication.authenticators.broker.IdpCreateUserIfUniqueAuthenticatorFactory org.keycloak.authentication.authenticators.broker.IdpCreateUserIfUniqueAuthenticatorFactory
org.keycloak.authentication.authenticators.broker.IdpDetectExistingBrokerUserAuthenticatorFactory
org.keycloak.authentication.authenticators.broker.IdpConfirmLinkAuthenticatorFactory org.keycloak.authentication.authenticators.broker.IdpConfirmLinkAuthenticatorFactory
org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAuthenticatorFactory org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAuthenticatorFactory
org.keycloak.authentication.authenticators.broker.IdpUsernamePasswordFormFactory org.keycloak.authentication.authenticators.broker.IdpUsernamePasswordFormFactory

View file

@ -18,6 +18,7 @@
package org.keycloak.testsuite.pages; package org.keycloak.testsuite.pages;
import org.jboss.arquillian.test.api.ArquillianResource; import org.jboss.arquillian.test.api.ArquillianResource;
import org.junit.Assert;
import org.keycloak.testsuite.util.DroneUtils; import org.keycloak.testsuite.util.DroneUtils;
import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.OAuthClient;
import org.openqa.selenium.By; import org.openqa.selenium.By;
@ -166,6 +167,12 @@ public class LoginPage extends LanguageComboboxAwarePage {
return DroneUtils.getCurrentDriver().getTitle().equals("Sign in to " + realm) || DroneUtils.getCurrentDriver().getTitle().equals("Anmeldung bei " + realm); return DroneUtils.getCurrentDriver().getTitle().equals("Sign in to " + realm) || DroneUtils.getCurrentDriver().getTitle().equals("Anmeldung bei " + realm);
} }
public void assertCurrent(String realm) {
String name = getClass().getSimpleName();
Assert.assertTrue("Expected " + name + " but was " + DroneUtils.getCurrentDriver().getTitle() + " (" + DroneUtils.getCurrentDriver().getCurrentUrl() + ")",
isCurrent(realm));
}
public void clickRegister() { public void clickRegister() {
registerLink.click(); registerLink.click();
} }

View file

@ -214,7 +214,8 @@ public class ProvidersTest extends AbstractAuthenticationTest {
"Flow is executed only if the user attribute exists and has the expected value"); "Flow is executed only if the user attribute exists and has the expected value");
addProviderInfo(result, "set-attribute", "Set user attribute", addProviderInfo(result, "set-attribute", "Set user attribute",
"Set a user attribute"); "Set a user attribute");
addProviderInfo(result, "idp-detect-existing-broker-user", "Detect existing broker user",
"Detect if there is an existing Keycloak account with same email like identity provider. If no, throw an error.");
return result; return result;
} }

View file

@ -0,0 +1,135 @@
package org.keycloak.testsuite.broker;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.admin.client.resource.AuthenticationManagementResource;
import org.keycloak.admin.client.resource.IdentityProviderResource;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.authentication.authenticators.broker.IdpAutoLinkAuthenticatorFactory;
import org.keycloak.authentication.authenticators.broker.IdpDetectExistingBrokerUserAuthenticatorFactory;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.IdentityProviderSyncMode;
import org.keycloak.representations.idm.AuthenticationExecutionRepresentation;
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.testsuite.pages.LoginUpdateProfilePage;
import org.keycloak.testsuite.util.ExecutionBuilder;
import static org.junit.Assert.*;
import static org.keycloak.testsuite.broker.BrokerTestTools.getConsumerRoot;
public class KcOidcFirstBrokerLoginDetectExistingUserTest extends AbstractInitializedBaseBrokerTest {
@Page
protected LoginUpdateProfilePage loginUpdateProfilePage;
@Override
protected BrokerConfiguration getBrokerConfiguration() {
return new KcOidcBrokerConfiguration();
}
@Override
@Before
public void beforeBrokerTest() {
super.beforeBrokerTest();
log.debug("creating detect existing user flow for realm " + bc.providerRealmName());
final RealmResource consumerRealm = adminClient.realm(bc.consumerRealmName());
AuthenticationManagementResource authMgmtResource = consumerRealm.flows();
// Creates detectExistingUserFlow
String detectExistingFlowAlias = "detectExistingUserFlow";
final AuthenticationFlowRepresentation authenticationFlowRepresentation = newFlow(detectExistingFlowAlias, detectExistingFlowAlias, "basic-flow", true, false);
authMgmtResource.createFlow(authenticationFlowRepresentation);
AuthenticationFlowRepresentation authenticationFlowRepresentation1 = getFlow(authMgmtResource, detectExistingFlowAlias);
assertNotNull("The authentication flow must exist", authenticationFlowRepresentation1);
String flowId = authenticationFlowRepresentation1.getId(); // retrieves the id of the newly created flow
// Adds executions to the flow
addExecution(authMgmtResource, flowId, IdpDetectExistingBrokerUserAuthenticatorFactory.PROVIDER_ID, 10);
addExecution(authMgmtResource, flowId, IdpAutoLinkAuthenticatorFactory.PROVIDER_ID, 20);
// Updates the FirstBrokerLoginFlowAlias for the identity provider
IdentityProviderResource identityConsumerResource = consumerRealm.identityProviders().get(bc.getIDPAlias());
IdentityProviderRepresentation identityProviderRepresentation = consumerRealm.identityProviders().findAll().get(0);
identityProviderRepresentation.setFirstBrokerLoginFlowAlias(detectExistingFlowAlias);
identityProviderRepresentation.getConfig().put(IdentityProviderModel.SYNC_MODE, IdentityProviderSyncMode.FORCE.toString());
identityConsumerResource.update(identityProviderRepresentation);
assertEquals("Two executions must have been created", 2, getFlow(authMgmtResource, detectExistingFlowAlias).getAuthenticationExecutions().size());
}
private void addExecution(AuthenticationManagementResource authMgmtResource, String flowId, String providerId, int priority) {
AuthenticationExecutionRepresentation exec = ExecutionBuilder.create()
.parentFlow(flowId)
.requirement(AuthenticationExecutionModel.Requirement.REQUIRED.toString())
.authenticator(providerId)
.priority(priority)
.authenticatorFlow(false)
.build();
authMgmtResource.addExecution(exec);
}
private AuthenticationFlowRepresentation getFlow(AuthenticationManagementResource authMgmtResource, String detectExistingFlowAlias) {
return authMgmtResource.getFlows().stream()
.filter(v -> detectExistingFlowAlias.equals(v.getAlias()))
.findFirst().get();
}
private AuthenticationFlowRepresentation newFlow(String alias, String description,
String providerId, boolean topLevel, boolean builtIn) {
AuthenticationFlowRepresentation flow = new AuthenticationFlowRepresentation();
flow.setAlias(alias);
flow.setDescription(description);
flow.setProviderId(providerId);
flow.setTopLevel(topLevel);
flow.setBuiltIn(builtIn);
return flow;
}
@Test
public void loginWhenUserDoesNotExistOnConsumer() {
updateExecutions(AbstractBrokerTest::disableUpdateProfileOnFirstLogin);
String firstname = "Firstname";
String lastname = "Lastname";
String username = "firstandlastname";
createUser(bc.providerRealmName(), username, BrokerTestConstants.USER_PASSWORD, firstname, lastname, "firstnamelastname@example.org");
driver.navigate().to(getAccountUrl(getConsumerRoot(), bc.consumerRealmName()));
logInWithIdp(bc.getIDPAlias(), username, BrokerTestConstants.USER_PASSWORD);
loginPage.assertCurrent(bc.consumerRealmName());
assertEquals("User " + username + " authenticated with identity provider " + bc.getIDPAlias() + " does not exists. Please contact your administrator.", loginPage.getInstruction());
}
@Test
public void loginWhenUserExistsOnConsumer() {
updateExecutions(AbstractBrokerTest::disableUpdateProfileOnFirstLogin);
final String firstname = "Firstname(loginWhenUserExistsOnConsumer)";
final String lastname = "Lastname(loginWhenUserExistsOnConsumer)";
final String username = "firstandlastname";
final String email = "firstnamelastname@example.org";
createUser(bc.providerRealmName(), username, BrokerTestConstants.USER_PASSWORD, firstname, lastname, email);
createUser(bc.consumerRealmName(), username, "THIS PASSWORD IS USELESS", null, null, email);
String accountUrl = getAccountUrl(getConsumerRoot(), bc.consumerRealmName());
getLogger().error("> LOG INTO " + accountUrl);
driver.navigate().to(accountUrl);
logInWithIdp(bc.getIDPAlias(), username, BrokerTestConstants.USER_PASSWORD);
assertTrue(driver.getTitle().contains("Account Management"));
assertTrue("email must be in the page", driver.getPageSource().contains("value=\""+ email + "\""));
assertTrue("firstname must appear in the page", driver.getPageSource().contains("value=\""+ firstname + "\""));
assertTrue("lastname must appear in the page", driver.getPageSource().contains("value=\""+ lastname + "\""));
}
}

View file

@ -205,6 +205,7 @@ usernameExistsMessage=Username already exists.
emailExistsMessage=Email already exists. emailExistsMessage=Email already exists.
federatedIdentityExistsMessage=User with {0} {1} already exists. Please login to account management to link the account. federatedIdentityExistsMessage=User with {0} {1} already exists. Please login to account management to link the account.
federatedIdentityUnavailableMessage=User {0} authenticated with identity provider {1} does not exists. Please contact your administrator.
confirmLinkIdpTitle=Account already exists confirmLinkIdpTitle=Account already exists
federatedIdentityConfirmLinkMessage=User with {0} {1} already exists. How do you want to continue? federatedIdentityConfirmLinkMessage=User with {0} {1} already exists. How do you want to continue?