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.security.NoSuchAlgorithmException;
23 import java.security.SecureRandom;
24 import java.security.spec.InvalidKeySpecException;
25 import java.util.Base64;
26 import java.util.EnumMap;
27 import javax.annotation.Nullable;
28 import javax.crypto.SecretKeyFactory;
29 import javax.crypto.spec.PBEKeySpec;
30 import org.apache.commons.codec.digest.DigestUtils;
31 import org.mindrot.jbcrypt.BCrypt;
32 import org.sonar.api.config.Configuration;
33 import org.sonar.db.DbClient;
34 import org.sonar.db.DbSession;
35 import org.sonar.db.user.UserDto;
36 import org.sonar.server.authentication.event.AuthenticationEvent.Method;
37 import org.sonar.server.authentication.event.AuthenticationEvent.Source;
38 import org.sonar.server.authentication.event.AuthenticationException;
40 import static com.google.common.base.Preconditions.checkArgument;
41 import static java.lang.String.format;
42 import static java.util.Objects.requireNonNull;
45 * Validates the password of a "local" user (password is stored in
48 public class CredentialsLocalAuthentication {
49 private static final SecureRandom SECURE_RANDOM = new SecureRandom();
50 private static final HashMethod DEFAULT = HashMethod.PBKDF2;
51 private static final String PBKDF2_ITERATIONS_PROP = "sonar.internal.pbkdf2.iterations";
53 private final DbClient dbClient;
54 private final EnumMap<HashMethod, HashFunction> hashFunctions = new EnumMap<>(HashMethod.class);
56 public enum HashMethod {
60 public CredentialsLocalAuthentication(DbClient dbClient, Configuration configuration) {
61 this.dbClient = dbClient;
62 hashFunctions.put(HashMethod.BCRYPT, new BcryptFunction());
63 hashFunctions.put(HashMethod.SHA1, new Sha1Function());
64 hashFunctions.put(HashMethod.PBKDF2, new PBKDF2Function(configuration.getInt(PBKDF2_ITERATIONS_PROP).orElse(null)));
69 * This method authenticate a user with his password against the value stored in user.
70 * If authentication failed an AuthenticationException will be thrown containing the failure message.
71 * If the password must be updated because an old algorithm is used, the UserDto is updated but the session
74 public void authenticate(DbSession session, UserDto user, @Nullable String password, Method method) {
75 if (user.getHashMethod() == null) {
76 throw AuthenticationException.newBuilder()
77 .setSource(Source.local(method))
78 .setLogin(user.getLogin())
79 .setMessage("null hash method")
83 HashMethod hashMethod;
85 hashMethod = HashMethod.valueOf(user.getHashMethod());
86 } catch (IllegalArgumentException ex) {
87 throw AuthenticationException.newBuilder()
88 .setSource(Source.local(method))
89 .setLogin(user.getLogin())
90 .setMessage(format("Unknown hash method [%s]", user.getHashMethod()))
94 HashFunction hashFunction = hashFunctions.get(hashMethod);
96 AuthenticationResult result = hashFunction.checkCredentials(user, password);
97 if (!result.isSuccessful()) {
98 throw AuthenticationException.newBuilder()
99 .setSource(Source.local(method))
100 .setLogin(user.getLogin())
101 .setMessage(result.getFailureMessage())
105 // Upgrade the password if it's an old hashMethod
106 if (hashMethod != DEFAULT || result.needsUpdate) {
107 hashFunctions.get(DEFAULT).storeHashPassword(user, password);
108 dbClient.userDao().update(session, user);
113 * Method used to store the password as a hash in database.
114 * The crypted_password, salt and hash_method are set
116 public void storeHashPassword(UserDto user, String password) {
117 hashFunctions.get(DEFAULT).storeHashPassword(user, password);
120 private static class AuthenticationResult {
121 private final boolean successful;
122 private final String failureMessage;
123 private final boolean needsUpdate;
125 private AuthenticationResult(boolean successful, String failureMessage) {
126 this(successful, failureMessage, false);
129 private AuthenticationResult(boolean successful, String failureMessage, boolean needsUpdate) {
130 checkArgument((successful && failureMessage.isEmpty()) || (!successful && !failureMessage.isEmpty()), "Incorrect parameters");
131 this.successful = successful;
132 this.failureMessage = failureMessage;
133 this.needsUpdate = needsUpdate;
136 public boolean isSuccessful() {
140 public String getFailureMessage() {
141 return failureMessage;
144 public boolean isNeedsUpdate() {
149 public interface HashFunction {
150 AuthenticationResult checkCredentials(UserDto user, String password);
152 void storeHashPassword(UserDto user, String password);
156 * Implementation of deprecated SHA1 hash function
158 private static final class Sha1Function implements HashFunction {
160 public AuthenticationResult checkCredentials(UserDto user, String password) {
161 if (user.getCryptedPassword() == null) {
162 return new AuthenticationResult(false, "null password in DB");
164 if (user.getSalt() == null) {
165 return new AuthenticationResult(false, "null salt");
167 if (!user.getCryptedPassword().equals(hash(user.getSalt(), password))) {
168 return new AuthenticationResult(false, "wrong password");
170 return new AuthenticationResult(true, "");
174 public void storeHashPassword(UserDto user, String password) {
175 requireNonNull(password, "Password cannot be null");
176 byte[] saltRandom = new byte[20];
177 SECURE_RANDOM.nextBytes(saltRandom);
178 String salt = DigestUtils.sha1Hex(saltRandom);
180 user.setHashMethod(HashMethod.SHA1.name())
181 .setCryptedPassword(hash(salt, password))
185 private static String hash(String salt, String password) {
186 return DigestUtils.sha1Hex("--" + salt + "--" + password + "--");
190 private static final class PBKDF2Function implements HashFunction {
191 private static final int DEFAULT_ITERATIONS = 100_000;
192 private static final String ALGORITHM = "PBKDF2WithHmacSHA512";
193 private static final int KEY_LEN = 512;
194 private final int gen_iterations;
196 public PBKDF2Function(@Nullable Integer gen_iterations) {
197 this.gen_iterations = gen_iterations != null ? gen_iterations : DEFAULT_ITERATIONS;
201 public AuthenticationResult checkCredentials(UserDto user, String password) {
202 if (user.getCryptedPassword() == null) {
203 return new AuthenticationResult(false, "null password in DB");
205 if (user.getSalt() == null) {
206 return new AuthenticationResult(false, "null salt");
209 int pos = user.getCryptedPassword().indexOf('$');
211 return new AuthenticationResult(false, "invalid hash stored");
215 iterations = Integer.parseInt(user.getCryptedPassword().substring(0, pos));
216 } catch (NumberFormatException e) {
217 return new AuthenticationResult(false, "invalid hash stored");
219 String hash = user.getCryptedPassword().substring(pos + 1);
220 byte[] salt = Base64.getDecoder().decode(user.getSalt());
222 if (!hash.equals(hash(salt, password, iterations))) {
223 return new AuthenticationResult(false, "wrong password");
225 boolean needsUpdate = iterations != gen_iterations;
226 return new AuthenticationResult(true, "", needsUpdate);
230 public void storeHashPassword(UserDto user, String password) {
231 byte[] salt = new byte[20];
232 SECURE_RANDOM.nextBytes(salt);
233 String hashStr = hash(salt, password, gen_iterations);
234 String saltStr = Base64.getEncoder().encodeToString(salt);
235 user.setHashMethod(HashMethod.PBKDF2.name())
236 .setCryptedPassword(gen_iterations + "$" + hashStr)
240 private String hash(byte[] salt, String password, int iterations) {
242 SecretKeyFactory skf = SecretKeyFactory.getInstance(ALGORITHM);
243 PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, iterations, KEY_LEN);
244 byte[] hash = skf.generateSecret(spec).getEncoded();
245 return Base64.getEncoder().encodeToString(hash);
246 } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
247 throw new RuntimeException(e);
253 * Implementation of deprecated bcrypt hash function
255 private static final class BcryptFunction implements HashFunction {
257 public AuthenticationResult checkCredentials(UserDto user, String password) {
258 // This behavior is overridden in most of integration tests for performance reasons, any changes to BCrypt calls should be propagated to Byteman classes
259 if (!BCrypt.checkpw(password, user.getCryptedPassword())) {
260 return new AuthenticationResult(false, "wrong password");
262 return new AuthenticationResult(true, "");
266 public void storeHashPassword(UserDto user, String password) {
267 requireNonNull(password, "Password cannot be null");
268 user.setHashMethod(HashMethod.BCRYPT.name())
269 .setCryptedPassword(BCrypt.hashpw(password, BCrypt.gensalt(12)))