3 * Copyright (C) 2009-2021 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.junit.rules.ExpectedException;
29 import org.mindrot.jbcrypt.BCrypt;
30 import org.sonar.api.config.internal.MapSettings;
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 org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
37 import static org.assertj.core.api.Assertions.assertThat;
38 import static org.sonar.db.user.UserTesting.newUserDto;
39 import static org.sonar.server.authentication.CredentialsLocalAuthentication.HashMethod.BCRYPT;
40 import static org.sonar.server.authentication.CredentialsLocalAuthentication.HashMethod.PBKDF2;
41 import static org.sonar.server.authentication.CredentialsLocalAuthentication.HashMethod.SHA1;
43 public class CredentialsLocalAuthenticationTest {
45 public ExpectedException expectedException = ExpectedException.none();
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 UserDto user = newUserDto()
62 .setHashMethod("ALGON2");
64 expectedException.expect(AuthenticationException.class);
65 expectedException.expectMessage("Unknown hash method [ALGON2]");
67 underTest.authenticate(db.getSession(), user, "whatever", AuthenticationEvent.Method.BASIC);
71 public void null_hash_should_throw_AuthenticationException() {
72 UserDto user = newUserDto();
74 expectedException.expect(AuthenticationException.class);
75 expectedException.expectMessage("null hash method");
77 underTest.authenticate(db.getSession(), user, "whatever", AuthenticationEvent.Method.BASIC);
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);
111 byte[] saltRandom = new byte[20];
112 RANDOM.nextBytes(saltRandom);
113 String salt = DigestUtils.sha1Hex(saltRandom);
115 UserDto user = newUserDto()
116 .setHashMethod(SHA1.name())
117 .setCryptedPassword(DigestUtils.sha1Hex("--" + salt + "--" + password + "--"))
120 expectedException.expect(AuthenticationException.class);
121 expectedException.expectMessage("wrong password");
123 underTest.authenticate(db.getSession(), user, "WHATEVER", AuthenticationEvent.Method.BASIC);
127 public void authentication_with_sha1_with_empty_password_should_throw_AuthenticationException() {
128 byte[] saltRandom = new byte[20];
129 RANDOM.nextBytes(saltRandom);
130 String salt = DigestUtils.sha1Hex(saltRandom);
132 UserDto user = newUserDto()
133 .setCryptedPassword(null)
134 .setHashMethod(SHA1.name())
137 expectedException.expect(AuthenticationException.class);
138 expectedException.expectMessage("null password in DB");
140 underTest.authenticate(db.getSession(), user, "WHATEVER", AuthenticationEvent.Method.BASIC);
144 public void authentication_with_sha1_with_empty_salt_should_throw_AuthenticationException() {
145 String password = randomAlphanumeric(60);
147 UserDto user = newUserDto()
148 .setHashMethod(SHA1.name())
149 .setCryptedPassword(DigestUtils.sha1Hex("--0242b0b4c0a93ddfe09dd886de50bc25ba000b51--" + password + "--"))
152 expectedException.expect(AuthenticationException.class);
153 expectedException.expectMessage("null salt");
155 underTest.authenticate(db.getSession(), user, "WHATEVER", AuthenticationEvent.Method.BASIC);
159 public void authentication_with_bcrypt_with_incorrect_password_should_throw_AuthenticationException() {
160 String password = randomAlphanumeric(60);
162 UserDto user = newUserDto()
163 .setHashMethod(BCRYPT.name())
164 .setCryptedPassword(BCrypt.hashpw(password, BCrypt.gensalt(12)));
166 expectedException.expect(AuthenticationException.class);
167 expectedException.expectMessage("wrong password");
169 underTest.authenticate(db.getSession(), user, "WHATEVER", AuthenticationEvent.Method.BASIC);
173 public void authentication_upgrade_hash_function_when_SHA1_was_used() {
174 String password = randomAlphanumeric(60);
176 byte[] saltRandom = new byte[20];
177 RANDOM.nextBytes(saltRandom);
178 String salt = DigestUtils.sha1Hex(saltRandom);
180 UserDto user = newUserDto()
182 .setHashMethod(SHA1.name())
183 .setCryptedPassword(DigestUtils.sha1Hex("--" + salt + "--" + password + "--"))
185 db.users().insertUser(user);
187 underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
189 Optional<UserDto> myself = db.users().selectUserByLogin("myself");
190 assertThat(myself).isPresent();
191 assertThat(myself.get().getHashMethod()).isEqualTo(PBKDF2.name());
192 assertThat(myself.get().getSalt()).isNotNull();
194 // authentication must work with upgraded hash method
195 underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
199 public void authentication_upgrade_hash_function_when_BCRYPT_was_used() {
200 String password = randomAlphanumeric(60);
202 byte[] saltRandom = new byte[20];
203 RANDOM.nextBytes(saltRandom);
204 String salt = DigestUtils.sha1Hex(saltRandom);
206 UserDto user = newUserDto()
208 .setHashMethod(BCRYPT.name())
209 .setCryptedPassword(BCrypt.hashpw(password, BCrypt.gensalt(12)))
211 db.users().insertUser(user);
213 underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
215 Optional<UserDto> myself = db.users().selectUserByLogin("myself");
216 assertThat(myself).isPresent();
217 assertThat(myself.get().getHashMethod()).isEqualTo(PBKDF2.name());
218 assertThat(myself.get().getSalt()).isNotNull();
220 // authentication must work with upgraded hash method
221 underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
225 public void authentication_updates_db_if_PBKDF2_iterations_changes() {
226 String password = randomAlphanumeric(60);
228 UserDto user = newUserDto().setLogin("myself");
229 db.users().insertUser(user);
230 underTest.storeHashPassword(user, password);
232 underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
233 assertThat(user.getCryptedPassword()).startsWith("1$");
235 settings.setProperty("sonar.internal.pbkdf2.iterations", 3);
236 CredentialsLocalAuthentication underTest = new CredentialsLocalAuthentication(db.getDbClient(), settings.asConfig());
238 underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
239 assertThat(user.getCryptedPassword()).startsWith("3$");
240 underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
244 public void authentication_with_pbkdf2_with_correct_password_should_work() {
245 String password = randomAlphanumeric(60);
246 UserDto user = newUserDto()
247 .setHashMethod(PBKDF2.name());
249 underTest.storeHashPassword(user, password);
250 assertThat(user.getCryptedPassword()).hasSize(88 + 2);
251 assertThat(user.getCryptedPassword()).startsWith("1$");
252 assertThat(user.getSalt()).hasSize(28);
254 underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
258 public void authentication_with_pbkdf2_with_default_number_of_iterations() {
260 CredentialsLocalAuthentication underTest = new CredentialsLocalAuthentication(db.getDbClient(), settings.asConfig());
262 String password = randomAlphanumeric(60);
263 UserDto user = newUserDto()
264 .setHashMethod(PBKDF2.name());
266 underTest.storeHashPassword(user, password);
267 assertThat(user.getCryptedPassword()).hasSize(88 + 7);
268 assertThat(user.getCryptedPassword()).startsWith("100000$");
269 assertThat(user.getSalt()).hasSize(28);
271 underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
275 public void authentication_with_pbkdf2_with_incorrect_password_should_throw_AuthenticationException() {
276 UserDto user = newUserDto()
277 .setHashMethod(PBKDF2.name())
278 .setCryptedPassword("1$hash")
281 expectedException.expect(AuthenticationException.class);
282 expectedException.expectMessage("wrong password");
284 underTest.authenticate(db.getSession(), user, "WHATEVER", AuthenticationEvent.Method.BASIC);
288 public void authentication_with_pbkdf2_with_invalid_password_should_throw_AuthenticationException() {
289 String password = randomAlphanumeric(60);
291 byte[] saltRandom = new byte[20];
292 RANDOM.nextBytes(saltRandom);
293 String salt = DigestUtils.sha1Hex(saltRandom);
295 UserDto user = newUserDto()
296 .setHashMethod(PBKDF2.name())
297 .setCryptedPassword(DigestUtils.sha1Hex("--" + salt + "--" + password + "--"))
300 expectedException.expect(AuthenticationException.class);
301 expectedException.expectMessage("invalid hash stored");
303 underTest.authenticate(db.getSession(), user, "WHATEVER", AuthenticationEvent.Method.BASIC);
307 public void authentication_with_pbkdf2_with_empty_password_should_throw_AuthenticationException() {
308 byte[] saltRandom = new byte[20];
309 RANDOM.nextBytes(saltRandom);
310 String salt = DigestUtils.sha1Hex(saltRandom);
312 UserDto user = newUserDto()
313 .setCryptedPassword(null)
314 .setHashMethod(PBKDF2.name())
317 expectedException.expect(AuthenticationException.class);
318 expectedException.expectMessage("null password in DB");
320 underTest.authenticate(db.getSession(), user, "WHATEVER", AuthenticationEvent.Method.BASIC);
324 public void authentication_with_pbkdf2_with_empty_salt_should_throw_AuthenticationException() {
325 String password = randomAlphanumeric(60);
327 UserDto user = newUserDto()
328 .setHashMethod(PBKDF2.name())
329 .setCryptedPassword(DigestUtils.sha1Hex("--0242b0b4c0a93ddfe09dd886de50bc25ba000b51--" + password + "--"))
332 expectedException.expect(AuthenticationException.class);
333 expectedException.expectMessage("null salt");
335 underTest.authenticate(db.getSession(), user, "WHATEVER", AuthenticationEvent.Method.BASIC);