KEYCLOAK-4544 Detect existing user before granting user autolink
This commit is contained in:
parent
b1a16e4654
commit
cb12fed96e
8 changed files with 339 additions and 1 deletions
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -78,6 +78,8 @@ public class Messages {
|
|||
|
||||
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_REAUTHENTICATE_MESSAGE = "federatedIdentityConfirmReauthenticateMessage";
|
||||
|
|
|
@ -34,6 +34,7 @@ org.keycloak.authentication.authenticators.resetcred.ResetOTP
|
|||
org.keycloak.authentication.authenticators.resetcred.ResetPassword
|
||||
org.keycloak.authentication.authenticators.broker.IdpReviewProfileAuthenticatorFactory
|
||||
org.keycloak.authentication.authenticators.broker.IdpCreateUserIfUniqueAuthenticatorFactory
|
||||
org.keycloak.authentication.authenticators.broker.IdpDetectExistingBrokerUserAuthenticatorFactory
|
||||
org.keycloak.authentication.authenticators.broker.IdpConfirmLinkAuthenticatorFactory
|
||||
org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAuthenticatorFactory
|
||||
org.keycloak.authentication.authenticators.broker.IdpUsernamePasswordFormFactory
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
package org.keycloak.testsuite.pages;
|
||||
|
||||
import org.jboss.arquillian.test.api.ArquillianResource;
|
||||
import org.junit.Assert;
|
||||
import org.keycloak.testsuite.util.DroneUtils;
|
||||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
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);
|
||||
}
|
||||
|
||||
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() {
|
||||
registerLink.click();
|
||||
}
|
||||
|
|
|
@ -214,7 +214,8 @@ public class ProvidersTest extends AbstractAuthenticationTest {
|
|||
"Flow is executed only if the user attribute exists and has the expected value");
|
||||
addProviderInfo(result, "set-attribute", "Set 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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 + "\""));
|
||||
}
|
||||
}
|
|
@ -205,6 +205,7 @@ usernameExistsMessage=Username already exists.
|
|||
emailExistsMessage=Email already exists.
|
||||
|
||||
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
|
||||
federatedIdentityConfirmLinkMessage=User with {0} {1} already exists. How do you want to continue?
|
||||
|
|
Loading…
Reference in a new issue