import org.sonar.api.utils.log.Loggers;
import static java.lang.String.format;
+import static org.sonar.server.authentication.EmailAlreadyExistsException.EMAIL_ALREADY_EXISTS_PATH;
+import static org.sonar.server.authentication.NotAllowUserToSignUpException.NOT_ALLOWED_TO_SIGHNUP_PATH;
public class AuthenticationError {
private static final Logger LOGGER = Loggers.get(AuthenticationError.class);
private static final String UNAUTHORIZED_PATH = "/sessions/unauthorized";
- private static final String NOT_ALLOWED_TO_SIGHNUP_PATH = "/sessions/not_allowed_to_sign_up?providerName=%s";
private AuthenticationError() {
// Utility class
redirectTo(response, format(NOT_ALLOWED_TO_SIGHNUP_PATH, e.getProvider().getName()));
}
+ public static void handleEmailAlreadyExistsError(EmailAlreadyExistsException e, HttpServletResponse response) {
+ redirectTo(response, format(EMAIL_ALREADY_EXISTS_PATH, e.getEmail()));
+ }
+
private static void redirectToUnauthorized(HttpServletResponse response) {
redirectTo(response, UNAUTHORIZED_PATH);
}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.authentication;
+
+public class EmailAlreadyExistsException extends RuntimeException {
+
+ public static final String EMAIL_ALREADY_EXISTS_PATH = "/sessions/email_already_exists?email=%s";
+
+ private final String email;
+
+ public EmailAlreadyExistsException(String email) {
+ this.email = email;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+}
import static com.google.common.base.Strings.isNullOrEmpty;
import static java.lang.String.format;
+import static org.sonar.server.authentication.AuthenticationError.handleEmailAlreadyExistsError;
import static org.sonar.server.authentication.AuthenticationError.handleError;
import static org.sonar.server.authentication.AuthenticationError.handleNotAllowedToSignUpError;
}
} catch (NotAllowUserToSignUpException e) {
handleNotAllowedToSignUpError(e, (HttpServletResponse) response);
+ } catch (EmailAlreadyExistsException e) {
+ handleEmailAlreadyExistsError(e, (HttpServletResponse) response);
} catch (Exception e) {
handleError(e, (HttpServletResponse) response, String.format("Fail to initialize authentication with provider '%s'", keyProvider));
}
public class NotAllowUserToSignUpException extends RuntimeException {
+ public static final String NOT_ALLOWED_TO_SIGHNUP_PATH = "/sessions/not_allowed_to_sign_up?providerName=%s";
+
private final IdentityProvider provider;
public NotAllowUserToSignUpException(IdentityProvider provider) {
import org.sonar.api.server.authentication.OAuth2IdentityProvider;
import org.sonar.api.web.ServletFilter;
+import static org.sonar.server.authentication.AuthenticationError.handleEmailAlreadyExistsError;
import static org.sonar.server.authentication.AuthenticationError.handleError;
import static org.sonar.server.authentication.AuthenticationError.handleNotAllowedToSignUpError;
}
} catch (NotAllowUserToSignUpException e) {
handleNotAllowedToSignUpError(e, (HttpServletResponse) response);
+ } catch (EmailAlreadyExistsException e) {
+ handleEmailAlreadyExistsError(e, (HttpServletResponse) response);
} catch (Exception e) {
handleError(e, (HttpServletResponse) response, String.format("Fail to callback authentication with %s", keyProvider));
}
if (!provider.allowsUsersToSignUp()) {
throw new NotAllowUserToSignUpException(provider);
}
+
+ String email = user.getEmail();
+ if (email != null && dbClient.userDao().doesEmailExist(dbSession, email)) {
+ throw new EmailAlreadyExistsException(email);
+ }
+
userUpdater.create(dbSession, NewUser.create()
.setLogin(uuidFactory.create())
.setEmail(user.getEmail())
assertError("Fail to initialize authentication with provider 'unsupported'");
}
+ @Test
+ public void redirect_when_failing_because_of_NotAllowUserToSignUpException() throws Exception {
+ IdentityProvider identityProvider = new FailWithNotAllowUserToSignUpIdProvider("failing");
+ when(request.getRequestURI()).thenReturn("/sessions/init/" + identityProvider.getKey());
+ identityProviderRepository.addIdentityProvider(identityProvider);
+
+ underTest.doFilter(request, response, chain);
+
+ verify(response).sendRedirect("/sessions/not_allowed_to_sign_up?providerName=Failing provider");
+ }
+
+ @Test
+ public void redirect_when_failing_because_of_EmailAlreadyExistsException() throws Exception {
+ IdentityProvider identityProvider = new FailWithEmailAlreadyExistsExceptionIdProvider("failing");
+ when(request.getRequestURI()).thenReturn("/sessions/init/" + identityProvider.getKey());
+ identityProviderRepository.addIdentityProvider(identityProvider);
+
+ underTest.doFilter(request, response, chain);
+
+ verify(response).sendRedirect("/sessions/email_already_exists?email=john@email.com");
+ }
+
private void assertOAuth2InitCalled(){
assertThat(logTester.logs(LoggerLevel.ERROR)).isEmpty();
assertThat(oAuth2IdentityProvider.isInitCalled()).isTrue();
verify(response).sendRedirect("/sessions/unauthorized");
assertThat(oAuth2IdentityProvider.isInitCalled()).isFalse();
}
+
+ private static class FailWithNotAllowUserToSignUpIdProvider extends FakeBasicIdentityProvider {
+
+ public FailWithNotAllowUserToSignUpIdProvider(String key) {
+ super(key, true);
+ }
+
+ @Override
+ public String getName() {
+ return "Failing provider";
+ }
+
+ @Override
+ public void init(Context context) {
+ throw new NotAllowUserToSignUpException(this);
+ }
+ }
+
+ private static class FailWithEmailAlreadyExistsExceptionIdProvider extends FakeBasicIdentityProvider {
+
+ public FailWithEmailAlreadyExistsExceptionIdProvider(String key) {
+ super(key, true);
+ }
+
+ @Override
+ public void init(Context context) {
+ throw new EmailAlreadyExistsException("john@email.com");
+ }
+ }
}
thrown.expect(NotAllowUserToSignUpException.class);
underTest.authenticate(USER_IDENTITY, identityProvider, httpSession);
}
+
+ @Test
+ public void fail_to_authenticate_new_user_when_email_already_exists() throws Exception {
+ when(userDao.selectByExternalIdentity(dbSession, USER_IDENTITY.getId(), IDENTITY_PROVIDER.getKey())).thenReturn(Optional.<UserDto>absent());
+ when(userDao.selectOrFailByExternalIdentity(dbSession, USER_IDENTITY.getId(), IDENTITY_PROVIDER.getKey())).thenReturn(ACTIVE_USER);
+ when(userDao.doesEmailExist(dbSession, USER_IDENTITY.getEmail())).thenReturn(true);
+
+ thrown.expect(EmailAlreadyExistsException.class);
+ underTest.authenticate(USER_IDENTITY, IDENTITY_PROVIDER, httpSession);
+ }
}
--- /dev/null
+<table class="spaced">
+ <tr>
+ <td align="center">
+
+ <div id="login_form">
+ <p id="unauthorized">Email <%= h params[:email] %> is already used by an existing user</p>
+ </div>
+ </td>
+ </tr>
+</table>
<td align="center">
<div id="login_form">
- <p id="unauthorized"><%= params[:providerName] %> users are not allowed to signup</p>
+ <p id="unauthorized"><%= h params[:providerName] %> users are not allowed to signup</p>
</div>
</td>
</tr>
throw new RowNotFoundException(String.format("User with identity provider '%s' and id '%s' has not been found", extIdentityProvider, extIdentity));
}
+ /**
+ * Please note that email is case insensitive, result for searching 'mail@email.com' or 'Mail@Email.com' will be the same
+ */
+ public boolean doesEmailExist(DbSession dbSession, String email){
+ return mapper(dbSession).countByEmail(email.toLowerCase()) > 0;
+ }
+
protected UserMapper mapper(DbSession session) {
return session.getMapper(UserMapper.class);
}
@CheckForNull
GroupDto selectGroupByName(String name);
+ long countByEmail(String email);
+
void insert(UserDto userDto);
void update(UserDto userDto);
</where>
</select>
+ <select id="countByEmail" parameterType="String" resultType="long">
+ SELECT count(u.id)
+ FROM users u
+ where lower(u.email)=#{email}
+ </select>
+
<select id="selectGroupByName" parameterType="string" resultType="Group">
SELECT id, name, description, created_at AS "createdAt", updated_at AS "updatedAt"
FROM groups WHERE name=#{id}
thrown.expectMessage("User with identity provider 'unknown' and id 'unknown' has not been found");
underTest.selectOrFailByExternalIdentity(session, "unknown", "unknown");
}
+
+ @Test
+ public void exists_by_email() throws Exception {
+ db.prepareDbUnit(getClass(), "exists_by_email.xml");
+
+ assertThat(underTest.doesEmailExist(session, "marius@lesbronzes.fr")).isTrue();
+ assertThat(underTest.doesEmailExist(session, "Marius@LesBronzes.fr")).isTrue();
+ assertThat(underTest.doesEmailExist(session, "unknown")).isFalse();
+ }
}
--- /dev/null
+<dataset>
+
+ <users id="101" login="marius" name="Marius" email="marius@lesbronzes.fr" active="[true]" scm_accounts=" ma marius33 " created_at="1418215735482" updated_at="1418215735485"
+ salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8365" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fg"/>
+ <users id="102" login="sbrandhof" name="Simon Brandhof" email="marius@lesbronzes.fr" active="[true]" scm_accounts="[null]" created_at="1418215735482" updated_at="1418215735485"
+ salt="79bd6a8e79fb8c76ac8b121cc7e8e11ad1af8366" crypted_password="650d2261c98361e2f67f90ce5c65a95e7d8ea2fh"/>
+
+</dataset>
* The first time a user is authenticated (and if {@link #allowsUsersToSignUp()} is true), a new user will be registered.
* Then, only user's name and email are updated.
*
- * @throws NotAllowUserToSignUpException when {@link #allowsUsersToSignUp()} is false and a new user try to authenticate
+ * If @link #allowsUsersToSignUp()} is set to false and a new user try to authenticate,
+ * then the user is not authenticated and he's redirected to a dedicated page.
+ *
+ * If the email of the user is already used by an existing user of the platform,
+ * then the user is not authenticated and he's redirected to a dedicated page.
*/
void authenticate(UserIdentity userIdentity);
void redirectToRequestedPage();
/**
- * Authenticate and register the user into the platform
+ * Authenticate and register the user into the platform.
+ * @see org.sonar.api.server.authentication.BaseIdentityProvider.Context#authenticate(UserIdentity)
*/
void authenticate(UserIdentity userIdentity);
}