diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index 4913b9a56b5..db2b5f32a7f 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -350,6 +350,9 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M private List userTwoFactorAuthenticationProviders; + private long validUserLastAuthTimeDurationInMs = 0L; + private static final long DEFAULT_USER_AUTH_TIME_DURATION_MS = 350L; + public static ConfigKey enableUserTwoFactorAuthentication = new ConfigKey<>("Advanced", Boolean.class, "enable.user.2fa", @@ -1599,7 +1602,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M for (UserAuthenticator userAuthenticator : _userPasswordEncoders) { Pair authenticationResult = userAuthenticator.authenticate(user.getUsername(), currentPassword, userAccount.getDomainId(), null); if (authenticationResult == null) { - logger.trace(String.format("Authenticator [%s] is returning null for the authenticate mehtod.", userAuthenticator.getClass())); + logger.trace(String.format("Authenticator [%s] is returning null for the authenticate method.", userAuthenticator.getClass())); continue; } if (BooleanUtils.toBoolean(authenticationResult.first())) { @@ -2644,107 +2647,17 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M @Override public UserAccount authenticateUser(final String username, final String password, final Long domainId, final InetAddress loginIpAddress, final Map requestParameters) { + long authStartTimeInMs = System.currentTimeMillis(); UserAccount user = null; final String[] oAuthProviderArray = (String[])requestParameters.get(ApiConstants.PROVIDER); final String[] secretCodeArray = (String[])requestParameters.get(ApiConstants.SECRET_CODE); String oauthProvider = ((oAuthProviderArray == null) ? null : oAuthProviderArray[0]); String secretCode = ((secretCodeArray == null) ? null : secretCodeArray[0]); - if ((password != null && !password.isEmpty()) || (oauthProvider != null && secretCode != null)) { user = getUserAccount(username, password, domainId, requestParameters); } else { - String key = _configDao.getValue("security.singlesignon.key"); - if (key == null) { - // the SSO key is gone, don't authenticate - return null; - } - - String singleSignOnTolerance = _configDao.getValue("security.singlesignon.tolerance.millis"); - if (singleSignOnTolerance == null) { - // the SSO tolerance is gone (how much time before/after system time we'll allow the login request to be - // valid), - // don't authenticate - return null; - } - - long tolerance = Long.parseLong(singleSignOnTolerance); - String signature = null; - long timestamp = 0L; - String unsignedRequest; - StringBuffer unsignedRequestBuffer = new StringBuffer(); - - // - build a request string with sorted params, make sure it's all lowercase - // - sign the request, verify the signature is the same - List parameterNames = new ArrayList<>(); - - for (Object paramNameObj : requestParameters.keySet()) { - parameterNames.add((String)paramNameObj); // put the name in a list that we'll sort later - } - - Collections.sort(parameterNames); - - try { - for (String paramName : parameterNames) { - // parameters come as name/value pairs in the form String/String[] - String paramValue = ((String[])requestParameters.get(paramName))[0]; - - if ("signature".equalsIgnoreCase(paramName)) { - signature = paramValue; - } else { - if ("timestamp".equalsIgnoreCase(paramName)) { - String timestampStr = paramValue; - try { - // If the timestamp is in a valid range according to our tolerance, verify the request - // signature, otherwise return null to indicate authentication failure - timestamp = Long.parseLong(timestampStr); - long currentTime = System.currentTimeMillis(); - if (Math.abs(currentTime - timestamp) > tolerance) { - if (logger.isDebugEnabled()) { - logger.debug("Expired timestamp passed in to login, current time = " + currentTime + ", timestamp = " + timestamp); - } - return null; - } - } catch (NumberFormatException nfe) { - if (logger.isDebugEnabled()) { - logger.debug("Invalid timestamp passed in to login: " + timestampStr); - } - return null; - } - } - - if (unsignedRequestBuffer.length() != 0) { - unsignedRequestBuffer.append("&"); - } - unsignedRequestBuffer.append(paramName).append("=").append(URLEncoder.encode(paramValue, "UTF-8")); - } - } - - if ((signature == null) || (timestamp == 0L)) { - if (logger.isDebugEnabled()) { - logger.debug("Missing parameters in login request, signature = " + signature + ", timestamp = " + timestamp); - } - return null; - } - - unsignedRequest = unsignedRequestBuffer.toString().toLowerCase().replaceAll("\\+", "%20"); - - Mac mac = Mac.getInstance("HmacSHA1"); - SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "HmacSHA1"); - mac.init(keySpec); - mac.update(unsignedRequest.getBytes()); - byte[] encryptedBytes = mac.doFinal(); - String computedSignature = new String(Base64.encodeBase64(encryptedBytes)); - boolean equalSig = ConstantTimeComparator.compareStrings(signature, computedSignature); - if (!equalSig) { - logger.info("User signature: " + signature + " is not equaled to computed signature: " + computedSignature); - } else { - user = _userAccountDao.getUserAccount(username, domainId); - } - } catch (Exception ex) { - logger.error("Exception authenticating user", ex); - return null; - } + user = getUserAccountForSSO(username, domainId, requestParameters); } if (user != null) { @@ -2778,18 +2691,35 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M } } + ActionEventUtils.onActionEvent(user.getId(), user.getAccountId(), user.getDomainId(), EventTypes.EVENT_USER_LOGIN, "user has logged in from IP Address " + loginIpAddress, user.getId(), ApiCommandResourceType.User.toString()); + + validUserLastAuthTimeDurationInMs = System.currentTimeMillis() - authStartTimeInMs; // Here all is fine! if (logger.isDebugEnabled()) { - logger.debug("User: " + username + " in domain " + domainId + " has successfully logged in"); + logger.debug(String.format("User: %s in domain %d has successfully logged in, auth time duration - %d ms", username, domainId, validUserLastAuthTimeDurationInMs)); } - ActionEventUtils.onActionEvent(user.getId(), user.getAccountId(), user.getDomainId(), EventTypes.EVENT_USER_LOGIN, "user has logged in from IP Address " + loginIpAddress, user.getId(), ApiCommandResourceType.User.toString()); - return user; } else { if (logger.isDebugEnabled()) { logger.debug("User: " + username + " in domain " + domainId + " has failed to log in"); } + + long waitTimeDurationInMs; + long invalidUserAuthTimeDurationInMs = System.currentTimeMillis() - authStartTimeInMs; + if (validUserLastAuthTimeDurationInMs > 0) { + waitTimeDurationInMs = validUserLastAuthTimeDurationInMs - invalidUserAuthTimeDurationInMs; + } else { + waitTimeDurationInMs = DEFAULT_USER_AUTH_TIME_DURATION_MS - invalidUserAuthTimeDurationInMs; + } + + if (waitTimeDurationInMs > 0) { + try { + Thread.sleep(waitTimeDurationInMs); + } catch (final InterruptedException e) { + } + } + return null; } } @@ -2827,7 +2757,6 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M boolean updateIncorrectLoginCount = actionsOnFailedAuthenticaion.contains(ActionOnFailedAuthentication.INCREMENT_INCORRECT_LOGIN_ATTEMPT_COUNT); if (authenticated) { - Domain domain = _domainMgr.getDomain(domainId); String domainName = null; if (domain != null) { @@ -2869,6 +2798,99 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M } } + private UserAccount getUserAccountForSSO(String username, Long domainId, Map requestParameters) { + String key = _configDao.getValue("security.singlesignon.key"); + if (key == null) { + // the SSO key is gone, don't authenticate + return null; + } + + String singleSignOnTolerance = _configDao.getValue("security.singlesignon.tolerance.millis"); + if (singleSignOnTolerance == null) { + // the SSO tolerance is gone (how much time before/after system time we'll allow the login request to be + // valid), + // don't authenticate + return null; + } + + UserAccount user = null; + long tolerance = Long.parseLong(singleSignOnTolerance); + String signature = null; + long timestamp = 0L; + String unsignedRequest; + StringBuffer unsignedRequestBuffer = new StringBuffer(); + + // - build a request string with sorted params, make sure it's all lowercase + // - sign the request, verify the signature is the same + List parameterNames = new ArrayList<>(); + + for (Object paramNameObj : requestParameters.keySet()) { + parameterNames.add((String)paramNameObj); // put the name in a list that we'll sort later + } + + Collections.sort(parameterNames); + + try { + for (String paramName : parameterNames) { + // parameters come as name/value pairs in the form String/String[] + String paramValue = ((String[])requestParameters.get(paramName))[0]; + + if ("signature".equalsIgnoreCase(paramName)) { + signature = paramValue; + } else { + if ("timestamp".equalsIgnoreCase(paramName)) { + String timestampStr = paramValue; + try { + // If the timestamp is in a valid range according to our tolerance, verify the request + // signature, otherwise return null to indicate authentication failure + timestamp = Long.parseLong(timestampStr); + long currentTime = System.currentTimeMillis(); + if (Math.abs(currentTime - timestamp) > tolerance) { + logger.debug("Expired timestamp passed in to login, current time = {}, timestamp = {}", currentTime, timestamp); + return null; + } + } catch (NumberFormatException nfe) { + logger.debug("Invalid timestamp passed in to login: {}", timestampStr); + return null; + } + } + + if (unsignedRequestBuffer.length() != 0) { + unsignedRequestBuffer.append("&"); + } + unsignedRequestBuffer.append(paramName).append("=").append(URLEncoder.encode(paramValue, "UTF-8")); + } + } + + if ((signature == null) || (timestamp == 0L)) { + if (logger.isDebugEnabled()) { + logger.debug("Missing parameters in login request, signature = " + signature + ", timestamp = " + timestamp); + } + return null; + } + + unsignedRequest = unsignedRequestBuffer.toString().toLowerCase().replaceAll("\\+", "%20"); + + Mac mac = Mac.getInstance("HmacSHA1"); + SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "HmacSHA1"); + mac.init(keySpec); + mac.update(unsignedRequest.getBytes()); + byte[] encryptedBytes = mac.doFinal(); + String computedSignature = new String(Base64.encodeBase64(encryptedBytes)); + boolean equalSig = ConstantTimeComparator.compareStrings(signature, computedSignature); + if (!equalSig) { + logger.info("User signature: " + signature + " is not equaled to computed signature: " + computedSignature); + } else { + user = _userAccountDao.getUserAccount(username, domainId); + } + } catch (Exception ex) { + logger.error("Exception authenticating user", ex); + return null; + } + + return user; + } + protected void updateLoginAttemptsWhenIncorrectLoginAttemptsEnabled(UserAccount account, boolean updateIncorrectLoginCount, int allowedLoginAttempts) { int attemptsMade = account.getLoginAttempts() + 1;