3 * Copyright (C) 2009-2022 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.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;
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;
45 public class CredentialsLocalAuthenticationTest {
47 public DbTester db = DbTester.create();
49 private static final Random RANDOM = new Random();
50 private static final MapSettings settings = new MapSettings();
52 private CredentialsLocalAuthentication underTest = new CredentialsLocalAuthentication(db.getDbClient(), settings.asConfig());
56 settings.setProperty("sonar.internal.pbkdf2.iterations", 1);
60 public void incorrect_hash_should_throw_AuthenticationException() {
61 DbSession dbSession = db.getSession();
62 UserDto user = newUserDto()
63 .setHashMethod("ALGON2");
65 assertThatThrownBy(() -> underTest.authenticate(dbSession, user, "whatever", AuthenticationEvent.Method.BASIC))
66 .isInstanceOf(AuthenticationException.class)
67 .hasMessage(format(CredentialsLocalAuthentication.ERROR_UNKNOWN_HASH_METHOD, "ALGON2"));
71 public void null_hash_should_throw_AuthenticationException() {
72 DbSession dbSession = db.getSession();
73 UserDto user = newUserDto();
75 assertThatThrownBy(() -> underTest.authenticate(dbSession, user, "whatever", AuthenticationEvent.Method.BASIC))
76 .isInstanceOf(AuthenticationException.class)
77 .hasMessage(CredentialsLocalAuthentication.ERROR_NULL_HASH_METHOD);
81 public void authentication_with_bcrypt_with_correct_password_should_work() {
82 String password = randomAlphanumeric(60);
84 UserDto user = newUserDto()
85 .setHashMethod(BCRYPT.name())
86 .setCryptedPassword(BCrypt.hashpw(password, BCrypt.gensalt(12)));
88 underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
92 public void authentication_with_sha1_with_correct_password_should_work() {
93 String password = randomAlphanumeric(60);
95 byte[] saltRandom = new byte[20];
96 RANDOM.nextBytes(saltRandom);
97 String salt = DigestUtils.sha1Hex(saltRandom);
99 UserDto user = newUserDto()
100 .setHashMethod(SHA1.name())
101 .setCryptedPassword(DigestUtils.sha1Hex("--" + salt + "--" + password + "--"))
104 underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
108 public void authentication_with_sha1_with_incorrect_password_should_throw_AuthenticationException() {
109 String password = randomAlphanumeric(60);
110 DbSession dbSession = db.getSession();
112 byte[] saltRandom = new byte[20];
113 RANDOM.nextBytes(saltRandom);
114 String salt = DigestUtils.sha1Hex(saltRandom);
116 UserDto user = newUserDto()
117 .setHashMethod(SHA1.name())
118 .setCryptedPassword(DigestUtils.sha1Hex("--" + salt + "--" + password + "--"))
121 assertThatThrownBy(() -> underTest.authenticate(dbSession, user, "WHATEVER", AuthenticationEvent.Method.BASIC))
122 .isInstanceOf(AuthenticationException.class)
123 .hasMessage(CredentialsLocalAuthentication.ERROR_WRONG_PASSWORD);
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);
133 UserDto user = newUserDto()
134 .setCryptedPassword(null)
135 .setHashMethod(SHA1.name())
138 assertThatThrownBy(() -> underTest.authenticate(dbSession, user, "WHATEVER", AuthenticationEvent.Method.BASIC))
139 .isInstanceOf(AuthenticationException.class)
140 .hasMessage(CredentialsLocalAuthentication.ERROR_NULL_PASSWORD_IN_DB);
144 public void authentication_with_sha1_with_empty_salt_should_throw_AuthenticationException() {
145 DbSession dbSession = db.getSession();
146 String password = randomAlphanumeric(60);
148 UserDto user = newUserDto()
149 .setHashMethod(SHA1.name())
150 .setCryptedPassword(DigestUtils.sha1Hex("--0242b0b4c0a93ddfe09dd886de50bc25ba000b51--" + password + "--"))
153 assertThatThrownBy(() -> underTest.authenticate(dbSession, user, "WHATEVER", AuthenticationEvent.Method.BASIC))
154 .isInstanceOf(AuthenticationException.class)
155 .hasMessage(CredentialsLocalAuthentication.ERROR_NULL_SALT);
159 public void authentication_with_bcrypt_with_incorrect_password_should_throw_AuthenticationException() {
160 DbSession dbSession = db.getSession();
161 String password = randomAlphanumeric(60);
163 UserDto user = newUserDto()
164 .setHashMethod(BCRYPT.name())
165 .setCryptedPassword(BCrypt.hashpw(password, BCrypt.gensalt(12)));
167 assertThatThrownBy(() -> underTest.authenticate(dbSession, user, "WHATEVER", AuthenticationEvent.Method.BASIC))
168 .isInstanceOf(AuthenticationException.class)
169 .hasMessage(CredentialsLocalAuthentication.ERROR_WRONG_PASSWORD);
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());
179 assertThatThrownBy(() -> underTest.authenticate(dbSession, user, "WHATEVER", AuthenticationEvent.Method.BASIC))
180 .isInstanceOf(AuthenticationException.class)
181 .hasMessage(CredentialsLocalAuthentication.ERROR_NULL_PASSWORD_IN_DB);
185 public void authentication_upgrade_hash_function_when_SHA1_was_used() {
186 String password = randomAlphanumeric(60);
188 byte[] saltRandom = new byte[20];
189 RANDOM.nextBytes(saltRandom);
190 String salt = DigestUtils.sha1Hex(saltRandom);
192 UserDto user = newUserDto()
194 .setHashMethod(SHA1.name())
195 .setCryptedPassword(DigestUtils.sha1Hex("--" + salt + "--" + password + "--"))
197 db.users().insertUser(user);
199 underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
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();
206 // authentication must work with upgraded hash method
207 underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
211 public void authentication_upgrade_hash_function_when_BCRYPT_was_used() {
212 String password = randomAlphanumeric(60);
214 byte[] saltRandom = new byte[20];
215 RANDOM.nextBytes(saltRandom);
216 String salt = DigestUtils.sha1Hex(saltRandom);
218 UserDto user = newUserDto()
220 .setHashMethod(BCRYPT.name())
221 .setCryptedPassword(BCrypt.hashpw(password, BCrypt.gensalt(12)))
223 db.users().insertUser(user);
225 underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
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();
232 // authentication must work with upgraded hash method
233 underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
237 public void authentication_updates_db_if_PBKDF2_iterations_changes() {
238 String password = randomAlphanumeric(60);
240 UserDto user = newUserDto().setLogin("myself");
241 db.users().insertUser(user);
242 underTest.storeHashPassword(user, password);
244 underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
245 assertThat(user.getCryptedPassword()).startsWith("1$");
247 settings.setProperty("sonar.internal.pbkdf2.iterations", 3);
248 CredentialsLocalAuthentication underTest = new CredentialsLocalAuthentication(db.getDbClient(), settings.asConfig());
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);
256 public void authentication_with_pbkdf2_with_correct_password_should_work() {
257 String password = randomAlphanumeric(60);
258 UserDto user = newUserDto()
259 .setHashMethod(PBKDF2.name());
261 underTest.storeHashPassword(user, password);
262 assertThat(user.getCryptedPassword()).hasSize(88 + 2);
263 assertThat(user.getCryptedPassword()).startsWith("1$");
264 assertThat(user.getSalt()).hasSize(28);
266 underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
270 public void authentication_with_pbkdf2_with_default_number_of_iterations() {
272 CredentialsLocalAuthentication underTest = new CredentialsLocalAuthentication(db.getDbClient(), settings.asConfig());
274 String password = randomAlphanumeric(60);
275 UserDto user = newUserDto()
276 .setHashMethod(PBKDF2.name());
278 underTest.storeHashPassword(user, password);
279 assertThat(user.getCryptedPassword()).hasSize(88 + 7);
280 assertThat(user.getCryptedPassword()).startsWith("100000$");
281 assertThat(user.getSalt()).hasSize(28);
283 underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
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")
294 assertThatThrownBy(() -> underTest.authenticate(dbSession, user, "WHATEVER", AuthenticationEvent.Method.BASIC))
295 .isInstanceOf(AuthenticationException.class)
296 .hasMessage(CredentialsLocalAuthentication.ERROR_WRONG_PASSWORD);
300 public void authentication_with_pbkdf2_with_invalid_password_should_throw_AuthenticationException() {
301 DbSession dbSession = db.getSession();
302 String password = randomAlphanumeric(60);
304 byte[] saltRandom = new byte[20];
305 RANDOM.nextBytes(saltRandom);
306 String salt = DigestUtils.sha1Hex(saltRandom);
308 UserDto userInvalidHash = newUserDto()
309 .setHashMethod(PBKDF2.name())
310 .setCryptedPassword(DigestUtils.sha1Hex("--" + salt + "--" + password + "--"))
313 assertThatThrownBy(() -> underTest.authenticate(dbSession, userInvalidHash, "WHATEVER", AuthenticationEvent.Method.BASIC))
314 .isInstanceOf(AuthenticationException.class)
315 .hasMessage("invalid hash stored");
317 UserDto userInvalidIterations = newUserDto()
318 .setHashMethod(PBKDF2.name())
319 .setCryptedPassword("a$")
322 assertThatThrownBy(() -> underTest.authenticate(dbSession, userInvalidIterations, "WHATEVER", AuthenticationEvent.Method.BASIC))
323 .isInstanceOf(AuthenticationException.class)
324 .hasMessage("invalid hash stored");
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();
334 UserDto user = newUserDto()
335 .setCryptedPassword(null)
336 .setHashMethod(PBKDF2.name())
339 assertThatThrownBy(() -> underTest.authenticate(dbSession, user, "WHATEVER", AuthenticationEvent.Method.BASIC))
340 .isInstanceOf(AuthenticationException.class)
341 .hasMessage(CredentialsLocalAuthentication.ERROR_NULL_PASSWORD_IN_DB);
345 public void authentication_with_pbkdf2_with_empty_salt_should_throw_AuthenticationException() {
346 String password = randomAlphanumeric(60);
347 DbSession dbSession = db.getSession();
349 UserDto user = newUserDto()
350 .setHashMethod(PBKDF2.name())
351 .setCryptedPassword(DigestUtils.sha1Hex("--0242b0b4c0a93ddfe09dd886de50bc25ba000b51--" + password + "--"))
354 assertThatThrownBy(() -> underTest.authenticate(dbSession, user, "WHATEVER", AuthenticationEvent.Method.BASIC))
355 .isInstanceOf(AuthenticationException.class)
356 .hasMessage(CredentialsLocalAuthentication.ERROR_NULL_SALT);