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