3 * Copyright (C) 2009-2019 SonarSource SA
4 * mailto:info AT sonarsource DOT com
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 3 of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 package org.sonar.server.authentication;
22 import com.google.common.collect.ImmutableMap;
23 import java.util.Collections;
24 import java.util.HashMap;
26 import java.util.Optional;
27 import javax.annotation.Nullable;
28 import javax.servlet.http.HttpServletRequest;
29 import javax.servlet.http.HttpServletResponse;
30 import org.junit.Before;
31 import org.junit.Rule;
32 import org.junit.Test;
33 import org.junit.rules.ExpectedException;
34 import org.sonar.api.config.internal.MapSettings;
35 import org.sonar.api.impl.utils.AlwaysIncreasingSystem2;
36 import org.sonar.api.utils.System2;
37 import org.sonar.core.util.stream.MoreCollectors;
38 import org.sonar.db.DbTester;
39 import org.sonar.db.user.GroupDto;
40 import org.sonar.db.user.UserDto;
41 import org.sonar.server.authentication.event.AuthenticationEvent;
42 import org.sonar.server.authentication.event.AuthenticationEvent.Source;
43 import org.sonar.server.es.EsTester;
44 import org.sonar.server.organization.DefaultOrganizationProvider;
45 import org.sonar.server.organization.OrganizationUpdater;
46 import org.sonar.server.organization.TestDefaultOrganizationProvider;
47 import org.sonar.server.organization.TestOrganizationFlags;
48 import org.sonar.server.user.NewUserNotifier;
49 import org.sonar.server.user.UserUpdater;
50 import org.sonar.server.user.index.UserIndexer;
51 import org.sonar.server.usergroups.DefaultGroupFinder;
53 import static java.util.Arrays.stream;
54 import static org.assertj.core.api.Assertions.assertThat;
55 import static org.junit.rules.ExpectedException.none;
56 import static org.mockito.ArgumentMatchers.any;
57 import static org.mockito.ArgumentMatchers.anyMap;
58 import static org.mockito.ArgumentMatchers.eq;
59 import static org.mockito.Mockito.mock;
60 import static org.mockito.Mockito.never;
61 import static org.mockito.Mockito.verify;
62 import static org.mockito.Mockito.verifyZeroInteractions;
63 import static org.mockito.Mockito.when;
64 import static org.sonar.db.user.UserTesting.newUserDto;
65 import static org.sonar.server.authentication.event.AuthenticationExceptionMatcher.authenticationException;
67 public class HttpHeadersAuthenticationTest {
69 private MapSettings settings = new MapSettings();
72 public ExpectedException expectedException = none();
74 public DbTester db = DbTester.create(new AlwaysIncreasingSystem2());
76 public EsTester es = EsTester.create();
78 private static final String DEFAULT_LOGIN = "john";
79 private static final String DEFAULT_NAME = "John";
80 private static final String DEFAULT_EMAIL = "john@doo.com";
81 private static final String GROUP1 = "dev";
82 private static final String GROUP2 = "admin";
83 private static final String GROUPS = GROUP1 + "," + GROUP2;
85 private static final Long NOW = 1_000_000L;
86 private static final Long CLOSE_REFRESH_TIME = NOW - 1_000L;
88 private static final UserDto DEFAULT_USER = newUserDto()
89 .setLogin(DEFAULT_LOGIN)
90 .setName(DEFAULT_NAME)
91 .setEmail(DEFAULT_EMAIL)
92 .setExternalLogin(DEFAULT_LOGIN)
93 .setExternalIdentityProvider("sonarqube");
95 private GroupDto group1;
96 private GroupDto group2;
97 private GroupDto sonarUsers;
99 private System2 system2 = mock(System2.class);
100 private OrganizationUpdater organizationUpdater = mock(OrganizationUpdater.class);
101 private DefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(db);
102 private TestOrganizationFlags organizationFlags = TestOrganizationFlags.standalone();
103 private CredentialsLocalAuthentication localAuthentication = new CredentialsLocalAuthentication(db.getDbClient());
105 private UserIndexer userIndexer = new UserIndexer(db.getDbClient(), es.client());
106 private UserRegistrarImpl userIdentityAuthenticator = new UserRegistrarImpl(
108 new UserUpdater(system2, mock(NewUserNotifier.class), db.getDbClient(), userIndexer, organizationFlags, defaultOrganizationProvider,
109 new DefaultGroupFinder(db.getDbClient()), settings.asConfig(), localAuthentication),
110 defaultOrganizationProvider, organizationFlags, new DefaultGroupFinder(db.getDbClient()), null);
112 private HttpServletResponse response = mock(HttpServletResponse.class);
113 private JwtHttpHandler jwtHttpHandler = mock(JwtHttpHandler.class);
114 private AuthenticationEvent authenticationEvent = mock(AuthenticationEvent.class);
116 private HttpHeadersAuthentication underTest = new HttpHeadersAuthentication(system2, settings.asConfig(), userIdentityAuthenticator, jwtHttpHandler, authenticationEvent);
119 public void setUp() throws Exception {
120 when(system2.now()).thenReturn(NOW);
121 group1 = db.users().insertGroup(db.getDefaultOrganization(), GROUP1);
122 group2 = db.users().insertGroup(db.getDefaultOrganization(), GROUP2);
123 sonarUsers = db.users().insertDefaultGroup(db.getDefaultOrganization(), "sonar-users");
127 public void create_user_when_authenticating_new_user() {
130 HttpServletRequest request = createRequest(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, GROUPS);
132 underTest.authenticate(request, response);
134 verifyUserInDb(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, group1, group2, sonarUsers);
135 verifyTokenIsUpdated(NOW);
136 verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
140 public void use_login_when_name_is_not_provided() {
144 HttpServletRequest request = createRequest(DEFAULT_LOGIN, null, null, null);
145 underTest.authenticate(request, response);
147 verifyUserInDb(DEFAULT_LOGIN, DEFAULT_LOGIN, null, sonarUsers);
148 verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
152 public void update_user_when_authenticating_exiting_user() {
155 insertUser(newUserDto().setLogin(DEFAULT_LOGIN).setName("old name").setEmail("old email"), group1);
156 // Name, email and groups are different
157 HttpServletRequest request = createRequest(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, GROUP2);
159 underTest.authenticate(request, response);
161 verifyUserInDb(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, group2);
162 verifyTokenIsUpdated(NOW);
163 verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
167 public void remove_groups_when_group_headers_is_empty() {
170 insertUser(DEFAULT_USER, group1);
171 HttpServletRequest request = createRequest(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, "");
173 underTest.authenticate(request, response);
175 verityUserHasNoGroup(DEFAULT_LOGIN);
176 verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
180 public void remove_groups_when_group_headers_is_null() {
183 insertUser(DEFAULT_USER, group1);
184 Map<String, String> headerValuesByName = new HashMap<>();
185 headerValuesByName.put("X-Forwarded-Login", DEFAULT_LOGIN);
186 headerValuesByName.put("X-Forwarded-Groups", null);
187 HttpServletRequest request = createRequest(headerValuesByName);
189 underTest.authenticate(request, response);
191 verityUserHasNoGroup(DEFAULT_LOGIN);
192 verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
196 public void does_not_update_groups_when_no_group_headers() {
199 insertUser(DEFAULT_USER, group1, sonarUsers);
200 HttpServletRequest request = createRequest(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, null);
202 underTest.authenticate(request, response);
204 verityUserGroups(DEFAULT_LOGIN, group1, sonarUsers);
205 verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
209 public void does_not_update_user_when_user_is_in_token_and_refresh_time_is_close() {
211 UserDto user = insertUser(DEFAULT_USER, group1);
212 setUserInToken(user, CLOSE_REFRESH_TIME);
213 HttpServletRequest request = createRequest(DEFAULT_LOGIN, "new name", "new email", GROUP2);
215 underTest.authenticate(request, response);
217 // User is not updated
218 verifyUserInDb(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, group1);
219 verifyTokenIsNotUpdated();
220 verifyZeroInteractions(authenticationEvent);
224 public void update_user_when_user_in_token_but_refresh_time_is_old() {
226 UserDto user = insertUser(DEFAULT_USER, group1);
227 // Refresh time was updated 6 minutes ago => more than 5 minutes
228 setUserInToken(user, NOW - 6 * 60 * 1000L);
229 HttpServletRequest request = createRequest(DEFAULT_LOGIN, "new name", "new email", GROUP2);
231 underTest.authenticate(request, response);
234 verifyUserInDb(DEFAULT_LOGIN, "new name", "new email", group2);
235 verifyTokenIsUpdated(NOW);
236 verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
240 public void update_user_when_user_in_token_but_no_refresh_time() {
242 UserDto user = insertUser(DEFAULT_USER, group1);
243 setUserInToken(user, null);
244 HttpServletRequest request = createRequest(DEFAULT_LOGIN, "new name", "new email", GROUP2);
246 underTest.authenticate(request, response);
249 verifyUserInDb(DEFAULT_LOGIN, "new name", "new email", group2);
250 verifyTokenIsUpdated(NOW);
251 verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
255 public void use_refresh_time_from_settings() {
256 settings.setProperty("sonar.web.sso.refreshIntervalInMinutes", "10");
258 UserDto user = insertUser(DEFAULT_USER, group1);
259 // Refresh time was updated 6 minutes ago => less than 10 minutes ago so not updated
260 setUserInToken(user, NOW - 6 * 60 * 1000L);
261 HttpServletRequest request = createRequest(DEFAULT_LOGIN, "new name", "new email", GROUP2);
263 underTest.authenticate(request, response);
265 // User is not updated
266 verifyUserInDb(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, group1);
267 verifyTokenIsNotUpdated();
268 verifyZeroInteractions(authenticationEvent);
272 public void update_user_when_login_from_token_is_different_than_login_from_request() {
274 insertUser(DEFAULT_USER, group1);
275 setUserInToken(DEFAULT_USER, CLOSE_REFRESH_TIME);
276 HttpServletRequest request = createRequest("AnotherLogin", "Another name", "Another email", GROUP2);
278 underTest.authenticate(request, response);
280 verifyUserInDb("AnotherLogin", "Another name", "Another email", group2, sonarUsers);
281 verifyTokenIsUpdated(NOW);
282 verify(authenticationEvent).loginSuccess(request, "AnotherLogin", Source.sso());
286 public void use_headers_from_settings() {
287 settings.setProperty("sonar.web.sso.loginHeader", "head-login");
288 settings.setProperty("sonar.web.sso.nameHeader", "head-name");
289 settings.setProperty("sonar.web.sso.emailHeader", "head-email");
290 settings.setProperty("sonar.web.sso.groupsHeader", "head-groups");
293 HttpServletRequest request = createRequest(ImmutableMap.of("head-login", DEFAULT_LOGIN, "head-name", DEFAULT_NAME, "head-email", DEFAULT_EMAIL, "head-groups", GROUPS));
295 underTest.authenticate(request, response);
297 verifyUserInDb(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, group1, group2, sonarUsers);
298 verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
302 public void detect_group_header_even_with_wrong_case() {
303 settings.setProperty("sonar.web.sso.loginHeader", "login");
304 settings.setProperty("sonar.web.sso.nameHeader", "name");
305 settings.setProperty("sonar.web.sso.emailHeader", "email");
306 settings.setProperty("sonar.web.sso.groupsHeader", "Groups");
309 HttpServletRequest request = createRequest(ImmutableMap.of("login", DEFAULT_LOGIN, "name", DEFAULT_NAME, "email", DEFAULT_EMAIL, "groups", GROUPS));
311 underTest.authenticate(request, response);
313 verifyUserInDb(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, group1, group2, sonarUsers);
314 verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
318 public void trim_groups() {
321 HttpServletRequest request = createRequest(DEFAULT_LOGIN, null, null, " dev , admin ");
323 underTest.authenticate(request, response);
325 verifyUserInDb(DEFAULT_LOGIN, DEFAULT_LOGIN, null, group1, group2, sonarUsers);
326 verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
330 public void does_not_authenticate_when_no_header() {
334 underTest.authenticate(createRequest(Collections.emptyMap()), response);
336 verifyUserNotAuthenticated();
337 verifyTokenIsNotUpdated();
338 verifyZeroInteractions(authenticationEvent);
342 public void does_not_authenticate_when_not_enabled() {
345 underTest.authenticate(createRequest(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, GROUPS), response);
347 verifyUserNotAuthenticated();
348 verifyZeroInteractions(jwtHttpHandler, authenticationEvent);
352 public void throw_AuthenticationException_when_BadRequestException_is_generated() {
356 expectedException.expect(authenticationException().from(Source.sso()).withoutLogin().andNoPublicMessage());
357 expectedException.expectMessage("Use only letters, numbers, and .-_@ please.");
359 underTest.authenticate(createRequest("invalid login", DEFAULT_NAME, DEFAULT_EMAIL, GROUPS), response);
361 verifyZeroInteractions(authenticationEvent);
365 private void startWithSso() {
366 settings.setProperty("sonar.web.sso.enable", true);
370 private void startWithoutSso() {
371 settings.setProperty("sonar.web.sso.enable", false);
375 private void setUserInToken(UserDto user, @Nullable Long lastRefreshTime) {
376 when(jwtHttpHandler.getToken(any(HttpServletRequest.class), any(HttpServletResponse.class)))
377 .thenReturn(Optional.of(new JwtHttpHandler.Token(
379 lastRefreshTime == null ? Collections.emptyMap() : ImmutableMap.of("ssoLastRefreshTime", lastRefreshTime))));
382 private void setNotUserInToken() {
383 when(jwtHttpHandler.getToken(any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(Optional.empty());
386 private UserDto insertUser(UserDto user, GroupDto... groups) {
387 db.users().insertUser(user);
388 stream(groups).forEach(group -> db.users().insertMember(group, user));
393 private static HttpServletRequest createRequest(Map<String, String> headerValuesByName) {
394 HttpServletRequest request = mock(HttpServletRequest.class);
395 setHeaders(request, headerValuesByName);
399 private static HttpServletRequest createRequest(String login, @Nullable String name, @Nullable String email, @Nullable String groups) {
400 Map<String, String> headerValuesByName = new HashMap<>();
401 headerValuesByName.put("X-Forwarded-Login", login);
403 headerValuesByName.put("X-Forwarded-Name", name);
406 headerValuesByName.put("X-Forwarded-Email", email);
408 if (groups != null) {
409 headerValuesByName.put("X-Forwarded-Groups", groups);
411 HttpServletRequest request = mock(HttpServletRequest.class);
412 setHeaders(request, headerValuesByName);
416 private static void setHeaders(HttpServletRequest request, Map<String, String> valuesByName) {
417 valuesByName.entrySet().forEach(entry -> when(request.getHeader(entry.getKey())).thenReturn(entry.getValue()));
418 when(request.getHeaderNames()).thenReturn(Collections.enumeration(valuesByName.keySet()));
421 private void verifyUserInDb(String expectedLogin, String expectedName, @Nullable String expectedEmail, GroupDto... expectedGroups) {
422 UserDto userDto = db.users().selectUserByLogin(expectedLogin).get();
423 assertThat(userDto.isActive()).isTrue();
424 assertThat(userDto.getName()).isEqualTo(expectedName);
425 assertThat(userDto.getEmail()).isEqualTo(expectedEmail);
426 assertThat(userDto.getExternalLogin()).isEqualTo(expectedLogin);
427 assertThat(userDto.getExternalIdentityProvider()).isEqualTo("sonarqube");
428 verityUserGroups(expectedLogin, expectedGroups);
431 private void verityUserGroups(String login, GroupDto... expectedGroups) {
432 UserDto userDto = db.users().selectUserByLogin(login).get();
433 if (expectedGroups.length == 0) {
434 assertThat(db.users().selectGroupIdsOfUser(userDto)).isEmpty();
436 assertThat(db.users().selectGroupIdsOfUser(userDto)).containsOnly(stream(expectedGroups).map(GroupDto::getId).collect(MoreCollectors.toList()).toArray(new Integer[] {}));
440 private void verityUserHasNoGroup(String login) {
441 verityUserGroups(login);
444 private void verifyUserNotAuthenticated() {
445 assertThat(db.countRowsOfTable(db.getSession(), "users")).isZero();
446 verifyTokenIsNotUpdated();
449 private void verifyTokenIsUpdated(long refreshTime) {
450 verify(jwtHttpHandler).generateToken(any(UserDto.class), eq(ImmutableMap.of("ssoLastRefreshTime", refreshTime)), any(HttpServletRequest.class), any(HttpServletResponse.class));
453 private void verifyTokenIsNotUpdated() {
454 verify(jwtHttpHandler, never()).generateToken(any(UserDto.class), anyMap(), any(HttpServletRequest.class), any(HttpServletResponse.class));