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.usertoken;
22 import com.tngtech.java.junit.dataprovider.DataProvider;
23 import com.tngtech.java.junit.dataprovider.DataProviderRunner;
24 import com.tngtech.java.junit.dataprovider.UseDataProvider;
25 import java.time.ZoneOffset;
26 import java.time.ZonedDateTime;
27 import java.util.Base64;
28 import java.util.Optional;
29 import org.junit.Before;
30 import org.junit.Rule;
31 import org.junit.Test;
32 import org.junit.runner.RunWith;
33 import org.sonar.api.server.http.HttpRequest;
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.UserAuthResult;
39 import org.sonar.server.authentication.UserLastConnectionDatesUpdater;
40 import org.sonar.server.authentication.event.AuthenticationEvent;
41 import org.sonar.server.authentication.event.AuthenticationException;
43 import static java.nio.charset.StandardCharsets.UTF_8;
44 import static org.assertj.core.api.Assertions.assertThat;
45 import static org.assertj.core.api.Assertions.assertThatThrownBy;
46 import static org.mockito.ArgumentMatchers.any;
47 import static org.mockito.Mockito.mock;
48 import static org.mockito.Mockito.never;
49 import static org.mockito.Mockito.verify;
50 import static org.mockito.Mockito.verifyNoInteractions;
51 import static org.mockito.Mockito.when;
52 import static org.sonar.api.utils.DateUtils.formatDateTime;
53 import static org.sonar.db.user.TokenType.GLOBAL_ANALYSIS_TOKEN;
54 import static org.sonar.db.user.TokenType.PROJECT_ANALYSIS_TOKEN;
55 import static org.sonar.db.user.TokenType.USER_TOKEN;
56 import static org.sonar.server.authentication.event.AuthenticationEvent.Method.SONARQUBE_TOKEN;
58 @RunWith(DataProviderRunner.class)
59 public class UserTokenAuthenticationTest {
61 private static final Base64.Encoder BASE64_ENCODER = Base64.getEncoder();
63 private static final String AUTHORIZATION_HEADER = "Authorization";
65 private static final String EXAMPLE_OLD_USER_TOKEN = "StringWith40CharactersThatIsOldUserToken";
66 private static final String EXAMPLE_NEW_USER_TOKEN = "squ_StringWith44CharactersThatIsNewUserToken";
67 private static final String EXAMPLE_GLOBAL_ANALYSIS_TOKEN = "sqa_StringWith44CharactersWhichIsGlobalToken";
68 private static final String EXAMPLE_PROJECT_ANALYSIS_TOKEN = "sqp_StringWith44CharactersThatIsProjectToken";
70 private static final String OLD_USER_TOKEN_HASH = "old-user-token-hash";
71 private static final String NEW_USER_TOKEN_HASH = "new-user-token-hash";
72 private static final String PROJECT_ANALYSIS_TOKEN_HASH = "project-analysis-token-hash";
73 private static final String GLOBAL_ANALYSIS_TOKEN_HASH = "global-analysis-token-hash";
76 public DbTester db = DbTester.create(System2.INSTANCE);
78 private final TokenGenerator tokenGenerator = mock(TokenGenerator.class);
79 private final UserLastConnectionDatesUpdater userLastConnectionDatesUpdater = mock(UserLastConnectionDatesUpdater.class);
80 private final AuthenticationEvent authenticationEvent = mock(AuthenticationEvent.class);
81 private final HttpRequest request = mock(HttpRequest.class);
82 private final UserTokenAuthentication underTest = new UserTokenAuthentication(tokenGenerator, db.getDbClient(), userLastConnectionDatesUpdater, authenticationEvent);
85 public void before() {
86 when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64("token:"));
87 when(request.getServletPath()).thenReturn("/api/anypath");
88 when(tokenGenerator.hash(EXAMPLE_OLD_USER_TOKEN)).thenReturn(OLD_USER_TOKEN_HASH);
89 when(tokenGenerator.hash(EXAMPLE_NEW_USER_TOKEN)).thenReturn(NEW_USER_TOKEN_HASH);
90 when(tokenGenerator.hash(EXAMPLE_PROJECT_ANALYSIS_TOKEN)).thenReturn(PROJECT_ANALYSIS_TOKEN_HASH);
91 when(tokenGenerator.hash(EXAMPLE_GLOBAL_ANALYSIS_TOKEN)).thenReturn(GLOBAL_ANALYSIS_TOKEN_HASH);
95 public void return_login_when_token_hash_found_in_db_and_basic_auth_used() {
96 String token = "known-token";
97 String tokenHash = "123456789";
98 when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64(token + ":"));
99 when(tokenGenerator.hash(token)).thenReturn(tokenHash);
100 UserDto user1 = db.users().insertUser();
101 UserTokenDto userTokenDto = db.users().insertToken(user1, t -> t.setTokenHash(tokenHash));
102 UserDto user2 = db.users().insertUser();
103 db.users().insertToken(user2, t -> t.setTokenHash("another-token-hash"));
105 Optional<UserAuthResult> result = underTest.authenticate(request);
107 assertThat(result).isPresent();
108 assertThat(result.get().getTokenDto().getUuid()).isEqualTo(userTokenDto.getUuid());
109 assertThat(result.get().getUserDto().getUuid())
111 .contains(user1.getUuid());
112 verify(userLastConnectionDatesUpdater).updateLastConnectionDateIfNeeded(any(UserTokenDto.class));
116 public static Object[][] bearerHeaderName() {
117 return new Object[][] {
126 @UseDataProvider("bearerHeaderName")
127 public void authenticate_withDifferentBearerHeaderNameCase_succeeds(String headerName) {
128 String token = setUpValidAuthToken();
129 when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn(headerName + " " + token);
131 Optional<UserAuthResult> result = underTest.authenticate(request);
133 assertThat(result).isPresent();
134 verify(userLastConnectionDatesUpdater).updateLastConnectionDateIfNeeded(any(UserTokenDto.class));
138 public void authenticate_withValidCamelcaseBearerTokenForMetricsAction_fails() {
139 String token = setUpValidAuthToken();
140 when(request.getServletPath()).thenReturn("/api/monitoring/metrics");
141 when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Bearer " + token);
143 Optional<UserAuthResult> result = underTest.authenticate(request);
145 assertThat(result).isEmpty();
148 private String setUpValidAuthToken() {
149 String token = "known-token";
150 String tokenHash = "123456789";
151 when(tokenGenerator.hash(token)).thenReturn(tokenHash);
152 UserDto user1 = db.users().insertUser();
153 UserTokenDto userTokenDto = db.users().insertToken(user1, t -> t.setTokenHash(tokenHash));
154 UserDto user2 = db.users().insertUser();
155 db.users().insertToken(user2, t -> t.setTokenHash("another-token-hash"));
160 public void return_login_when_token_hash_found_in_db_and_future_expiration_date() {
161 String token = "known-token";
162 String tokenHash = "123456789";
164 long expirationTimestamp = ZonedDateTime.now(ZoneOffset.UTC).plusDays(10).toInstant().toEpochMilli();
165 when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64(token + ":"));
166 when(tokenGenerator.hash(token)).thenReturn(tokenHash);
167 UserDto user1 = db.users().insertUser();
168 UserTokenDto userTokenDto = db.users().insertToken(user1, t -> t.setTokenHash(tokenHash).setExpirationDate(expirationTimestamp));
169 UserDto user2 = db.users().insertUser();
170 db.users().insertToken(user2, t -> t.setTokenHash("another-token-hash"));
172 Optional<UserAuthResult> result = underTest.authenticate(request);
174 assertThat(result).isPresent();
175 assertThat(result.get().getTokenDto().getUuid()).isEqualTo(userTokenDto.getUuid());
176 assertThat(result.get().getTokenDto().getExpirationDate()).isEqualTo(expirationTimestamp);
177 assertThat(result.get().getUserDto().getUuid())
179 .contains(user1.getUuid());
180 verify(userLastConnectionDatesUpdater).updateLastConnectionDateIfNeeded(any(UserTokenDto.class));
184 public void return_absent_if_username_password_used() {
185 when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64("login:password"));
187 Optional<UserAuthResult> result = underTest.authenticate(request);
189 assertThat(result).isEmpty();
190 verify(userLastConnectionDatesUpdater, never()).updateLastConnectionDateIfNeeded(any(UserTokenDto.class));
191 verifyNoInteractions(authenticationEvent);
195 public void throw_authentication_exception_if_token_hash_is_not_found() {
196 when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64(EXAMPLE_OLD_USER_TOKEN + ":"));
198 assertThatThrownBy(() -> underTest.authenticate(request))
199 .hasMessageContaining("Token doesn't exist")
200 .isInstanceOf(AuthenticationException.class);
201 verify(userLastConnectionDatesUpdater, never()).updateLastConnectionDateIfNeeded(any(UserTokenDto.class));
202 verifyNoInteractions(authenticationEvent);
206 public void throw_authentication_exception_if_token_is_expired() {
207 String token = "known-token";
208 String tokenHash = "123456789";
209 long expirationTimestamp = System.currentTimeMillis();
210 when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64(token + ":"));
211 when(tokenGenerator.hash(token)).thenReturn(tokenHash);
212 UserDto user1 = db.users().insertUser();
213 db.users().insertToken(user1, t -> t.setTokenHash(tokenHash).setExpirationDate(expirationTimestamp));
215 assertThatThrownBy(() -> underTest.authenticate(request))
216 .hasMessageContaining("The token expired on " + formatDateTime(expirationTimestamp))
217 .isInstanceOf(AuthenticationException.class);
218 verify(userLastConnectionDatesUpdater, never()).updateLastConnectionDateIfNeeded(any(UserTokenDto.class));
219 verifyNoInteractions(authenticationEvent);
223 public void authenticate_givenGlobalToken_resultContainsUuid() {
224 UserDto user = db.users().insertUser();
225 String tokenName = db.users().insertToken(user, t -> t.setTokenHash(GLOBAL_ANALYSIS_TOKEN_HASH).setType(GLOBAL_ANALYSIS_TOKEN.name())).getName();
227 when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64(EXAMPLE_GLOBAL_ANALYSIS_TOKEN + ":"));
228 var result = underTest.authenticate(request);
230 assertThat(result).isPresent();
231 assertThat(result.get().getTokenDto().getUuid()).isNotNull();
232 assertThat(result.get().getTokenDto().getType()).isEqualTo(GLOBAL_ANALYSIS_TOKEN.name());
233 verify(authenticationEvent).loginSuccess(request, user.getLogin(), AuthenticationEvent.Source.local(SONARQUBE_TOKEN));
234 verify(request).setAttribute("TOKEN_NAME", tokenName);
238 public void authenticate_givenNewUserToken_resultContainsUuid() {
239 UserDto user = db.users().insertUser();
240 String tokenName = db.users().insertToken(user, t -> t.setTokenHash(NEW_USER_TOKEN_HASH).setType(USER_TOKEN.name())).getName();
242 when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64(EXAMPLE_NEW_USER_TOKEN + ":"));
243 var result = underTest.authenticate(request);
245 assertThat(result).isPresent();
246 assertThat(result.get().getTokenDto().getUuid()).isNotNull();
247 assertThat(result.get().getTokenDto().getType()).isEqualTo(USER_TOKEN.name());
248 verify(authenticationEvent).loginSuccess(request, user.getLogin(), AuthenticationEvent.Source.local(SONARQUBE_TOKEN));
249 verify(request).setAttribute("TOKEN_NAME", tokenName);
253 public void authenticate_givenProjectToken_resultContainsUuid() {
254 UserDto user = db.users().insertUser();
255 String tokenName = db.users().insertToken(user, t -> t.setTokenHash(PROJECT_ANALYSIS_TOKEN_HASH)
256 .setProjectKey("project-key")
257 .setType(PROJECT_ANALYSIS_TOKEN.name())).getName();
259 when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64(EXAMPLE_PROJECT_ANALYSIS_TOKEN + ":"));
260 var result = underTest.authenticate(request);
262 assertThat(result).isPresent();
263 assertThat(result.get().getTokenDto().getUuid()).isNotNull();
264 assertThat(result.get().getTokenDto().getType()).isEqualTo(PROJECT_ANALYSIS_TOKEN.name());
265 assertThat(result.get().getTokenDto().getProjectKey()).isEqualTo("project-key");
266 verify(authenticationEvent).loginSuccess(request, user.getLogin(), AuthenticationEvent.Source.local(SONARQUBE_TOKEN));
267 verify(request).setAttribute("TOKEN_NAME", tokenName);
271 public void does_not_authenticate_from_user_token_when_token_does_not_match_active_user() {
272 UserDto user = db.users().insertDisabledUser();
273 String tokenName = db.users().insertToken(user, t -> t.setTokenHash(NEW_USER_TOKEN_HASH).setType(USER_TOKEN.name())).getName();
275 when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64(EXAMPLE_NEW_USER_TOKEN + ":"));
277 assertThatThrownBy(() -> underTest.authenticate(request))
278 .hasMessageContaining("User doesn't exist")
279 .isInstanceOf(AuthenticationException.class)
280 .hasFieldOrPropertyWithValue("source", AuthenticationEvent.Source.local(SONARQUBE_TOKEN));
282 verifyNoInteractions(authenticationEvent);
286 public void return_token_from_db() {
287 String token = "known-token";
288 String tokenHash = "123456789";
289 when(tokenGenerator.hash(token)).thenReturn(tokenHash);
290 UserDto user1 = db.users().insertUser();
291 UserTokenDto userTokenDto = db.users().insertToken(user1, t -> t.setTokenHash(tokenHash));
293 UserTokenDto result = underTest.getUserToken(token);
295 assertThat(result.getUuid()).isEqualTo(userTokenDto.getUuid());
299 public void return_login_when_token_hash_found_in_db2() {
300 when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64("login:password"));
302 Optional<UserAuthResult> result = underTest.authenticate(request);
304 assertThat(result).isEmpty();
308 public void return_login_when_token_hash_found_in_db3() {
309 when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn(null);
311 Optional<UserAuthResult> result = underTest.authenticate(request);
313 assertThat(result).isEmpty();
316 private static String toBase64(String text) {
317 return new String(BASE64_ENCODER.encode(text.getBytes(UTF_8)));