KEYCLOAK-592 Display login form with error message if trying to login with social provider where email already exists
This commit is contained in:
parent
94be235a0f
commit
f95418dfc9
5 changed files with 117 additions and 61 deletions
|
@ -51,6 +51,8 @@ successTotpRemoved=Google authenticator removed.
|
||||||
|
|
||||||
usernameExists=Username already exists
|
usernameExists=Username already exists
|
||||||
|
|
||||||
|
socialEmailExists=User with email already exists. Please login to account management to link the account.
|
||||||
|
|
||||||
loginTitle=Log in to
|
loginTitle=Log in to
|
||||||
loginOauthTitle=Temporary access.
|
loginOauthTitle=Temporary access.
|
||||||
loginOauthTitleHtml=Temporary access requested. Login to grant access.
|
loginOauthTitleHtml=Temporary access requested. Login to grant access.
|
||||||
|
|
|
@ -64,7 +64,7 @@ public class DefaultKeycloakTransactionManager implements KeycloakTransactionMan
|
||||||
exception = exception == null ? e : exception;
|
exception = exception == null ? e : exception;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
active = false;
|
||||||
if (exception != null) {
|
if (exception != null) {
|
||||||
throw exception;
|
throw exception;
|
||||||
}
|
}
|
||||||
|
@ -87,6 +87,7 @@ public class DefaultKeycloakTransactionManager implements KeycloakTransactionMan
|
||||||
exception = exception != null ? e : exception;
|
exception = exception != null ? e : exception;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
active = false;
|
||||||
if (exception != null) {
|
if (exception != null) {
|
||||||
throw exception;
|
throw exception;
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,10 +31,12 @@ import org.keycloak.audit.Details;
|
||||||
import org.keycloak.audit.Errors;
|
import org.keycloak.audit.Errors;
|
||||||
import org.keycloak.audit.EventType;
|
import org.keycloak.audit.EventType;
|
||||||
import org.keycloak.jose.jws.JWSInput;
|
import org.keycloak.jose.jws.JWSInput;
|
||||||
|
import org.keycloak.login.LoginFormsProvider;
|
||||||
import org.keycloak.models.AccountRoles;
|
import org.keycloak.models.AccountRoles;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.Constants;
|
import org.keycloak.models.Constants;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.ModelDuplicateException;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.SocialLinkModel;
|
import org.keycloak.models.SocialLinkModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
|
@ -46,6 +48,7 @@ import org.keycloak.services.managers.RealmManager;
|
||||||
import org.keycloak.services.managers.TokenManager;
|
import org.keycloak.services.managers.TokenManager;
|
||||||
import org.keycloak.services.resources.flows.Flows;
|
import org.keycloak.services.resources.flows.Flows;
|
||||||
import org.keycloak.services.resources.flows.OAuthFlows;
|
import org.keycloak.services.resources.flows.OAuthFlows;
|
||||||
|
import org.keycloak.services.resources.flows.OAuthRedirect;
|
||||||
import org.keycloak.services.resources.flows.Urls;
|
import org.keycloak.services.resources.flows.Urls;
|
||||||
import org.keycloak.social.AuthCallback;
|
import org.keycloak.social.AuthCallback;
|
||||||
import org.keycloak.social.SocialAccessDeniedException;
|
import org.keycloak.social.SocialAccessDeniedException;
|
||||||
|
@ -67,6 +70,7 @@ import javax.ws.rs.core.Response;
|
||||||
import javax.ws.rs.core.UriBuilder;
|
import javax.ws.rs.core.UriBuilder;
|
||||||
import javax.ws.rs.core.UriInfo;
|
import javax.ws.rs.core.UriInfo;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -182,76 +186,85 @@ public class SocialResource {
|
||||||
|
|
||||||
audit.detail(Details.USERNAME, socialUser.getId() + "@" + provider.getId());
|
audit.detail(Details.USERNAME, socialUser.getId() + "@" + provider.getId());
|
||||||
|
|
||||||
SocialLinkModel socialLink = new SocialLinkModel(provider.getId(), socialUser.getId(), socialUser.getUsername());
|
try {
|
||||||
UserModel user = session.users().getUserBySocialLink(socialLink, realm);
|
SocialLinkModel socialLink = new SocialLinkModel(provider.getId(), socialUser.getId(), socialUser.getUsername());
|
||||||
|
UserModel user = session.users().getUserBySocialLink(socialLink, realm);
|
||||||
|
|
||||||
// Check if user is already authenticated (this means linking social into existing user account)
|
// Check if user is already authenticated (this means linking social into existing user account)
|
||||||
String userId = initialRequest.getUser();
|
String userId = initialRequest.getUser();
|
||||||
if (userId != null) {
|
if (userId != null) {
|
||||||
UserModel authenticatedUser = session.users().getUserById(userId, realm);
|
UserModel authenticatedUser = session.users().getUserById(userId, realm);
|
||||||
|
|
||||||
audit.event(EventType.SOCIAL_LINK).user(userId);
|
audit.event(EventType.SOCIAL_LINK).user(userId);
|
||||||
|
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
audit.error(Errors.SOCIAL_ID_IN_USE);
|
audit.error(Errors.SOCIAL_ID_IN_USE);
|
||||||
return oauth.forwardToSecurityFailure("This social account is already linked to other user");
|
return oauth.forwardToSecurityFailure("This social account is already linked to other user");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!authenticatedUser.isEnabled()) {
|
||||||
|
audit.error(Errors.USER_DISABLED);
|
||||||
|
return oauth.forwardToSecurityFailure("User is disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!authenticatedUser.hasRole(realm.getApplicationByName(Constants.ACCOUNT_MANAGEMENT_APP).getRole(AccountRoles.MANAGE_ACCOUNT))) {
|
||||||
|
audit.error(Errors.NOT_ALLOWED);
|
||||||
|
return oauth.forwardToSecurityFailure("Insufficient permissions to link social account");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (redirectUri == null) {
|
||||||
|
audit.error(Errors.INVALID_REDIRECT_URI);
|
||||||
|
return oauth.forwardToSecurityFailure("Unknown redirectUri");
|
||||||
|
}
|
||||||
|
|
||||||
|
session.users().addSocialLink(realm, authenticatedUser, socialLink);
|
||||||
|
logger.debug("Social provider " + provider.getId() + " linked with user " + authenticatedUser.getUsername());
|
||||||
|
|
||||||
|
audit.success();
|
||||||
|
return Response.status(302).location(UriBuilder.fromUri(redirectUri).build()).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!authenticatedUser.isEnabled()) {
|
if (user == null) {
|
||||||
|
user = session.users().addUser(realm, KeycloakModelUtils.generateId());
|
||||||
|
user.setEnabled(true);
|
||||||
|
user.setFirstName(socialUser.getFirstName());
|
||||||
|
user.setLastName(socialUser.getLastName());
|
||||||
|
user.setEmail(socialUser.getEmail());
|
||||||
|
|
||||||
|
if (realm.isUpdateProfileOnInitialSocialLogin()) {
|
||||||
|
user.addRequiredAction(UserModel.RequiredAction.UPDATE_PROFILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
session.users().addSocialLink(realm, user, socialLink);
|
||||||
|
|
||||||
|
audit.clone().user(user).event(EventType.REGISTER)
|
||||||
|
.detail(Details.REGISTER_METHOD, "social@" + provider.getId())
|
||||||
|
.detail(Details.EMAIL, socialUser.getEmail())
|
||||||
|
.removeDetail("auth_method")
|
||||||
|
.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
audit.user(user);
|
||||||
|
|
||||||
|
if (!user.isEnabled()) {
|
||||||
audit.error(Errors.USER_DISABLED);
|
audit.error(Errors.USER_DISABLED);
|
||||||
return oauth.forwardToSecurityFailure("User is disabled");
|
return oauth.forwardToSecurityFailure("Your account is not enabled.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!authenticatedUser.hasRole(realm.getApplicationByName(Constants.ACCOUNT_MANAGEMENT_APP).getRole(AccountRoles.MANAGE_ACCOUNT))) {
|
String username = socialLink.getSocialUserId() + "@" + socialLink.getSocialProvider();
|
||||||
audit.error(Errors.NOT_ALLOWED);
|
|
||||||
return oauth.forwardToSecurityFailure("Insufficient permissions to link social account");
|
UserSessionModel userSession = session.sessions().createUserSession(realm, user, username, clientConnection.getRemoteAddr(), authMethod, false);
|
||||||
|
audit.session(userSession);
|
||||||
|
|
||||||
|
Response response = oauth.processAccessCode(scope, state, redirectUri, client, user, userSession, audit);
|
||||||
|
if (session.getTransaction().isActive()) {
|
||||||
|
session.getTransaction().commit();
|
||||||
}
|
}
|
||||||
|
return response;
|
||||||
if (redirectUri == null) {
|
} catch (ModelDuplicateException e) {
|
||||||
audit.error(Errors.INVALID_REDIRECT_URI);
|
// Assume email is the duplicate as there's nothing else atm
|
||||||
return oauth.forwardToSecurityFailure("Unknown redirectUri");
|
return returnToLogin(realm, client, initialRequest.getAttributes(), "socialEmailExists");
|
||||||
}
|
|
||||||
|
|
||||||
session.users().addSocialLink(realm, authenticatedUser, socialLink);
|
|
||||||
logger.debug("Social provider " + provider.getId() + " linked with user " + authenticatedUser.getUsername());
|
|
||||||
|
|
||||||
audit.success();
|
|
||||||
return Response.status(302).location(UriBuilder.fromUri(redirectUri).build()).build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user == null) {
|
|
||||||
user = session.users().addUser(realm, KeycloakModelUtils.generateId());
|
|
||||||
user.setEnabled(true);
|
|
||||||
user.setFirstName(socialUser.getFirstName());
|
|
||||||
user.setLastName(socialUser.getLastName());
|
|
||||||
user.setEmail(socialUser.getEmail());
|
|
||||||
|
|
||||||
if (realm.isUpdateProfileOnInitialSocialLogin()) {
|
|
||||||
user.addRequiredAction(UserModel.RequiredAction.UPDATE_PROFILE);
|
|
||||||
}
|
|
||||||
|
|
||||||
session.users().addSocialLink(realm, user, socialLink);
|
|
||||||
|
|
||||||
audit.clone().user(user).event(EventType.REGISTER)
|
|
||||||
.detail(Details.REGISTER_METHOD, "social@" + provider.getId())
|
|
||||||
.detail(Details.EMAIL, socialUser.getEmail())
|
|
||||||
.removeDetail("auth_method")
|
|
||||||
.success();
|
|
||||||
}
|
|
||||||
|
|
||||||
audit.user(user);
|
|
||||||
|
|
||||||
if (!user.isEnabled()) {
|
|
||||||
audit.error(Errors.USER_DISABLED);
|
|
||||||
return oauth.forwardToSecurityFailure("Your account is not enabled.");
|
|
||||||
}
|
|
||||||
|
|
||||||
String username = socialLink.getSocialUserId() + "@" + socialLink.getSocialProvider();
|
|
||||||
|
|
||||||
UserSessionModel userSession = session.sessions().createUserSession(realm, user, username, clientConnection.getRemoteAddr(), authMethod, false);
|
|
||||||
audit.session(userSession);
|
|
||||||
|
|
||||||
return oauth.processAccessCode(scope, state, redirectUri, client, user, userSession, audit);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
|
@ -307,6 +320,17 @@ public class SocialResource {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Response returnToLogin(RealmModel realm, ClientModel client, Map<String, String> attributes, String error) {
|
||||||
|
MultivaluedMap<String, String> q = new MultivaluedMapImpl<String, String>();
|
||||||
|
for (Entry<String, String> e : attributes.entrySet()) {
|
||||||
|
q.add(e.getKey(), e.getValue());
|
||||||
|
}
|
||||||
|
return Flows.forms(session, realm, client, uriInfo)
|
||||||
|
.setQueryParams(q)
|
||||||
|
.setError(error)
|
||||||
|
.createLogin();
|
||||||
|
}
|
||||||
|
|
||||||
private Map<String, String[]> getQueryParams() {
|
private Map<String, String[]> getQueryParams() {
|
||||||
Map<String, String[]> queryParams = new HashMap<String, String[]>();
|
Map<String, String[]> queryParams = new HashMap<String, String[]>();
|
||||||
for (Entry<String, List<String>> e : uriInfo.getQueryParameters().entrySet()) {
|
for (Entry<String, List<String>> e : uriInfo.getQueryParameters().entrySet()) {
|
||||||
|
|
|
@ -106,6 +106,10 @@ public class UsersResource {
|
||||||
}
|
}
|
||||||
updateUserFromRep(user, rep);
|
updateUserFromRep(user, rep);
|
||||||
|
|
||||||
|
if (session.getTransaction().isActive()) {
|
||||||
|
session.getTransaction().commit();
|
||||||
|
}
|
||||||
|
|
||||||
return Response.noContent().build();
|
return Response.noContent().build();
|
||||||
} catch (ModelDuplicateException e) {
|
} catch (ModelDuplicateException e) {
|
||||||
return Flows.errors().exists("User exists with same username or email");
|
return Flows.errors().exists("User exists with same username or email");
|
||||||
|
@ -128,6 +132,10 @@ public class UsersResource {
|
||||||
UserModel user = session.users().addUser(realm, rep.getUsername());
|
UserModel user = session.users().addUser(realm, rep.getUsername());
|
||||||
updateUserFromRep(user, rep);
|
updateUserFromRep(user, rep);
|
||||||
|
|
||||||
|
if (session.getTransaction().isActive()) {
|
||||||
|
session.getTransaction().commit();
|
||||||
|
}
|
||||||
|
|
||||||
return Response.created(uriInfo.getAbsolutePathBuilder().path(user.getUsername()).build()).build();
|
return Response.created(uriInfo.getAbsolutePathBuilder().path(user.getUsername()).build()).build();
|
||||||
} catch (ModelDuplicateException e) {
|
} catch (ModelDuplicateException e) {
|
||||||
return Flows.errors().exists("User exists with same username or email");
|
return Flows.errors().exists("User exists with same username or email");
|
||||||
|
|
|
@ -156,6 +156,27 @@ public class SocialLoginTest {
|
||||||
events.expectLogin().user(userId).detail(Details.USERNAME, "1@dummy").detail(Details.AUTH_METHOD, "social@dummy").assertEvent();
|
events.expectLogin().user(userId).detail(Details.USERNAME, "1@dummy").detail(Details.AUTH_METHOD, "social@dummy").assertEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void loginEmailExists() throws Exception {
|
||||||
|
loginSuccess();
|
||||||
|
oauth.openLogout();
|
||||||
|
events.clear();
|
||||||
|
|
||||||
|
loginPage.open();
|
||||||
|
|
||||||
|
loginPage.clickSocial("dummy");
|
||||||
|
|
||||||
|
driver.findElement(By.id("id")).sendKeys("2");
|
||||||
|
driver.findElement(By.id("username")).sendKeys("dummy-user2");
|
||||||
|
driver.findElement(By.id("firstname")).sendKeys("Bob2");
|
||||||
|
driver.findElement(By.id("lastname")).sendKeys("Builder2");
|
||||||
|
driver.findElement(By.id("email")).sendKeys("bob@builder.com");
|
||||||
|
driver.findElement(By.id("login")).click();
|
||||||
|
|
||||||
|
Assert.assertTrue(loginPage.isCurrent());
|
||||||
|
Assert.assertEquals("User with email already exists. Please login to account management to link the account.", loginPage.getError());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void loginCancelled() throws Exception {
|
public void loginCancelled() throws Exception {
|
||||||
loginPage.open();
|
loginPage.open();
|
||||||
|
|
Loading…
Reference in a new issue