]> source.dussan.org Git - sonarqube.git/blob
72631d2ee95545ded8be70da254388d658ea8612
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2022 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.authentication;
21
22 import java.util.Optional;
23 import java.util.Random;
24 import org.apache.commons.codec.digest.DigestUtils;
25 import org.junit.Before;
26 import org.junit.Rule;
27 import org.junit.Test;
28 import org.mindrot.jbcrypt.BCrypt;
29 import org.sonar.api.config.internal.MapSettings;
30 import org.sonar.db.DbSession;
31 import org.sonar.db.DbTester;
32 import org.sonar.db.user.UserDto;
33 import org.sonar.server.authentication.event.AuthenticationEvent;
34 import org.sonar.server.authentication.event.AuthenticationException;
35
36 import static java.lang.String.format;
37 import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
38 import static org.assertj.core.api.Assertions.assertThat;
39 import static org.assertj.core.api.Assertions.assertThatThrownBy;
40 import static org.sonar.db.user.UserTesting.newUserDto;
41 import static org.sonar.server.authentication.CredentialsLocalAuthentication.HashMethod.BCRYPT;
42 import static org.sonar.server.authentication.CredentialsLocalAuthentication.HashMethod.PBKDF2;
43 import static org.sonar.server.authentication.CredentialsLocalAuthentication.HashMethod.SHA1;
44
45 public class CredentialsLocalAuthenticationTest {
46   @Rule
47   public DbTester db = DbTester.create();
48
49   private static final Random RANDOM = new Random();
50   private static final MapSettings settings = new MapSettings();
51
52   private CredentialsLocalAuthentication underTest = new CredentialsLocalAuthentication(db.getDbClient(), settings.asConfig());
53
54   @Before
55   public void setup() {
56     settings.setProperty("sonar.internal.pbkdf2.iterations", 1);
57   }
58
59   @Test
60   public void incorrect_hash_should_throw_AuthenticationException() {
61     DbSession dbSession = db.getSession();
62     UserDto user = newUserDto()
63       .setHashMethod("ALGON2");
64
65     assertThatThrownBy(() -> underTest.authenticate(dbSession, user, "whatever", AuthenticationEvent.Method.BASIC))
66       .isInstanceOf(AuthenticationException.class)
67       .hasMessage(format(CredentialsLocalAuthentication.ERROR_UNKNOWN_HASH_METHOD, "ALGON2"));
68   }
69
70   @Test
71   public void null_hash_should_throw_AuthenticationException() {
72     DbSession dbSession = db.getSession();
73     UserDto user = newUserDto();
74
75     assertThatThrownBy(() -> underTest.authenticate(dbSession, user, "whatever", AuthenticationEvent.Method.BASIC))
76       .isInstanceOf(AuthenticationException.class)
77       .hasMessage(CredentialsLocalAuthentication.ERROR_NULL_HASH_METHOD);
78   }
79
80   @Test
81   public void authentication_with_bcrypt_with_correct_password_should_work() {
82     String password = randomAlphanumeric(60);
83
84     UserDto user = newUserDto()
85       .setHashMethod(BCRYPT.name())
86       .setCryptedPassword(BCrypt.hashpw(password, BCrypt.gensalt(12)));
87
88     underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
89   }
90
91   @Test
92   public void authentication_with_sha1_with_correct_password_should_work() {
93     String password = randomAlphanumeric(60);
94
95     byte[] saltRandom = new byte[20];
96     RANDOM.nextBytes(saltRandom);
97     String salt = DigestUtils.sha1Hex(saltRandom);
98
99     UserDto user = newUserDto()
100       .setHashMethod(SHA1.name())
101       .setCryptedPassword(DigestUtils.sha1Hex("--" + salt + "--" + password + "--"))
102       .setSalt(salt);
103
104     underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
105   }
106
107   @Test
108   public void authentication_with_sha1_with_incorrect_password_should_throw_AuthenticationException() {
109     String password = randomAlphanumeric(60);
110     DbSession dbSession = db.getSession();
111
112     byte[] saltRandom = new byte[20];
113     RANDOM.nextBytes(saltRandom);
114     String salt = DigestUtils.sha1Hex(saltRandom);
115
116     UserDto user = newUserDto()
117       .setHashMethod(SHA1.name())
118       .setCryptedPassword(DigestUtils.sha1Hex("--" + salt + "--" + password + "--"))
119       .setSalt(salt);
120
121     assertThatThrownBy(() -> underTest.authenticate(dbSession, user, "WHATEVER", AuthenticationEvent.Method.BASIC))
122       .isInstanceOf(AuthenticationException.class)
123       .hasMessage(CredentialsLocalAuthentication.ERROR_WRONG_PASSWORD);
124   }
125
126   @Test
127   public void authentication_with_sha1_with_empty_password_should_throw_AuthenticationException() {
128     DbSession dbSession = db.getSession();
129     byte[] saltRandom = new byte[20];
130     RANDOM.nextBytes(saltRandom);
131     String salt = DigestUtils.sha1Hex(saltRandom);
132
133     UserDto user = newUserDto()
134       .setCryptedPassword(null)
135       .setHashMethod(SHA1.name())
136       .setSalt(salt);
137
138     assertThatThrownBy(() -> underTest.authenticate(dbSession, user, "WHATEVER", AuthenticationEvent.Method.BASIC))
139       .isInstanceOf(AuthenticationException.class)
140       .hasMessage(CredentialsLocalAuthentication.ERROR_NULL_PASSWORD_IN_DB);
141   }
142
143   @Test
144   public void authentication_with_sha1_with_empty_salt_should_throw_AuthenticationException() {
145     DbSession dbSession = db.getSession();
146     String password = randomAlphanumeric(60);
147
148     UserDto user = newUserDto()
149       .setHashMethod(SHA1.name())
150       .setCryptedPassword(DigestUtils.sha1Hex("--0242b0b4c0a93ddfe09dd886de50bc25ba000b51--" + password + "--"))
151       .setSalt(null);
152
153     assertThatThrownBy(() -> underTest.authenticate(dbSession, user, "WHATEVER", AuthenticationEvent.Method.BASIC))
154       .isInstanceOf(AuthenticationException.class)
155       .hasMessage(CredentialsLocalAuthentication.ERROR_NULL_SALT);
156   }
157
158   @Test
159   public void authentication_with_bcrypt_with_incorrect_password_should_throw_AuthenticationException() {
160     DbSession dbSession = db.getSession();
161     String password = randomAlphanumeric(60);
162
163     UserDto user = newUserDto()
164       .setHashMethod(BCRYPT.name())
165       .setCryptedPassword(BCrypt.hashpw(password, BCrypt.gensalt(12)));
166
167     assertThatThrownBy(() -> underTest.authenticate(dbSession, user, "WHATEVER", AuthenticationEvent.Method.BASIC))
168       .isInstanceOf(AuthenticationException.class)
169       .hasMessage(CredentialsLocalAuthentication.ERROR_WRONG_PASSWORD);
170   }
171
172   @Test
173   public void authentication_with_bcrypt_with_empty_password_should_throw_AuthenticationException() {
174     DbSession dbSession = db.getSession();
175     UserDto user = newUserDto()
176       .setCryptedPassword(null)
177       .setHashMethod(BCRYPT.name());
178
179     assertThatThrownBy(() -> underTest.authenticate(dbSession, user, "WHATEVER", AuthenticationEvent.Method.BASIC))
180       .isInstanceOf(AuthenticationException.class)
181       .hasMessage(CredentialsLocalAuthentication.ERROR_NULL_PASSWORD_IN_DB);
182   }
183
184   @Test
185   public void authentication_upgrade_hash_function_when_SHA1_was_used() {
186     String password = randomAlphanumeric(60);
187
188     byte[] saltRandom = new byte[20];
189     RANDOM.nextBytes(saltRandom);
190     String salt = DigestUtils.sha1Hex(saltRandom);
191
192     UserDto user = newUserDto()
193       .setLogin("myself")
194       .setHashMethod(SHA1.name())
195       .setCryptedPassword(DigestUtils.sha1Hex("--" + salt + "--" + password + "--"))
196       .setSalt(salt);
197     db.users().insertUser(user);
198
199     underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
200
201     Optional<UserDto> myself = db.users().selectUserByLogin("myself");
202     assertThat(myself).isPresent();
203     assertThat(myself.get().getHashMethod()).isEqualTo(PBKDF2.name());
204     assertThat(myself.get().getSalt()).isNotNull();
205
206     // authentication must work with upgraded hash method
207     underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
208   }
209
210   @Test
211   public void authentication_upgrade_hash_function_when_BCRYPT_was_used() {
212     String password = randomAlphanumeric(60);
213
214     byte[] saltRandom = new byte[20];
215     RANDOM.nextBytes(saltRandom);
216     String salt = DigestUtils.sha1Hex(saltRandom);
217
218     UserDto user = newUserDto()
219       .setLogin("myself")
220       .setHashMethod(BCRYPT.name())
221       .setCryptedPassword(BCrypt.hashpw(password, BCrypt.gensalt(12)))
222       .setSalt(salt);
223     db.users().insertUser(user);
224
225     underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
226
227     Optional<UserDto> myself = db.users().selectUserByLogin("myself");
228     assertThat(myself).isPresent();
229     assertThat(myself.get().getHashMethod()).isEqualTo(PBKDF2.name());
230     assertThat(myself.get().getSalt()).isNotNull();
231
232     // authentication must work with upgraded hash method
233     underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
234   }
235
236   @Test
237   public void authentication_updates_db_if_PBKDF2_iterations_changes() {
238     String password = randomAlphanumeric(60);
239
240     UserDto user = newUserDto().setLogin("myself");
241     db.users().insertUser(user);
242     underTest.storeHashPassword(user, password);
243
244     underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
245     assertThat(user.getCryptedPassword()).startsWith("1$");
246
247     settings.setProperty("sonar.internal.pbkdf2.iterations", 3);
248     CredentialsLocalAuthentication underTest = new CredentialsLocalAuthentication(db.getDbClient(), settings.asConfig());
249
250     underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
251     assertThat(user.getCryptedPassword()).startsWith("3$");
252     underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
253   }
254
255   @Test
256   public void authentication_with_pbkdf2_with_correct_password_should_work() {
257     String password = randomAlphanumeric(60);
258     UserDto user = newUserDto()
259       .setHashMethod(PBKDF2.name());
260
261     underTest.storeHashPassword(user, password);
262     assertThat(user.getCryptedPassword()).hasSize(88 + 2);
263     assertThat(user.getCryptedPassword()).startsWith("1$");
264     assertThat(user.getSalt()).hasSize(28);
265
266     underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
267   }
268
269   @Test
270   public void authentication_with_pbkdf2_with_default_number_of_iterations() {
271     settings.clear();
272     CredentialsLocalAuthentication underTest = new CredentialsLocalAuthentication(db.getDbClient(), settings.asConfig());
273
274     String password = randomAlphanumeric(60);
275     UserDto user = newUserDto()
276       .setHashMethod(PBKDF2.name());
277
278     underTest.storeHashPassword(user, password);
279     assertThat(user.getCryptedPassword()).hasSize(88 + 7);
280     assertThat(user.getCryptedPassword()).startsWith("100000$");
281     assertThat(user.getSalt()).hasSize(28);
282
283     underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
284   }
285
286   @Test
287   public void authentication_with_pbkdf2_with_incorrect_password_should_throw_AuthenticationException() {
288     DbSession dbSession = db.getSession();
289     UserDto user = newUserDto()
290       .setHashMethod(PBKDF2.name())
291       .setCryptedPassword("1$hash")
292       .setSalt("salt");
293
294     assertThatThrownBy(() -> underTest.authenticate(dbSession, user, "WHATEVER", AuthenticationEvent.Method.BASIC))
295       .isInstanceOf(AuthenticationException.class)
296       .hasMessage(CredentialsLocalAuthentication.ERROR_WRONG_PASSWORD);
297   }
298
299   @Test
300   public void authentication_with_pbkdf2_with_invalid_password_should_throw_AuthenticationException() {
301     DbSession dbSession = db.getSession();
302     String password = randomAlphanumeric(60);
303
304     byte[] saltRandom = new byte[20];
305     RANDOM.nextBytes(saltRandom);
306     String salt = DigestUtils.sha1Hex(saltRandom);
307
308     UserDto userInvalidHash = newUserDto()
309       .setHashMethod(PBKDF2.name())
310       .setCryptedPassword(DigestUtils.sha1Hex("--" + salt + "--" + password + "--"))
311       .setSalt(salt);
312
313     assertThatThrownBy(() -> underTest.authenticate(dbSession, userInvalidHash, "WHATEVER", AuthenticationEvent.Method.BASIC))
314       .isInstanceOf(AuthenticationException.class)
315       .hasMessage("invalid hash stored");
316
317     UserDto userInvalidIterations = newUserDto()
318       .setHashMethod(PBKDF2.name())
319       .setCryptedPassword("a$")
320       .setSalt(salt);
321
322     assertThatThrownBy(() -> underTest.authenticate(dbSession, userInvalidIterations, "WHATEVER", AuthenticationEvent.Method.BASIC))
323       .isInstanceOf(AuthenticationException.class)
324       .hasMessage("invalid hash stored");
325   }
326
327   @Test
328   public void authentication_with_pbkdf2_with_empty_password_should_throw_AuthenticationException() {
329     byte[] saltRandom = new byte[20];
330     RANDOM.nextBytes(saltRandom);
331     String salt = DigestUtils.sha1Hex(saltRandom);
332     DbSession dbSession = db.getSession();
333
334     UserDto user = newUserDto()
335       .setCryptedPassword(null)
336       .setHashMethod(PBKDF2.name())
337       .setSalt(salt);
338
339     assertThatThrownBy(() -> underTest.authenticate(dbSession, user, "WHATEVER", AuthenticationEvent.Method.BASIC))
340       .isInstanceOf(AuthenticationException.class)
341       .hasMessage(CredentialsLocalAuthentication.ERROR_NULL_PASSWORD_IN_DB);
342   }
343
344   @Test
345   public void authentication_with_pbkdf2_with_empty_salt_should_throw_AuthenticationException() {
346     String password = randomAlphanumeric(60);
347     DbSession dbSession = db.getSession();
348
349     UserDto user = newUserDto()
350       .setHashMethod(PBKDF2.name())
351       .setCryptedPassword(DigestUtils.sha1Hex("--0242b0b4c0a93ddfe09dd886de50bc25ba000b51--" + password + "--"))
352       .setSalt(null);
353
354     assertThatThrownBy(() -> underTest.authenticate(dbSession, user, "WHATEVER", AuthenticationEvent.Method.BASIC))
355       .isInstanceOf(AuthenticationException.class)
356       .hasMessage(CredentialsLocalAuthentication.ERROR_NULL_SALT);
357   }
358 }