3 * Copyright (C) 2009-2024 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.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;
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;
47 public class CredentialsLocalAuthenticationIT {
49 private static final SecureRandom SECURE_RANDOM = new SecureRandom();
50 private static final String PBKDF2_SALT = generatePBKDF2Salt();
53 public DbTester db = DbTester.create();
55 private static final Random RANDOM = new Random();
56 private static final MapSettings settings = new MapSettings();
58 private CredentialsLocalAuthentication underTest = new CredentialsLocalAuthentication(db.getDbClient(), settings.asConfig());
62 settings.setProperty("sonar.internal.pbkdf2.iterations", 1);
66 public void incorrect_hash_should_throw_AuthenticationException() {
67 DbSession dbSession = db.getSession();
68 UserDto user = newUserDto()
69 .setHashMethod("ALGON2");
71 assertThatThrownBy(() -> underTest.authenticate(dbSession, user, "whatever", AuthenticationEvent.Method.BASIC))
72 .isInstanceOf(AuthenticationException.class)
73 .hasMessage(format(CredentialsLocalAuthentication.ERROR_UNKNOWN_HASH_METHOD, "ALGON2"));
77 public void null_hash_should_throw_AuthenticationException() {
78 DbSession dbSession = db.getSession();
79 UserDto user = newUserDto();
81 assertThatThrownBy(() -> underTest.authenticate(dbSession, user, "whatever", AuthenticationEvent.Method.BASIC))
82 .isInstanceOf(AuthenticationException.class)
83 .hasMessage(CredentialsLocalAuthentication.ERROR_NULL_HASH_METHOD);
87 public void authentication_with_bcrypt_with_correct_password_should_work() {
88 String password = randomAlphanumeric(60);
90 UserDto user = newUserDto()
91 .setHashMethod(BCRYPT.name())
92 .setCryptedPassword(BCrypt.hashpw(password, BCrypt.gensalt(12)));
94 underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
98 public void authentication_with_sha1_should_throw_AuthenticationException() {
99 String password = randomAlphanumeric(60);
101 byte[] saltRandom = new byte[20];
102 RANDOM.nextBytes(saltRandom);
103 String salt = DigestUtils.sha1Hex(saltRandom);
105 UserDto user = newUserDto()
106 .setHashMethod("SHA1")
107 .setCryptedPassword(DigestUtils.sha1Hex("--" + salt + "--" + password + "--"))
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]");
117 public void authentication_with_bcrypt_with_incorrect_password_should_throw_AuthenticationException() {
118 DbSession dbSession = db.getSession();
119 String password = randomAlphanumeric(60);
121 UserDto user = newUserDto()
122 .setHashMethod(BCRYPT.name())
123 .setCryptedPassword(BCrypt.hashpw(password, BCrypt.gensalt(12)));
125 assertThatThrownBy(() -> underTest.authenticate(dbSession, user, "WHATEVER", AuthenticationEvent.Method.BASIC))
126 .isInstanceOf(AuthenticationException.class)
127 .hasMessage(CredentialsLocalAuthentication.ERROR_WRONG_PASSWORD);
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());
137 assertThatThrownBy(() -> underTest.authenticate(dbSession, user, "WHATEVER", AuthenticationEvent.Method.BASIC))
138 .isInstanceOf(AuthenticationException.class)
139 .hasMessage(CredentialsLocalAuthentication.ERROR_NULL_PASSWORD_IN_DB);
143 public void authentication_upgrade_hash_function_when_BCRYPT_was_used() {
144 String password = randomAlphanumeric(60);
146 UserDto user = newUserDto()
148 .setHashMethod(BCRYPT.name())
149 .setCryptedPassword(BCrypt.hashpw(password, BCrypt.gensalt(12)))
151 db.users().insertUser(user);
153 underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
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();
160 // authentication must work with upgraded hash method
161 underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
165 public void authentication_updates_db_if_PBKDF2_iterations_changes() {
166 String password = randomAlphanumeric(60);
168 UserDto user = newUserDto().setLogin("myself");
169 db.users().insertUser(user);
170 underTest.storeHashPassword(user, password);
172 underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
173 assertThat(user.getCryptedPassword()).startsWith("1$");
175 settings.setProperty("sonar.internal.pbkdf2.iterations", 3);
176 CredentialsLocalAuthentication underTest = new CredentialsLocalAuthentication(db.getDbClient(), settings.asConfig());
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);
184 public void authentication_with_pbkdf2_with_correct_password_should_work() {
185 String password = randomAlphanumeric(60);
186 UserDto user = newUserDto()
187 .setHashMethod(PBKDF2.name());
189 underTest.storeHashPassword(user, password);
190 assertThat(user.getCryptedPassword()).hasSize(88 + 2);
191 assertThat(user.getCryptedPassword()).startsWith("1$");
192 assertThat(user.getSalt()).hasSize(28);
194 underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
198 public void authentication_with_pbkdf2_with_default_number_of_iterations() {
200 CredentialsLocalAuthentication underTest = new CredentialsLocalAuthentication(db.getDbClient(), settings.asConfig());
202 String password = randomAlphanumeric(60);
203 UserDto user = newUserDto()
204 .setHashMethod(PBKDF2.name());
206 underTest.storeHashPassword(user, password);
207 assertThat(user.getCryptedPassword()).hasSize(88 + 7);
208 assertThat(user.getCryptedPassword()).startsWith("100000$");
209 assertThat(user.getSalt()).hasSize(28);
211 underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
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")
222 assertThatThrownBy(() -> underTest.authenticate(dbSession, user, "WHATEVER", AuthenticationEvent.Method.BASIC))
223 .isInstanceOf(AuthenticationException.class)
224 .hasMessage(CredentialsLocalAuthentication.ERROR_WRONG_PASSWORD);
228 public void authentication_with_pbkdf2_with_invalid_hash_should_throw_AuthenticationException() {
229 DbSession dbSession = db.getSession();
230 String password = randomAlphanumeric(60);
232 UserDto userInvalidHash = newUserDto()
233 .setHashMethod(PBKDF2.name())
234 .setCryptedPassword(password)
235 .setSalt(PBKDF2_SALT);
237 assertThatThrownBy(() -> underTest.authenticate(dbSession, userInvalidHash, password, AuthenticationEvent.Method.BASIC))
238 .isInstanceOf(AuthenticationException.class)
239 .hasMessage("invalid hash stored");
241 UserDto userInvalidIterations = newUserDto()
242 .setHashMethod(PBKDF2.name())
243 .setCryptedPassword("a$" + password)
244 .setSalt(PBKDF2_SALT);
246 assertThatThrownBy(() -> underTest.authenticate(dbSession, userInvalidIterations, password, AuthenticationEvent.Method.BASIC))
247 .isInstanceOf(AuthenticationException.class)
248 .hasMessage("invalid hash stored");
252 public void authentication_with_pbkdf2_with_empty_password_should_throw_AuthenticationException() {
253 DbSession dbSession = db.getSession();
255 UserDto user = newUserDto()
256 .setCryptedPassword(null)
257 .setHashMethod(PBKDF2.name())
258 .setSalt(PBKDF2_SALT);
260 assertThatThrownBy(() -> underTest.authenticate(dbSession, user, "WHATEVER", AuthenticationEvent.Method.BASIC))
261 .isInstanceOf(AuthenticationException.class)
262 .hasMessage(CredentialsLocalAuthentication.ERROR_NULL_PASSWORD_IN_DB);
266 public void authentication_with_pbkdf2_with_empty_salt_should_throw_AuthenticationException() {
267 String password = randomAlphanumeric(60);
268 DbSession dbSession = db.getSession();
270 UserDto user = newUserDto()
271 .setHashMethod(PBKDF2.name())
272 .setCryptedPassword("1$" + password)
275 assertThatThrownBy(() -> underTest.authenticate(dbSession, user, password, AuthenticationEvent.Method.BASIC))
276 .isInstanceOf(AuthenticationException.class)
277 .hasMessage(CredentialsLocalAuthentication.ERROR_NULL_SALT);
280 private static String generatePBKDF2Salt() {
281 byte[] salt = new byte[20];
282 SECURE_RANDOM.nextBytes(salt);
283 String saltStr = Base64.getEncoder().encodeToString(salt);