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_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";
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.
|
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?
|
||||||
|
|
Loading…
Reference in a new issue