You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

CredentialsLocalAuthentication.java 7.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2020 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. import java.security.SecureRandom;
  22. import javax.annotation.Nullable;
  23. import org.apache.commons.codec.digest.DigestUtils;
  24. import org.mindrot.jbcrypt.BCrypt;
  25. import org.sonar.db.DbClient;
  26. import org.sonar.db.DbSession;
  27. import org.sonar.db.user.UserDto;
  28. import org.sonar.server.authentication.event.AuthenticationEvent.Method;
  29. import org.sonar.server.authentication.event.AuthenticationEvent.Source;
  30. import org.sonar.server.authentication.event.AuthenticationException;
  31. import static com.google.common.base.Preconditions.checkArgument;
  32. import static java.lang.String.format;
  33. import static java.util.Objects.requireNonNull;
  34. /**
  35. * Validates the password of a "local" user (password is stored in
  36. * database).
  37. */
  38. public class CredentialsLocalAuthentication {
  39. private final DbClient dbClient;
  40. private static final SecureRandom SECURE_RANDOM = new SecureRandom();
  41. // The default hash method that must be used is BCRYPT
  42. private static final HashMethod DEFAULT = HashMethod.BCRYPT;
  43. public CredentialsLocalAuthentication(DbClient dbClient) {
  44. this.dbClient = dbClient;
  45. }
  46. /**
  47. * This method authenticate a user with his password against the value stored in user.
  48. * If authentication failed an AuthenticationException will be thrown containing the failure message.
  49. * If the password must be updated because an old algorithm is used, the UserDto is updated but the session
  50. * is not committed
  51. */
  52. public void authenticate(DbSession session, UserDto user, @Nullable String password, Method method) {
  53. if (user.getHashMethod() == null) {
  54. throw AuthenticationException.newBuilder()
  55. .setSource(Source.local(method))
  56. .setLogin(user.getLogin())
  57. .setMessage("null hash method")
  58. .build();
  59. }
  60. HashMethod hashMethod;
  61. try {
  62. hashMethod = HashMethod.valueOf(user.getHashMethod());
  63. } catch (IllegalArgumentException ex) {
  64. throw AuthenticationException.newBuilder()
  65. .setSource(Source.local(method))
  66. .setLogin(user.getLogin())
  67. .setMessage(format("Unknown hash method [%s]", user.getHashMethod()))
  68. .build();
  69. }
  70. AuthenticationResult result = hashMethod.checkCredentials(user, password);
  71. if (!result.isSuccessful()) {
  72. throw AuthenticationException.newBuilder()
  73. .setSource(Source.local(method))
  74. .setLogin(user.getLogin())
  75. .setMessage(result.getFailureMessage())
  76. .build();
  77. }
  78. // Upgrade the password if it's an old hashMethod
  79. if (hashMethod != DEFAULT) {
  80. DEFAULT.storeHashPassword(user, password);
  81. dbClient.userDao().update(session, user);
  82. }
  83. }
  84. /**
  85. * Method used to store the password as a hash in database.
  86. * The crypted_password, salt and hash_method are set
  87. */
  88. public void storeHashPassword(UserDto user, String password) {
  89. DEFAULT.storeHashPassword(user, password);
  90. }
  91. public enum HashMethod implements HashFunction {
  92. SHA1(new Sha1Function()), BCRYPT(new BcryptFunction());
  93. private HashFunction hashFunction;
  94. HashMethod(HashFunction hashFunction) {
  95. this.hashFunction = hashFunction;
  96. }
  97. @Override
  98. public AuthenticationResult checkCredentials(UserDto user, String password) {
  99. return hashFunction.checkCredentials(user, password);
  100. }
  101. @Override
  102. public void storeHashPassword(UserDto user, String password) {
  103. hashFunction.storeHashPassword(user, password);
  104. }
  105. }
  106. private static class AuthenticationResult {
  107. private final boolean successful;
  108. private final String failureMessage;
  109. private AuthenticationResult(boolean successful, String failureMessage) {
  110. checkArgument((successful && failureMessage.isEmpty()) || (!successful && !failureMessage.isEmpty()), "Incorrect parameters");
  111. this.successful = successful;
  112. this.failureMessage = failureMessage;
  113. }
  114. public boolean isSuccessful() {
  115. return successful;
  116. }
  117. public String getFailureMessage() {
  118. return failureMessage;
  119. }
  120. }
  121. public interface HashFunction {
  122. AuthenticationResult checkCredentials(UserDto user, String password);
  123. void storeHashPassword(UserDto user, String password);
  124. }
  125. /**
  126. * Implementation of deprecated SHA1 hash function
  127. */
  128. private static final class Sha1Function implements HashFunction {
  129. @Override
  130. public AuthenticationResult checkCredentials(UserDto user, String password) {
  131. if (user.getCryptedPassword() == null) {
  132. return new AuthenticationResult(false, "null password in DB");
  133. }
  134. if (user.getSalt() == null) {
  135. return new AuthenticationResult(false, "null salt");
  136. }
  137. if (!user.getCryptedPassword().equals(hash(user.getSalt(), password))) {
  138. return new AuthenticationResult(false, "wrong password");
  139. }
  140. return new AuthenticationResult(true, "");
  141. }
  142. @Override
  143. public void storeHashPassword(UserDto user, String password) {
  144. requireNonNull(password, "Password cannot be null");
  145. byte[] saltRandom = new byte[20];
  146. SECURE_RANDOM.nextBytes(saltRandom);
  147. String salt = DigestUtils.sha1Hex(saltRandom);
  148. user.setHashMethod(HashMethod.SHA1.name())
  149. .setCryptedPassword(hash(salt, password))
  150. .setSalt(salt);
  151. }
  152. private static String hash(String salt, String password) {
  153. return DigestUtils.sha1Hex("--" + salt + "--" + password + "--");
  154. }
  155. }
  156. /**
  157. * Implementation of bcrypt hash function
  158. */
  159. private static final class BcryptFunction implements HashFunction {
  160. @Override
  161. public AuthenticationResult checkCredentials(UserDto user, String password) {
  162. // This behavior is overridden in most of integration tests for performance reasons, any changes to BCrypt calls should be propagated to Byteman classes
  163. if (!BCrypt.checkpw(password, user.getCryptedPassword())) {
  164. return new AuthenticationResult(false, "wrong password");
  165. }
  166. return new AuthenticationResult(true, "");
  167. }
  168. @Override
  169. public void storeHashPassword(UserDto user, String password) {
  170. requireNonNull(password, "Password cannot be null");
  171. user.setHashMethod(HashMethod.BCRYPT.name())
  172. .setCryptedPassword(BCrypt.hashpw(password, BCrypt.gensalt(12)))
  173. .setSalt(null);
  174. }
  175. }
  176. }