]> source.dussan.org Git - sonarqube.git/blob
597f85f7cf39ebe6652f18b03e3db64122c0d893
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2023 SonarSource SA
4  * mailto:info AT sonarsource DOT com
5  *
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.
10  *
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.
15  *
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.
19  */
20 package org.sonar.server.usertoken;
21
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;
42
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;
57
58 @RunWith(DataProviderRunner.class)
59 public class UserTokenAuthenticationTest {
60
61   private static final Base64.Encoder BASE64_ENCODER = Base64.getEncoder();
62
63   private static final String AUTHORIZATION_HEADER = "Authorization";
64
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";
69
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";
74
75   @Rule
76   public DbTester db = DbTester.create(System2.INSTANCE);
77
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);
83
84   @Before
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);
92   }
93
94   @Test
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"));
104
105     Optional<UserAuthResult> result = underTest.authenticate(request);
106
107     assertThat(result).isPresent();
108     assertThat(result.get().getTokenDto().getUuid()).isEqualTo(userTokenDto.getUuid());
109     assertThat(result.get().getUserDto().getUuid())
110       .isNotNull()
111       .contains(user1.getUuid());
112     verify(userLastConnectionDatesUpdater).updateLastConnectionDateIfNeeded(any(UserTokenDto.class));
113   }
114
115   @DataProvider
116   public static Object[][] bearerHeaderName() {
117     return new Object[][] {
118       {"bearer"},
119       {"BEARER"},
120       {"Bearer"},
121       {"bEarer"},
122     };
123   }
124
125   @Test
126   @UseDataProvider("bearerHeaderName")
127   public void authenticate_withDifferentBearerHeaderNameCase_succeeds(String headerName) {
128     String token = setUpValidAuthToken();
129     when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn(headerName + " " + token);
130
131     Optional<UserAuthResult> result = underTest.authenticate(request);
132
133     assertThat(result).isPresent();
134     verify(userLastConnectionDatesUpdater).updateLastConnectionDateIfNeeded(any(UserTokenDto.class));
135   }
136
137   @Test
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);
142
143     Optional<UserAuthResult> result = underTest.authenticate(request);
144
145     assertThat(result).isEmpty();
146   }
147
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"));
156     return token;
157   }
158
159   @Test
160   public void return_login_when_token_hash_found_in_db_and_future_expiration_date() {
161     String token = "known-token";
162     String tokenHash = "123456789";
163
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"));
171
172     Optional<UserAuthResult> result = underTest.authenticate(request);
173
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())
178       .isNotNull()
179       .contains(user1.getUuid());
180     verify(userLastConnectionDatesUpdater).updateLastConnectionDateIfNeeded(any(UserTokenDto.class));
181   }
182
183   @Test
184   public void return_absent_if_username_password_used() {
185     when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64("login:password"));
186
187     Optional<UserAuthResult> result = underTest.authenticate(request);
188
189     assertThat(result).isEmpty();
190     verify(userLastConnectionDatesUpdater, never()).updateLastConnectionDateIfNeeded(any(UserTokenDto.class));
191     verifyNoInteractions(authenticationEvent);
192   }
193
194   @Test
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 + ":"));
197
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);
203   }
204
205   @Test
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));
214
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);
220   }
221
222   @Test
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();
226
227     when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64(EXAMPLE_GLOBAL_ANALYSIS_TOKEN + ":"));
228     var result = underTest.authenticate(request);
229
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);
235   }
236
237   @Test
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();
241
242     when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64(EXAMPLE_NEW_USER_TOKEN + ":"));
243     var result = underTest.authenticate(request);
244
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);
250   }
251
252   @Test
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();
258
259     when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64(EXAMPLE_PROJECT_ANALYSIS_TOKEN + ":"));
260     var result = underTest.authenticate(request);
261
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);
268   }
269
270   @Test
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();
274
275     when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64(EXAMPLE_NEW_USER_TOKEN + ":"));
276
277     assertThatThrownBy(() -> underTest.authenticate(request))
278       .hasMessageContaining("User doesn't exist")
279       .isInstanceOf(AuthenticationException.class)
280       .hasFieldOrPropertyWithValue("source", AuthenticationEvent.Source.local(SONARQUBE_TOKEN));
281
282     verifyNoInteractions(authenticationEvent);
283   }
284
285   @Test
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));
292
293     UserTokenDto result = underTest.getUserToken(token);
294
295     assertThat(result.getUuid()).isEqualTo(userTokenDto.getUuid());
296   }
297
298   @Test
299   public void return_login_when_token_hash_found_in_db2() {
300     when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn("Basic " + toBase64("login:password"));
301
302     Optional<UserAuthResult> result = underTest.authenticate(request);
303
304     assertThat(result).isEmpty();
305   }
306
307   @Test
308   public void return_login_when_token_hash_found_in_db3() {
309     when(request.getHeader(AUTHORIZATION_HEADER)).thenReturn(null);
310
311     Optional<UserAuthResult> result = underTest.authenticate(request);
312
313     assertThat(result).isEmpty();
314   }
315
316   private static String toBase64(String text) {
317     return new String(BASE64_ENCODER.encode(text.getBytes(UTF_8)));
318   }
319 }