3 * Copyright (C) 2009-2024 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 java.time.LocalDateTime;
23 import java.time.ZoneOffset;
24 import org.junit.Before;
25 import org.junit.Rule;
26 import org.junit.Test;
27 import org.mockito.ArgumentCaptor;
29 import org.sonar.api.config.internal.MapSettings;
30 import org.sonar.api.server.authentication.BaseIdentityProvider;
31 import org.sonar.api.server.http.Cookie;
32 import org.sonar.api.server.http.HttpRequest;
33 import org.sonar.api.server.http.HttpResponse;
34 import org.sonar.api.utils.System2;
35 import org.sonar.db.DbTester;
36 import org.sonar.db.user.UserDto;
37 import org.sonar.db.user.UserTokenDto;
38 import org.sonar.server.authentication.event.AuthenticationEvent;
39 import org.sonar.server.authentication.event.AuthenticationEvent.Method;
40 import org.sonar.server.authentication.event.AuthenticationEvent.Source;
41 import org.sonar.server.authentication.event.AuthenticationException;
42 import org.sonar.server.tester.AnonymousMockUserSession;
43 import org.sonar.server.tester.MockUserSession;
44 import org.sonar.server.user.ThreadLocalUserSession;
45 import org.sonar.server.user.TokenUserSession;
46 import org.sonar.server.user.UserSession;
48 import static org.assertj.core.api.Assertions.assertThat;
49 import static org.mockito.ArgumentMatchers.eq;
50 import static org.mockito.Mockito.doThrow;
51 import static org.mockito.Mockito.mock;
52 import static org.mockito.Mockito.reset;
53 import static org.mockito.Mockito.verify;
54 import static org.mockito.Mockito.verifyNoMoreInteractions;
55 import static org.mockito.Mockito.when;
56 import static org.sonar.api.utils.DateUtils.formatDateTime;
58 public class UserSessionInitializerIT {
61 public DbTester dbTester = DbTester.create(System2.INSTANCE);
63 private ThreadLocalUserSession threadLocalSession = mock(ThreadLocalUserSession.class);
64 private HttpRequest request = mock(HttpRequest.class);
65 private HttpResponse response = mock(HttpResponse.class);
66 private RequestAuthenticator authenticator = mock(RequestAuthenticator.class);
67 private AuthenticationEvent authenticationEvent = mock(AuthenticationEvent.class);
68 private MapSettings settings = new MapSettings();
69 private ArgumentCaptor<Cookie> cookieArgumentCaptor = ArgumentCaptor.forClass(Cookie.class);
71 private UserSessionInitializer underTest = new UserSessionInitializer(settings.asConfig(), threadLocalSession, authenticationEvent, authenticator);
75 when(request.getContextPath()).thenReturn("");
76 when(request.getRequestURI()).thenReturn("/measures");
80 public void check_urls() {
81 assertPathIsNotIgnored("/");
82 assertPathIsNotIgnored("/foo");
83 assertPathIsNotIgnored("/api/server_id/show");
85 assertPathIsIgnored("/api/authentication/login");
86 assertPathIsIgnored("/api/authentication/logout");
87 assertPathIsIgnored("/api/authentication/validate");
88 assertPathIsIgnored("/batch/index");
89 assertPathIsIgnored("/batch/file");
90 assertPathIsIgnored("/maintenance/index");
91 assertPathIsIgnored("/setup/index");
92 assertPathIsIgnored("/sessions/new");
93 assertPathIsIgnored("/sessions/logout");
94 assertPathIsIgnored("/sessions/unauthorized");
95 assertPathIsIgnored("/oauth2/callback/github");
96 assertPathIsIgnored("/oauth2/callback/foo");
97 assertPathIsIgnored("/api/system/db_migration_status");
98 assertPathIsIgnored("/api/system/status");
99 assertPathIsIgnored("/api/system/migrate_db");
100 assertPathIsIgnored("/api/server/version");
101 assertPathIsIgnored("/api/users/identity_providers");
102 assertPathIsIgnored("/api/l10n/index");
104 // exclude project_badge url, as they can be auth. by a token as queryparam
105 assertPathIsIgnored("/api/project_badges/measure");
106 assertPathIsIgnored("/api/project_badges/quality_gate");
108 // exlude passcode urls
109 assertPathIsIgnoredWithAnonymousAccess("/api/ce/info");
110 assertPathIsIgnoredWithAnonymousAccess("/api/ce/pause");
111 assertPathIsIgnoredWithAnonymousAccess("/api/ce/resume");
112 assertPathIsIgnoredWithAnonymousAccess("/api/system/health");
113 assertPathIsIgnoredWithAnonymousAccess("/api/system/liveness");
114 assertPathIsIgnoredWithAnonymousAccess("/api/monitoring/metrics");
116 // exclude static resources
117 assertPathIsIgnored("/css/style.css");
118 assertPathIsIgnored("/images/logo.png");
119 assertPathIsIgnored("/js/jquery.js");
123 public void return_code_401_when_not_authenticated_and_with_force_authentication() {
124 ArgumentCaptor<AuthenticationException> exceptionArgumentCaptor = ArgumentCaptor.forClass(AuthenticationException.class);
125 when(threadLocalSession.isLoggedIn()).thenReturn(false);
126 when(authenticator.authenticate(request, response)).thenReturn(new AnonymousMockUserSession());
128 assertThat(underTest.initUserSession(request, response)).isTrue();
130 verifyNoMoreInteractions(response);
131 verify(authenticationEvent).loginFailure(eq(request), exceptionArgumentCaptor.capture());
132 verifyNoMoreInteractions(threadLocalSession);
133 AuthenticationException authenticationException = exceptionArgumentCaptor.getValue();
134 assertThat(authenticationException.getSource()).isEqualTo(Source.local(Method.BASIC));
135 assertThat(authenticationException.getLogin()).isNull();
136 assertThat(authenticationException.getMessage()).isEqualTo("User must be authenticated");
137 assertThat(authenticationException.getPublicMessage()).isNull();
141 public void does_not_return_code_401_when_not_authenticated_and_with_force_authentication_off() {
142 when(threadLocalSession.isLoggedIn()).thenReturn(false);
143 when(authenticator.authenticate(request, response)).thenReturn(new AnonymousMockUserSession());
144 settings.setProperty("sonar.forceAuthentication", false);
146 assertThat(underTest.initUserSession(request, response)).isTrue();
148 verifyNoMoreInteractions(response);
152 public void return_401_and_stop_on_ws() {
153 when(request.getRequestURI()).thenReturn("/api/issues");
154 AuthenticationException authenticationException = AuthenticationException.newBuilder().setSource(Source.jwt()).setMessage("Token id hasn't been found").build();
155 doThrow(authenticationException).when(authenticator).authenticate(request, response);
157 assertThat(underTest.initUserSession(request, response)).isFalse();
159 verify(response).setStatus(401);
160 verify(authenticationEvent).loginFailure(request, authenticationException);
161 verifyNoMoreInteractions(threadLocalSession);
165 public void return_401_and_stop_on_batch_ws() {
166 when(request.getRequestURI()).thenReturn("/batch/global");
167 doThrow(AuthenticationException.newBuilder().setSource(Source.jwt()).setMessage("Token id hasn't been found").build())
168 .when(authenticator).authenticate(request, response);
170 assertThat(underTest.initUserSession(request, response)).isFalse();
172 verify(response).setStatus(401);
173 verifyNoMoreInteractions(threadLocalSession);
177 public void return_to_session_unauthorized_when_error_on_from_external_provider() throws Exception {
178 doThrow(AuthenticationException.newBuilder().setSource(Source.external(newBasicIdentityProvider("failing"))).setPublicMessage("Token id hasn't been found").build())
179 .when(authenticator).authenticate(request, response);
181 assertThat(underTest.initUserSession(request, response)).isFalse();
183 verify(response).sendRedirect("/sessions/unauthorized");
184 verify(response).addCookie(cookieArgumentCaptor.capture());
185 Cookie cookie = cookieArgumentCaptor.getValue();
186 assertThat(cookie.getName()).isEqualTo("AUTHENTICATION-ERROR");
187 assertThat(cookie.getValue()).isEqualTo("Token%20id%20hasn%27t%20been%20found");
188 assertThat(cookie.getPath()).isEqualTo("/");
189 assertThat(cookie.isHttpOnly()).isFalse();
190 assertThat(cookie.getMaxAge()).isEqualTo(300);
191 assertThat(cookie.isSecure()).isFalse();
195 public void return_to_session_unauthorized_when_error_on_from_external_provider_with_context_path() throws Exception {
196 when(request.getContextPath()).thenReturn("/sonarqube");
197 doThrow(AuthenticationException.newBuilder().setSource(Source.external(newBasicIdentityProvider("failing"))).setPublicMessage("Token id hasn't been found").build())
198 .when(authenticator).authenticate(request, response);
200 assertThat(underTest.initUserSession(request, response)).isFalse();
202 verify(response).sendRedirect("/sonarqube/sessions/unauthorized");
206 public void expiration_header_added_when_authenticating_with_an_expiring_token() {
207 long expirationTimestamp = LocalDateTime.now().toInstant(ZoneOffset.UTC).toEpochMilli();
208 UserDto userDto = new UserDto();
209 UserTokenDto userTokenDto = new UserTokenDto().setExpirationDate(expirationTimestamp);
210 UserSession session = new TokenUserSession(DbTester.create().getDbClient(), userDto, userTokenDto);
212 when(authenticator.authenticate(request, response)).thenReturn(session);
213 when(threadLocalSession.isLoggedIn()).thenReturn(true);
215 assertThat(underTest.initUserSession(request, response)).isTrue();
216 verify(response).addHeader("SonarQube-Authentication-Token-Expiration", formatDateTime(expirationTimestamp));
220 public void initUserSession_shouldPutLoginInMDC() {
221 when(threadLocalSession.isLoggedIn()).thenReturn(false);
222 when(authenticator.authenticate(request, response)).thenReturn(new MockUserSession("user"));
224 underTest.initUserSession(request, response);
226 assertThat(MDC.get("LOGIN")).isEqualTo("user");
230 public void initUserSession_whenSessionLoginIsNull_shouldPutDefaultLoginValueInMDC() {
231 when(threadLocalSession.isLoggedIn()).thenReturn(false);
232 when(authenticator.authenticate(request, response)).thenReturn(new AnonymousMockUserSession());
234 underTest.initUserSession(request, response);
236 assertThat(MDC.get("LOGIN")).isEqualTo("-");
240 public void removeUserSession_shoudlRemoveMDCLogin() {
241 when(threadLocalSession.isLoggedIn()).thenReturn(false);
242 when(authenticator.authenticate(request, response)).thenReturn(new MockUserSession("user"));
243 underTest.initUserSession(request, response);
245 underTest.removeUserSession();
247 assertThat(MDC.get("LOGIN")).isNull();
250 private void assertPathIsIgnored(String path) {
251 when(request.getRequestURI()).thenReturn(path);
253 assertThat(underTest.initUserSession(request, response)).isTrue();
255 verifyNoMoreInteractions(threadLocalSession, authenticator);
256 reset(threadLocalSession, authenticator);
259 private void assertPathIsIgnoredWithAnonymousAccess(String path) {
260 when(request.getRequestURI()).thenReturn(path);
261 UserSession session = new AnonymousMockUserSession();
262 when(authenticator.authenticate(request, response)).thenReturn(session);
264 assertThat(underTest.initUserSession(request, response)).isTrue();
266 verify(threadLocalSession).set(session);
267 reset(threadLocalSession, authenticator);
270 private void assertPathIsNotIgnored(String path) {
271 when(request.getRequestURI()).thenReturn(path);
272 UserSession session = new MockUserSession("foo");
273 when(authenticator.authenticate(request, response)).thenReturn(session);
275 assertThat(underTest.initUserSession(request, response)).isTrue();
277 verify(threadLocalSession).set(session);
278 reset(threadLocalSession, authenticator);
281 private static BaseIdentityProvider newBasicIdentityProvider(String name) {
282 BaseIdentityProvider mock = mock(BaseIdentityProvider.class);
283 when(mock.getName()).thenReturn(name);