3 * Copyright (C) 2009-2023 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.sonar.api.config.internal.MapSettings;
34 import org.sonar.api.impl.utils.AlwaysIncreasingSystem2;
35 import org.sonar.api.utils.System2;
36 import org.sonar.db.DbTester;
37 import org.sonar.db.audit.AuditPersister;
38 import org.sonar.db.user.GroupDto;
39 import org.sonar.db.user.UserDto;
40 import org.sonar.server.authentication.event.AuthenticationEvent;
41 import org.sonar.server.authentication.event.AuthenticationEvent.Source;
42 import org.sonar.server.authentication.event.AuthenticationException;
43 import org.sonar.server.es.EsTester;
44 import org.sonar.server.user.NewUserNotifier;
45 import org.sonar.server.user.UserUpdater;
46 import org.sonar.server.user.index.UserIndexer;
47 import org.sonar.server.usergroups.DefaultGroupFinder;
49 import static java.util.Arrays.stream;
50 import static org.assertj.core.api.Assertions.assertThat;
51 import static org.assertj.core.api.Assertions.assertThatThrownBy;
52 import static org.mockito.ArgumentMatchers.any;
53 import static org.mockito.ArgumentMatchers.anyMap;
54 import static org.mockito.ArgumentMatchers.eq;
55 import static org.mockito.Mockito.mock;
56 import static org.mockito.Mockito.never;
57 import static org.mockito.Mockito.verify;
58 import static org.mockito.Mockito.verifyNoInteractions;
59 import static org.mockito.Mockito.when;
60 import static org.sonar.db.user.UserTesting.newUserDto;
62 public class HttpHeadersAuthenticationTest {
64 private final MapSettings settings = new MapSettings().setProperty("sonar.internal.pbkdf2.iterations", "1");
67 public DbTester db = DbTester.create(new AlwaysIncreasingSystem2());
69 public EsTester es = EsTester.create();
71 private static final String DEFAULT_LOGIN = "john";
72 private static final String DEFAULT_NAME = "John";
73 private static final String DEFAULT_EMAIL = "john@doo.com";
74 private static final String GROUP1 = "dev";
75 private static final String GROUP2 = "admin";
76 private static final String GROUPS = GROUP1 + "," + GROUP2;
78 private static final Long NOW = 1_000_000L;
79 private static final Long CLOSE_REFRESH_TIME = NOW - 1_000L;
81 private static final UserDto DEFAULT_USER = newUserDto()
82 .setLogin(DEFAULT_LOGIN)
83 .setName(DEFAULT_NAME)
84 .setEmail(DEFAULT_EMAIL)
85 .setExternalLogin(DEFAULT_LOGIN)
86 .setExternalIdentityProvider("sonarqube");
88 private GroupDto group1;
89 private GroupDto group2;
90 private GroupDto sonarUsers;
92 private final System2 system2 = mock(System2.class);
93 private final CredentialsLocalAuthentication localAuthentication = new CredentialsLocalAuthentication(db.getDbClient(), settings.asConfig());
95 private final UserIndexer userIndexer = new UserIndexer(db.getDbClient(), es.client());
97 private final DefaultGroupFinder defaultGroupFinder = new DefaultGroupFinder(db.getDbClient());
98 private final UserRegistrarImpl userIdentityAuthenticator = new UserRegistrarImpl(db.getDbClient(),
99 new UserUpdater(mock(NewUserNotifier.class), db.getDbClient(), userIndexer, defaultGroupFinder, settings.asConfig(), mock(AuditPersister.class), localAuthentication),
101 private final HttpServletResponse response = mock(HttpServletResponse.class);
102 private final JwtHttpHandler jwtHttpHandler = mock(JwtHttpHandler.class);
103 private final AuthenticationEvent authenticationEvent = mock(AuthenticationEvent.class);
104 private final HttpHeadersAuthentication underTest = new HttpHeadersAuthentication(system2, settings.asConfig(), userIdentityAuthenticator, jwtHttpHandler,
105 authenticationEvent);
108 public void setUp() {
109 when(system2.now()).thenReturn(NOW);
110 group1 = db.users().insertGroup(GROUP1);
111 group2 = db.users().insertGroup(GROUP2);
112 sonarUsers = db.users().insertDefaultGroup();
116 public void create_user_when_authenticating_new_user() {
119 HttpServletRequest request = createRequest(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, GROUPS);
121 underTest.authenticate(request, response);
123 verifyUserInDb(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, group1, group2, sonarUsers);
124 verifyTokenIsUpdated(NOW);
125 verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
129 public void use_login_when_name_is_not_provided() {
133 HttpServletRequest request = createRequest(DEFAULT_LOGIN, null, null, null);
134 underTest.authenticate(request, response);
136 verifyUserInDb(DEFAULT_LOGIN, DEFAULT_LOGIN, null, sonarUsers);
137 verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
141 public void update_user_when_authenticating_exiting_user() {
144 insertUser(newUserDto().setLogin(DEFAULT_LOGIN).setExternalLogin(DEFAULT_LOGIN).setExternalIdentityProvider("sonarqube").setName("old name").setEmail(DEFAULT_USER.getEmail()), group1);
145 // Name, email and groups are different
146 HttpServletRequest request = createRequest(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, GROUP2);
148 underTest.authenticate(request, response);
150 verifyUserInDb(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, group2);
151 verifyTokenIsUpdated(NOW);
152 verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
156 public void remove_groups_when_group_headers_is_empty() {
159 insertUser(DEFAULT_USER, group1);
160 HttpServletRequest request = createRequest(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, "");
162 underTest.authenticate(request, response);
164 verityUserHasNoGroup(DEFAULT_LOGIN);
165 verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
169 public void remove_groups_when_group_headers_is_null() {
172 insertUser(DEFAULT_USER, group1);
173 Map<String, String> headerValuesByName = new HashMap<>();
174 headerValuesByName.put("X-Forwarded-Login", DEFAULT_LOGIN);
175 headerValuesByName.put("X-Forwarded-Email", DEFAULT_USER.getEmail());
176 headerValuesByName.put("X-Forwarded-Groups", null);
177 HttpServletRequest request = createRequest(headerValuesByName);
179 underTest.authenticate(request, response);
181 verityUserHasNoGroup(DEFAULT_LOGIN);
182 verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
186 public void does_not_update_groups_when_no_group_headers() {
189 insertUser(DEFAULT_USER, group1, sonarUsers);
190 HttpServletRequest request = createRequest(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, null);
192 underTest.authenticate(request, response);
194 verityUserGroups(DEFAULT_LOGIN, group1, sonarUsers);
195 verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
199 public void does_not_update_user_when_user_is_in_token_and_refresh_time_is_close() {
201 UserDto user = insertUser(DEFAULT_USER, group1);
202 setUserInToken(user, CLOSE_REFRESH_TIME);
203 HttpServletRequest request = createRequest(DEFAULT_LOGIN, "new name", "new email", GROUP2);
205 underTest.authenticate(request, response);
207 // User is not updated
208 verifyUserInDb(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, group1);
209 verifyTokenIsNotUpdated();
210 verifyNoInteractions(authenticationEvent);
214 public void update_user_when_user_in_token_but_refresh_time_is_old() {
216 UserDto user = insertUser(DEFAULT_USER, group1);
217 // Refresh time was updated 6 minutes ago => more than 5 minutes
218 setUserInToken(user, NOW - 6 * 60 * 1000L);
219 HttpServletRequest request = createRequest(DEFAULT_LOGIN, "new name", DEFAULT_USER.getEmail(), GROUP2);
221 underTest.authenticate(request, response);
224 verifyUserInDb(DEFAULT_LOGIN, "new name", DEFAULT_USER.getEmail(), group2);
225 verifyTokenIsUpdated(NOW);
226 verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
230 public void update_user_when_user_in_token_but_no_refresh_time() {
232 UserDto user = insertUser(DEFAULT_USER, group1);
233 setUserInToken(user, null);
234 HttpServletRequest request = createRequest(DEFAULT_LOGIN, "new name", DEFAULT_USER.getEmail(), GROUP2);
236 underTest.authenticate(request, response);
239 verifyUserInDb(DEFAULT_LOGIN, "new name", DEFAULT_USER.getEmail(), group2);
240 verifyTokenIsUpdated(NOW);
241 verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
245 public void use_refresh_time_from_settings() {
246 settings.setProperty("sonar.web.sso.refreshIntervalInMinutes", "10");
248 UserDto user = insertUser(DEFAULT_USER, group1);
249 // Refresh time was updated 6 minutes ago => less than 10 minutes ago so not updated
250 setUserInToken(user, NOW - 6 * 60 * 1000L);
251 HttpServletRequest request = createRequest(DEFAULT_LOGIN, "new name", "new email", GROUP2);
253 underTest.authenticate(request, response);
255 // User is not updated
256 verifyUserInDb(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, group1);
257 verifyTokenIsNotUpdated();
258 verifyNoInteractions(authenticationEvent);
262 public void update_user_when_login_from_token_is_different_than_login_from_request() {
264 insertUser(DEFAULT_USER, group1);
265 setUserInToken(DEFAULT_USER, CLOSE_REFRESH_TIME);
266 HttpServletRequest request = createRequest("AnotherLogin", "Another name", "Another email", GROUP2);
268 underTest.authenticate(request, response);
270 verifyUserInDb("AnotherLogin", "Another name", "Another email", group2, sonarUsers);
271 verifyTokenIsUpdated(NOW);
272 verify(authenticationEvent).loginSuccess(request, "AnotherLogin", Source.sso());
276 public void use_headers_from_settings() {
277 settings.setProperty("sonar.web.sso.loginHeader", "head-login");
278 settings.setProperty("sonar.web.sso.nameHeader", "head-name");
279 settings.setProperty("sonar.web.sso.emailHeader", "head-email");
280 settings.setProperty("sonar.web.sso.groupsHeader", "head-groups");
283 HttpServletRequest request = createRequest(ImmutableMap.of("head-login", DEFAULT_LOGIN, "head-name", DEFAULT_NAME, "head-email", DEFAULT_EMAIL, "head-groups", GROUPS));
285 underTest.authenticate(request, response);
287 verifyUserInDb(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, group1, group2, sonarUsers);
288 verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
292 public void detect_group_header_even_with_wrong_case() {
293 settings.setProperty("sonar.web.sso.loginHeader", "login");
294 settings.setProperty("sonar.web.sso.nameHeader", "name");
295 settings.setProperty("sonar.web.sso.emailHeader", "email");
296 settings.setProperty("sonar.web.sso.groupsHeader", "Groups");
299 HttpServletRequest request = createRequest(ImmutableMap.of("login", DEFAULT_LOGIN, "name", DEFAULT_NAME, "email", DEFAULT_EMAIL, "groups", GROUPS));
301 underTest.authenticate(request, response);
303 verifyUserInDb(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, group1, group2, sonarUsers);
304 verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
308 public void trim_groups() {
311 HttpServletRequest request = createRequest(DEFAULT_LOGIN, null, null, " dev , admin ");
313 underTest.authenticate(request, response);
315 verifyUserInDb(DEFAULT_LOGIN, DEFAULT_LOGIN, null, group1, group2, sonarUsers);
316 verify(authenticationEvent).loginSuccess(request, DEFAULT_LOGIN, Source.sso());
320 public void does_not_authenticate_when_no_header() {
324 underTest.authenticate(createRequest(Collections.emptyMap()), response);
326 verifyUserNotAuthenticated();
327 verifyTokenIsNotUpdated();
328 verifyNoInteractions(authenticationEvent);
332 public void does_not_authenticate_when_not_enabled() {
335 underTest.authenticate(createRequest(DEFAULT_LOGIN, DEFAULT_NAME, DEFAULT_EMAIL, GROUPS), response);
337 verifyUserNotAuthenticated();
338 verifyNoInteractions(jwtHttpHandler, authenticationEvent);
342 public void throw_AuthenticationException_when_BadRequestException_is_generated() {
346 assertThatThrownBy(() -> underTest.authenticate(createRequest("invalid login", DEFAULT_NAME, DEFAULT_EMAIL, GROUPS), response))
347 .hasMessage("Login should contain only letters, numbers, and .-_@")
348 .isInstanceOf(AuthenticationException.class)
349 .hasFieldOrPropertyWithValue("source", Source.sso());
351 verifyNoInteractions(authenticationEvent);
354 private void startWithSso() {
355 settings.setProperty("sonar.web.sso.enable", true);
359 private void startWithoutSso() {
360 settings.setProperty("sonar.web.sso.enable", false);
364 private void setUserInToken(UserDto user, @Nullable Long lastRefreshTime) {
365 when(jwtHttpHandler.getToken(any(HttpServletRequest.class), any(HttpServletResponse.class)))
366 .thenReturn(Optional.of(new JwtHttpHandler.Token(
368 lastRefreshTime == null ? Collections.emptyMap() : ImmutableMap.of("ssoLastRefreshTime", lastRefreshTime))));
371 private void setNotUserInToken() {
372 when(jwtHttpHandler.getToken(any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(Optional.empty());
375 private UserDto insertUser(UserDto user, GroupDto... groups) {
376 db.users().insertUser(user);
377 stream(groups).forEach(group -> db.users().insertMember(group, user));
382 private static HttpServletRequest createRequest(Map<String, String> headerValuesByName) {
383 HttpServletRequest request = mock(HttpServletRequest.class);
384 setHeaders(request, headerValuesByName);
388 private static HttpServletRequest createRequest(String login, @Nullable String name, @Nullable String email, @Nullable String groups) {
389 Map<String, String> headerValuesByName = new HashMap<>();
390 headerValuesByName.put("X-Forwarded-Login", login);
392 headerValuesByName.put("X-Forwarded-Name", name);
395 headerValuesByName.put("X-Forwarded-Email", email);
397 if (groups != null) {
398 headerValuesByName.put("X-Forwarded-Groups", groups);
400 HttpServletRequest request = mock(HttpServletRequest.class);
401 setHeaders(request, headerValuesByName);
405 private static void setHeaders(HttpServletRequest request, Map<String, String> valuesByName) {
406 valuesByName.forEach((key, value) -> when(request.getHeader(key)).thenReturn(value));
407 when(request.getHeaderNames()).thenReturn(Collections.enumeration(valuesByName.keySet()));
410 private void verifyUserInDb(String expectedLogin, String expectedName, @Nullable String expectedEmail, GroupDto... expectedGroups) {
411 UserDto userDto = db.users().selectUserByLogin(expectedLogin).get();
412 assertThat(userDto.isActive()).isTrue();
413 assertThat(userDto.getName()).isEqualTo(expectedName);
414 assertThat(userDto.getEmail()).isEqualTo(expectedEmail);
415 assertThat(userDto.getExternalLogin()).isEqualTo(expectedLogin);
416 assertThat(userDto.getExternalIdentityProvider()).isEqualTo("sonarqube");
417 verityUserGroups(expectedLogin, expectedGroups);
420 private void verityUserGroups(String login, GroupDto... expectedGroups) {
421 UserDto userDto = db.users().selectUserByLogin(login).get();
422 if (expectedGroups.length == 0) {
423 assertThat(db.users().selectGroupUuidsOfUser(userDto)).isEmpty();
425 assertThat(db.users().selectGroupUuidsOfUser(userDto)).containsOnly(stream(expectedGroups).map(GroupDto::getUuid).toArray(String[]::new));
429 private void verityUserHasNoGroup(String login) {
430 verityUserGroups(login);
433 private void verifyUserNotAuthenticated() {
434 assertThat(db.countRowsOfTable(db.getSession(), "users")).isZero();
435 verifyTokenIsNotUpdated();
438 private void verifyTokenIsUpdated(long refreshTime) {
439 verify(jwtHttpHandler).generateToken(any(UserDto.class), eq(ImmutableMap.of("ssoLastRefreshTime", refreshTime)), any(HttpServletRequest.class), any(HttpServletResponse.class));
442 private void verifyTokenIsNotUpdated() {
443 verify(jwtHttpHandler, never()).generateToken(any(UserDto.class), anyMap(), any(HttpServletRequest.class), any(HttpServletResponse.class));