private String externalIdentityProvider;
// Hashed password that may be null in case of external authentication
private String cryptedPassword;
- // Salt used for SHA1, null when bcrypt is used or for external authentication
+ // Salt used for PBKDF2, null when bcrypt is used or for external authentication
private String salt;
// Hash method used to generate cryptedPassword, my be null in case of external authentication
private String hashMethod;
.add(6302, "Drop unused Issues Column ACTION_PLAN_KEY", DropActionPlanKeyIssueColumn.class)
.add(6303, "Drop unused Issues Column ISSUE_ATTRIBUTES", DropIssuesAttributesIssueColumn.class)
.add(6304, "Create table 'SCANNER_ANALYSIS_CACHE", CreateScannerAnalysisCacheTable.class)
+ .add(6305, "Issue warning for users using SHA1 hash method", SelectUsersWithSha1HashMethod.class)
;
}
}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.version.v94;
+
+import java.sql.SQLException;
+import java.util.List;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.step.DataChange;
+import org.sonar.server.platform.db.migration.step.Select;
+
+public class SelectUsersWithSha1HashMethod extends DataChange {
+ private static final Logger LOG = Loggers.get(SelectUsersWithSha1HashMethod.class);
+
+ private static final String UNSUPPORTED_HASH_METHOD = "SHA1";
+
+ public SelectUsersWithSha1HashMethod(Database db) {
+ super(db);
+ }
+
+ @Override
+ protected void execute(Context context) throws SQLException {
+ Select select = context.prepareSelect("select login from users where hash_method = ?");
+ select.setString(1, UNSUPPORTED_HASH_METHOD);
+ List<String> logins = select.list(row -> row.getString(1));
+ if (!logins.isEmpty()) {
+ LOG.warn("The following local accounts have their password hashed with an algorithm which is not longer supported. "
+ + "They will not be able to login anymore. Please reset their password if the accounts need to be kept. {}", logins);
+ }
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.version.v94;
+
+import java.sql.SQLException;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.utils.log.LogAndArguments;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.core.util.UuidFactoryFast;
+import org.sonar.db.CoreDbTester;
+import org.sonar.server.platform.db.migration.step.DataChange;
+
+import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
+import static org.apache.commons.lang.RandomStringUtils.randomNumeric;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class SelectUsersWithSha1HashMethodTest {
+
+ private static final UuidFactory UUID_FACTORY = UuidFactoryFast.getInstance();
+
+ @Rule
+ public LogTester logTester = new LogTester();
+
+ @Rule
+ public CoreDbTester db = CoreDbTester.createForSchema(SelectUsersWithSha1HashMethodTest.class, "schema.sql");
+
+ private final DataChange underTest = new SelectUsersWithSha1HashMethod(db.database());
+
+ @Test
+ public void migration_ifSomeUsersUseSha1_shouldLogThem() throws SQLException {
+ String user1sha1 = insertUser("SHA1");
+ String user2sha1 = insertUser("SHA1");
+ insertUser(null);
+ insertUser("PBKDF2");
+ insertUser("BCRYPT");
+ insertUser("");
+
+ underTest.execute();
+
+ assertThat(logTester.getLogs(LoggerLevel.WARN))
+ .hasSize(1)
+ .first()
+ .extracting(LogAndArguments::getFormattedMsg)
+ .asString()
+ .startsWith("The following local accounts have their password hashed with an algorithm which is not longer supported. They will not be able to login anymore. "
+ + "Please reset their password if the accounts need to be kept.")
+ .contains(user1sha1, user2sha1);
+ }
+
+ @Test
+ public void migration_ifAllUsersAreNotUsingSha1_shouldNotLogAnything() throws SQLException {
+ insertUser(null);
+ insertUser("PBKDF2");
+ insertUser("BCRYPT");
+ insertUser("");
+
+ underTest.execute();
+
+ assertThat(logTester.getLogs()).isEmpty();
+ }
+
+ @Test
+ public void migration_should_be_reentrant() throws SQLException {
+ underTest.execute();
+ // re-entrant
+ underTest.execute();
+ }
+
+ private String insertUser(@Nullable String hashMethod) {
+ String login = hashMethod + randomAlphabetic(20);
+
+ Map<String, Object> map = new HashMap<>();
+ String uuid = UUID_FACTORY.create();
+ map.put("UUID", uuid);
+ map.put("LOGIN", login);
+ map.put("HASH_METHOD", hashMethod);
+ map.put("EXTERNAL_LOGIN", login);
+ map.put("EXTERNAL_IDENTITY_PROVIDER", "sonarqube");
+ map.put("EXTERNAL_ID", randomNumeric(5));
+ map.put("IS_ROOT", false);
+ map.put("ONBOARDED", false);
+ map.put("CREATED_AT", System.currentTimeMillis());
+ map.put("RESET_PASSWORD", false);
+ db.executeInsert("users", map);
+ return login;
+ }
+}
--- /dev/null
+CREATE TABLE "USERS"(
+ "UUID" CHARACTER VARYING(255) NOT NULL,
+ "LOGIN" CHARACTER VARYING(255) NOT NULL,
+ "NAME" CHARACTER VARYING(200),
+ "EMAIL" CHARACTER VARYING(100),
+ "CRYPTED_PASSWORD" CHARACTER VARYING(100),
+ "SALT" CHARACTER VARYING(40),
+ "HASH_METHOD" CHARACTER VARYING(10),
+ "ACTIVE" BOOLEAN DEFAULT TRUE,
+ "SCM_ACCOUNTS" CHARACTER VARYING(4000),
+ "EXTERNAL_LOGIN" CHARACTER VARYING(255) NOT NULL,
+ "EXTERNAL_IDENTITY_PROVIDER" CHARACTER VARYING(100) NOT NULL,
+ "EXTERNAL_ID" CHARACTER VARYING(255) NOT NULL,
+ "IS_ROOT" BOOLEAN NOT NULL,
+ "USER_LOCAL" BOOLEAN,
+ "ONBOARDED" BOOLEAN NOT NULL,
+ "HOMEPAGE_TYPE" CHARACTER VARYING(40),
+ "HOMEPAGE_PARAMETER" CHARACTER VARYING(40),
+ "LAST_CONNECTION_DATE" BIGINT,
+ "CREATED_AT" BIGINT,
+ "UPDATED_AT" BIGINT,
+ "RESET_PASSWORD" BOOLEAN NOT NULL,
+ "LAST_SONARLINT_CONNECTION" BIGINT,
+ "SONARLINT_AD_SEEN" BOOLEAN DEFAULT FALSE
+);
+ALTER TABLE "USERS" ADD CONSTRAINT "PK_USERS" PRIMARY KEY("UUID");
+CREATE UNIQUE INDEX "USERS_LOGIN" ON "USERS"("LOGIN" NULLS FIRST);
+CREATE INDEX "USERS_UPDATED_AT" ON "USERS"("UPDATED_AT" NULLS FIRST);
+CREATE UNIQUE INDEX "UNIQ_EXTERNAL_ID" ON "USERS"("EXTERNAL_IDENTITY_PROVIDER" NULLS FIRST, "EXTERNAL_ID" NULLS FIRST);
+CREATE UNIQUE INDEX "UNIQ_EXTERNAL_LOGIN" ON "USERS"("EXTERNAL_IDENTITY_PROVIDER" NULLS FIRST, "EXTERNAL_LOGIN" NULLS FIRST);
* delegated to an external system, e.g. LDAP.
*/
public class CredentialsAuthentication {
-
+ static final String ERROR_PASSWORD_CANNOT_BE_NULL = "Password cannot be null";
private final DbClient dbClient;
private final AuthenticationEvent authenticationEvent;
private final CredentialsExternalAuthentication externalAuthentication;
private UserDto authenticate(DbSession dbSession, Credentials credentials, HttpServletRequest request, Method method) {
UserDto localUser = dbClient.userDao().selectActiveUserByLogin(dbSession, credentials.getLogin());
if (localUser != null && localUser.isLocal()) {
- localAuthentication.authenticate(dbSession, localUser, credentials.getPassword().orElse(null), method);
+ String password = getNonNullPassword(credentials);
+ localAuthentication.authenticate(dbSession, localUser, password, method);
dbSession.commit();
authenticationEvent.loginSuccess(request, localUser.getLogin(), Source.local(method));
return localUser;
.build();
}
+ private static String getNonNullPassword(Credentials credentials) {
+ return credentials.getPassword().orElseThrow(() -> new IllegalArgumentException(ERROR_PASSWORD_CANNOT_BE_NULL));
+ }
+
}
import javax.annotation.Nullable;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
-import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.RandomStringUtils;
import org.mindrot.jbcrypt.BCrypt;
import org.sonar.api.config.Configuration;
import static com.google.common.base.Preconditions.checkArgument;
import static java.lang.String.format;
-import static java.util.Objects.requireNonNull;
/**
* Validates the password of a "local" user (password is stored in
public static final String ERROR_NULL_PASSWORD_IN_DB = "null password in DB";
public static final String ERROR_NULL_SALT = "null salt";
public static final String ERROR_WRONG_PASSWORD = "wrong password";
- public static final String ERROR_PASSWORD_CANNOT_BE_NULL = "Password cannot be null";
public static final String ERROR_UNKNOWN_HASH_METHOD = "Unknown hash method [%s]";
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
private static final String PBKDF2_ITERATIONS_PROP = "sonar.internal.pbkdf2.iterations";
private final EnumMap<HashMethod, HashFunction> hashFunctions = new EnumMap<>(HashMethod.class);
public enum HashMethod {
- SHA1, BCRYPT, PBKDF2
+ BCRYPT, PBKDF2
}
public CredentialsLocalAuthentication(DbClient dbClient, Configuration configuration) {
this.dbClient = dbClient;
hashFunctions.put(HashMethod.BCRYPT, new BcryptFunction());
- hashFunctions.put(HashMethod.SHA1, new Sha1Function());
hashFunctions.put(HashMethod.PBKDF2, new PBKDF2Function(configuration.getInt(PBKDF2_ITERATIONS_PROP).orElse(null)));
}
* If the password must be updated because an old algorithm is used, the UserDto is updated but the session
* is not committed
*/
- public void authenticate(DbSession session, UserDto user, @Nullable String password, Method method) {
+ public void authenticate(DbSession session, UserDto user, String password, Method method) {
+ HashMethod hashMethod = getHashMethod(user, method);
+ HashFunction hashFunction = hashFunctions.get(hashMethod);
+ AuthenticationResult result = authenticate(user, password, method, hashFunction);
+
+ // Upgrade the password if it's an old hashMethod
+ if (hashMethod != DEFAULT || result.needsUpdate) {
+ hashFunctions.get(DEFAULT).storeHashPassword(user, password);
+ dbClient.userDao().update(session, user);
+ }
+ }
+
+ private HashMethod getHashMethod(UserDto user, Method method) {
if (user.getHashMethod() == null) {
throw AuthenticationException.newBuilder()
.setSource(Source.local(method))
.setMessage(ERROR_NULL_HASH_METHOD)
.build();
}
-
- HashMethod hashMethod;
try {
- hashMethod = HashMethod.valueOf(user.getHashMethod());
+ return HashMethod.valueOf(user.getHashMethod());
} catch (IllegalArgumentException ex) {
generateHashToAvoidEnumerationAttack();
throw AuthenticationException.newBuilder()
.setMessage(format(ERROR_UNKNOWN_HASH_METHOD, user.getHashMethod()))
.build();
}
+ }
- HashFunction hashFunction = hashFunctions.get(hashMethod);
-
+ private static AuthenticationResult authenticate(UserDto user, String password, Method method, HashFunction hashFunction) {
AuthenticationResult result = hashFunction.checkCredentials(user, password);
if (!result.isSuccessful()) {
throw AuthenticationException.newBuilder()
.setMessage(result.getFailureMessage())
.build();
}
-
- // Upgrade the password if it's an old hashMethod
- if (hashMethod != DEFAULT || result.needsUpdate) {
- hashFunctions.get(DEFAULT).storeHashPassword(user, password);
- dbClient.userDao().update(session, user);
- }
+ return result;
}
/**
}
}
- /**
- * Implementation of deprecated SHA1 hash function
- */
- private static final class Sha1Function implements HashFunction {
- @Override
- public AuthenticationResult checkCredentials(UserDto user, String password) {
- if (user.getCryptedPassword() == null) {
- return new AuthenticationResult(false, ERROR_NULL_PASSWORD_IN_DB);
- }
- if (user.getSalt() == null) {
- return new AuthenticationResult(false, ERROR_NULL_SALT);
- }
- if (!user.getCryptedPassword().equals(hash(user.getSalt(), password))) {
- return new AuthenticationResult(false, ERROR_WRONG_PASSWORD);
- }
- return new AuthenticationResult(true, "");
- }
-
- @Override
- public void storeHashPassword(UserDto user, String password) {
- requireNonNull(password, ERROR_PASSWORD_CANNOT_BE_NULL);
- byte[] saltRandom = new byte[20];
- SECURE_RANDOM.nextBytes(saltRandom);
- String salt = DigestUtils.sha1Hex(saltRandom);
-
- user.setHashMethod(HashMethod.SHA1.name())
- .setCryptedPassword(hash(salt, password))
- .setSalt(salt);
- }
-
- private static String hash(String salt, String password) {
- return DigestUtils.sha1Hex("--" + salt + "--" + password + "--");
- }
- }
-
static final class PBKDF2Function implements HashFunction {
private static final char ITERATIONS_HASH_SEPARATOR = '$';
private static final int DEFAULT_ITERATIONS = 100_000;
@Override
public void storeHashPassword(UserDto user, String password) {
- requireNonNull(password, ERROR_PASSWORD_CANNOT_BE_NULL);
user.setHashMethod(HashMethod.BCRYPT.name())
.setCryptedPassword(BCrypt.hashpw(password, BCrypt.gensalt(12)))
.setSalt(null);
import javax.servlet.http.HttpServletRequest;
import org.junit.Rule;
import org.junit.Test;
-import org.mockito.Mockito;
import org.sonar.api.config.internal.MapSettings;
import org.sonar.api.utils.System2;
import org.sonar.db.DbClient;
import org.sonar.server.authentication.event.AuthenticationEvent;
import org.sonar.server.authentication.event.AuthenticationException;
+import static java.lang.String.format;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import static org.sonar.db.user.UserTesting.newUserDto;
+import static org.sonar.server.authentication.CredentialsAuthentication.ERROR_PASSWORD_CANNOT_BE_NULL;
+import static org.sonar.server.authentication.CredentialsLocalAuthentication.ERROR_UNKNOWN_HASH_METHOD;
import static org.sonar.server.authentication.event.AuthenticationEvent.Method.BASIC;
import static org.sonar.server.authentication.event.AuthenticationEvent.Method.BASIC_TOKEN;
import static org.sonar.server.authentication.event.AuthenticationEvent.Source;
private static final String LOGIN = "LOGIN";
private static final String PASSWORD = "PASSWORD";
private static final String SALT = "0242b0b4c0a93ddfe09dd886de50bc25ba000b51";
- private static final String ENCRYPTED_PASSWORD = "540e4fc4be4e047db995bc76d18374a5b5db08cc";
+ private static final int NUMBER_OF_PBKDF2_ITERATIONS = 1;
+ private static final String ENCRYPTED_PASSWORD = format("%d$%s", NUMBER_OF_PBKDF2_ITERATIONS, "FVu1Wtpe0MM/Rs+CcLT7nbzMMQ0emHDXpcfjJoQrDtCe8cQqWP4rpCXZenBw9bC3/UWx5+kA9go9zKkhq2UmAQ==");
+ private static final String DEPRECATED_HASH_METHOD = "SHA1";
@Rule
public DbTester dbTester = DbTester.create(System2.INSTANCE);
- private DbClient dbClient = dbTester.getDbClient();
- private DbSession dbSession = dbTester.getSession();
- private HttpServletRequest request = mock(HttpServletRequest.class);
- private AuthenticationEvent authenticationEvent = mock(AuthenticationEvent.class);
- private MapSettings settings = new MapSettings().setProperty("sonar.internal.pbkdf2.iterations", "1");
- private CredentialsExternalAuthentication externalAuthentication = mock(CredentialsExternalAuthentication.class);
- private CredentialsLocalAuthentication localAuthentication = Mockito.spy(new CredentialsLocalAuthentication(dbClient, settings.asConfig()));
- private CredentialsAuthentication underTest = new CredentialsAuthentication(dbClient, authenticationEvent, externalAuthentication, localAuthentication);
+ private final DbClient dbClient = dbTester.getDbClient();
+ private final DbSession dbSession = dbTester.getSession();
+ private final HttpServletRequest request = mock(HttpServletRequest.class);
+ private final AuthenticationEvent authenticationEvent = mock(AuthenticationEvent.class);
+ private final MapSettings settings = new MapSettings().setProperty("sonar.internal.pbkdf2.iterations", NUMBER_OF_PBKDF2_ITERATIONS);
+ private final CredentialsExternalAuthentication externalAuthentication = mock(CredentialsExternalAuthentication.class);
+ private final CredentialsLocalAuthentication localAuthentication = spy(new CredentialsLocalAuthentication(dbClient, settings.asConfig()));
+ private final CredentialsAuthentication underTest = new CredentialsAuthentication(dbClient, authenticationEvent, externalAuthentication, localAuthentication);
@Test
public void authenticate_local_user() {
insertUser(newUserDto()
.setLogin(LOGIN)
.setCryptedPassword(ENCRYPTED_PASSWORD)
- .setHashMethod(CredentialsLocalAuthentication.HashMethod.SHA1.name())
+ .setHashMethod(CredentialsLocalAuthentication.HashMethod.PBKDF2.name())
.setSalt(SALT)
.setLocal(true));
public void fail_to_authenticate_local_user_when_password_is_wrong() {
insertUser(newUserDto()
.setLogin(LOGIN)
- .setCryptedPassword("Wrong password")
- .setSalt("Wrong salt")
- .setHashMethod(CredentialsLocalAuthentication.HashMethod.SHA1.name())
+ .setCryptedPassword(format("%d$%s", NUMBER_OF_PBKDF2_ITERATIONS, "WrongPassword"))
+ .setSalt("salt")
+ .setHashMethod(CredentialsLocalAuthentication.HashMethod.PBKDF2.name())
.setLocal(true));
assertThatThrownBy(() -> executeAuthenticate(BASIC))
.setLogin(LOGIN)
.setCryptedPassword(null)
.setSalt(SALT)
- .setHashMethod(CredentialsLocalAuthentication.HashMethod.SHA1.name())
+ .setHashMethod(CredentialsLocalAuthentication.HashMethod.PBKDF2.name())
.setLocal(true));
assertThatThrownBy(() -> executeAuthenticate(BASIC))
.setLogin(LOGIN)
.setCryptedPassword(ENCRYPTED_PASSWORD)
.setSalt(null)
- .setHashMethod(CredentialsLocalAuthentication.HashMethod.SHA1.name())
+ .setHashMethod(CredentialsLocalAuthentication.HashMethod.PBKDF2.name())
.setLocal(true));
assertThatThrownBy(() -> executeAuthenticate(BASIC_TOKEN))
verifyNoInteractions(authenticationEvent);
}
+ @Test
+ public void fail_to_authenticate_unknown_hash_method_should_force_hash() {
+ insertUser(newUserDto()
+ .setLogin(LOGIN)
+ .setCryptedPassword(ENCRYPTED_PASSWORD)
+ .setSalt(SALT)
+ .setHashMethod(DEPRECATED_HASH_METHOD)
+ .setLocal(true));
+
+ assertThatThrownBy(() -> executeAuthenticate(BASIC_TOKEN))
+ .hasMessage(format(ERROR_UNKNOWN_HASH_METHOD, DEPRECATED_HASH_METHOD))
+ .isInstanceOf(AuthenticationException.class)
+ .hasFieldOrPropertyWithValue("source", Source.local(BASIC_TOKEN))
+ .hasFieldOrPropertyWithValue("login", LOGIN);
+
+ verify(localAuthentication).generateHashToAvoidEnumerationAttack();
+ verifyNoInteractions(authenticationEvent);
+ }
+
+ @Test
+ public void local_authentication_without_password_should_throw_IAE() {
+ insertUser(newUserDto()
+ .setLogin(LOGIN)
+ .setCryptedPassword(ENCRYPTED_PASSWORD)
+ .setSalt(SALT)
+ .setHashMethod(DEPRECATED_HASH_METHOD)
+ .setLocal(true));
+
+ Credentials credentials = new Credentials(LOGIN, null);
+ assertThatThrownBy(() -> underTest.authenticate(credentials, request, BASIC_TOKEN))
+ .hasMessage(ERROR_PASSWORD_CANNOT_BE_NULL)
+ .isInstanceOf(IllegalArgumentException.class);
+
+ verifyNoInteractions(authenticationEvent);
+ }
+
@Test
public void fail_to_authenticate_unknown_user_after_forcing_hash() {
assertThatThrownBy(() -> executeAuthenticate(BASIC))
*/
package org.sonar.server.authentication;
+import java.security.SecureRandom;
+import java.util.Base64;
import java.util.Optional;
import java.util.Random;
import org.apache.commons.codec.digest.DigestUtils;
import static java.lang.String.format;
import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.sonar.db.user.UserTesting.newUserDto;
import static org.sonar.server.authentication.CredentialsLocalAuthentication.HashMethod.BCRYPT;
import static org.sonar.server.authentication.CredentialsLocalAuthentication.HashMethod.PBKDF2;
-import static org.sonar.server.authentication.CredentialsLocalAuthentication.HashMethod.SHA1;
public class CredentialsLocalAuthenticationTest {
+
+ private static final SecureRandom SECURE_RANDOM = new SecureRandom();
+ private static final String PBKDF2_SALT = generatePBKDF2Salt();
+
@Rule
public DbTester db = DbTester.create();
}
@Test
- public void authentication_with_sha1_with_correct_password_should_work() {
+ public void authentication_with_sha1_should_throw_AuthenticationException() {
String password = randomAlphanumeric(60);
byte[] saltRandom = new byte[20];
String salt = DigestUtils.sha1Hex(saltRandom);
UserDto user = newUserDto()
- .setHashMethod(SHA1.name())
+ .setHashMethod("SHA1")
.setCryptedPassword(DigestUtils.sha1Hex("--" + salt + "--" + password + "--"))
.setSalt(salt);
- underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
- }
-
- @Test
- public void authentication_with_sha1_with_incorrect_password_should_throw_AuthenticationException() {
- String password = randomAlphanumeric(60);
- DbSession dbSession = db.getSession();
-
- byte[] saltRandom = new byte[20];
- RANDOM.nextBytes(saltRandom);
- String salt = DigestUtils.sha1Hex(saltRandom);
-
- UserDto user = newUserDto()
- .setHashMethod(SHA1.name())
- .setCryptedPassword(DigestUtils.sha1Hex("--" + salt + "--" + password + "--"))
- .setSalt(salt);
-
- assertThatThrownBy(() -> underTest.authenticate(dbSession, user, "WHATEVER", AuthenticationEvent.Method.BASIC))
- .isInstanceOf(AuthenticationException.class)
- .hasMessage(CredentialsLocalAuthentication.ERROR_WRONG_PASSWORD);
- }
-
- @Test
- public void authentication_with_sha1_with_empty_password_should_throw_AuthenticationException() {
- DbSession dbSession = db.getSession();
- byte[] saltRandom = new byte[20];
- RANDOM.nextBytes(saltRandom);
- String salt = DigestUtils.sha1Hex(saltRandom);
-
- UserDto user = newUserDto()
- .setCryptedPassword(null)
- .setHashMethod(SHA1.name())
- .setSalt(salt);
-
- assertThatThrownBy(() -> underTest.authenticate(dbSession, user, "WHATEVER", AuthenticationEvent.Method.BASIC))
- .isInstanceOf(AuthenticationException.class)
- .hasMessage(CredentialsLocalAuthentication.ERROR_NULL_PASSWORD_IN_DB);
- }
-
- @Test
- public void authentication_with_sha1_with_empty_salt_should_throw_AuthenticationException() {
- DbSession dbSession = db.getSession();
- String password = randomAlphanumeric(60);
-
- UserDto user = newUserDto()
- .setHashMethod(SHA1.name())
- .setCryptedPassword(DigestUtils.sha1Hex("--0242b0b4c0a93ddfe09dd886de50bc25ba000b51--" + password + "--"))
- .setSalt(null);
-
- assertThatThrownBy(() -> underTest.authenticate(dbSession, user, "WHATEVER", AuthenticationEvent.Method.BASIC))
- .isInstanceOf(AuthenticationException.class)
- .hasMessage(CredentialsLocalAuthentication.ERROR_NULL_SALT);
+ DbSession session = db.getSession();
+ assertThatExceptionOfType(AuthenticationException.class)
+ .isThrownBy(() -> underTest.authenticate(session, user, password, AuthenticationEvent.Method.BASIC))
+ .withMessage("Unknown hash method [SHA1]");
}
@Test
.hasMessage(CredentialsLocalAuthentication.ERROR_NULL_PASSWORD_IN_DB);
}
- @Test
- public void authentication_upgrade_hash_function_when_SHA1_was_used() {
- String password = randomAlphanumeric(60);
-
- byte[] saltRandom = new byte[20];
- RANDOM.nextBytes(saltRandom);
- String salt = DigestUtils.sha1Hex(saltRandom);
-
- UserDto user = newUserDto()
- .setLogin("myself")
- .setHashMethod(SHA1.name())
- .setCryptedPassword(DigestUtils.sha1Hex("--" + salt + "--" + password + "--"))
- .setSalt(salt);
- db.users().insertUser(user);
-
- underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
-
- Optional<UserDto> myself = db.users().selectUserByLogin("myself");
- assertThat(myself).isPresent();
- assertThat(myself.get().getHashMethod()).isEqualTo(PBKDF2.name());
- assertThat(myself.get().getSalt()).isNotNull();
-
- // authentication must work with upgraded hash method
- underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
- }
-
@Test
public void authentication_upgrade_hash_function_when_BCRYPT_was_used() {
String password = randomAlphanumeric(60);
- byte[] saltRandom = new byte[20];
- RANDOM.nextBytes(saltRandom);
- String salt = DigestUtils.sha1Hex(saltRandom);
-
UserDto user = newUserDto()
.setLogin("myself")
.setHashMethod(BCRYPT.name())
.setCryptedPassword(BCrypt.hashpw(password, BCrypt.gensalt(12)))
- .setSalt(salt);
+ .setSalt(null);
db.users().insertUser(user);
underTest.authenticate(db.getSession(), user, password, AuthenticationEvent.Method.BASIC);
}
@Test
- public void authentication_with_pbkdf2_with_invalid_password_should_throw_AuthenticationException() {
+ public void authentication_with_pbkdf2_with_invalid_hash_should_throw_AuthenticationException() {
DbSession dbSession = db.getSession();
String password = randomAlphanumeric(60);
- byte[] saltRandom = new byte[20];
- RANDOM.nextBytes(saltRandom);
- String salt = DigestUtils.sha1Hex(saltRandom);
-
UserDto userInvalidHash = newUserDto()
.setHashMethod(PBKDF2.name())
- .setCryptedPassword(DigestUtils.sha1Hex("--" + salt + "--" + password + "--"))
- .setSalt(salt);
+ .setCryptedPassword(password)
+ .setSalt(PBKDF2_SALT);
- assertThatThrownBy(() -> underTest.authenticate(dbSession, userInvalidHash, "WHATEVER", AuthenticationEvent.Method.BASIC))
+ assertThatThrownBy(() -> underTest.authenticate(dbSession, userInvalidHash, password, AuthenticationEvent.Method.BASIC))
.isInstanceOf(AuthenticationException.class)
.hasMessage("invalid hash stored");
UserDto userInvalidIterations = newUserDto()
.setHashMethod(PBKDF2.name())
- .setCryptedPassword("a$")
- .setSalt(salt);
+ .setCryptedPassword("a$" + password)
+ .setSalt(PBKDF2_SALT);
- assertThatThrownBy(() -> underTest.authenticate(dbSession, userInvalidIterations, "WHATEVER", AuthenticationEvent.Method.BASIC))
+ assertThatThrownBy(() -> underTest.authenticate(dbSession, userInvalidIterations, password, AuthenticationEvent.Method.BASIC))
.isInstanceOf(AuthenticationException.class)
.hasMessage("invalid hash stored");
}
@Test
public void authentication_with_pbkdf2_with_empty_password_should_throw_AuthenticationException() {
- byte[] saltRandom = new byte[20];
- RANDOM.nextBytes(saltRandom);
- String salt = DigestUtils.sha1Hex(saltRandom);
DbSession dbSession = db.getSession();
UserDto user = newUserDto()
.setCryptedPassword(null)
.setHashMethod(PBKDF2.name())
- .setSalt(salt);
+ .setSalt(PBKDF2_SALT);
assertThatThrownBy(() -> underTest.authenticate(dbSession, user, "WHATEVER", AuthenticationEvent.Method.BASIC))
.isInstanceOf(AuthenticationException.class)
UserDto user = newUserDto()
.setHashMethod(PBKDF2.name())
- .setCryptedPassword(DigestUtils.sha1Hex("--0242b0b4c0a93ddfe09dd886de50bc25ba000b51--" + password + "--"))
+ .setCryptedPassword("1$" + password)
.setSalt(null);
- assertThatThrownBy(() -> underTest.authenticate(dbSession, user, "WHATEVER", AuthenticationEvent.Method.BASIC))
+ assertThatThrownBy(() -> underTest.authenticate(dbSession, user, password, AuthenticationEvent.Method.BASIC))
.isInstanceOf(AuthenticationException.class)
.hasMessage(CredentialsLocalAuthentication.ERROR_NULL_SALT);
}
+
+ private static String generatePBKDF2Salt() {
+ byte[] salt = new byte[20];
+ SECURE_RANDOM.nextBytes(salt);
+ String saltStr = Base64.getEncoder().encodeToString(salt);
+ return saltStr;
+ }
}