mirror of
https://github.com/apache/cloudstack.git
synced 2025-11-02 20:02:29 +01:00
CLOUDSTACK-5237: Add a default PBKDF2-SHA-256 based authenticator
Signed-off-by: Rohit Yadav <rohit.yadav@shapeblue.com> (cherry picked from commit 9533c54db669b22b268fcc21766e21c231e48d84) Signed-off-by: Rohit Yadav <rohit.yadav@shapeblue.com>
This commit is contained in:
parent
d8e1bf1506
commit
6f4db0ce4b
@ -75,6 +75,11 @@
|
||||
<artifactId>cloud-plugin-user-authenticator-md5</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.cloudstack</groupId>
|
||||
<artifactId>cloud-plugin-user-authenticator-pbkdf2</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.cloudstack</groupId>
|
||||
<artifactId>cloud-plugin-user-authenticator-plaintext</artifactId>
|
||||
|
||||
@ -33,7 +33,7 @@
|
||||
class="org.apache.cloudstack.spring.lifecycle.registry.ExtensionRegistry">
|
||||
<property name="orderConfigKey" value="user.authenticators.order" />
|
||||
<property name="excludeKey" value="user.authenticators.exclude" />
|
||||
<property name="orderConfigDefault" value="SHA256SALT,MD5,LDAP,SAML2,PLAINTEXT" />
|
||||
<property name="orderConfigDefault" value="PBKDF2,SHA256SALT,MD5,LDAP,SAML2,PLAINTEXT" />
|
||||
</bean>
|
||||
|
||||
<bean id="pluggableAPIAuthenticatorsRegistry"
|
||||
@ -47,7 +47,7 @@
|
||||
class="org.apache.cloudstack.spring.lifecycle.registry.ExtensionRegistry">
|
||||
<property name="orderConfigKey" value="user.password.encoders.order" />
|
||||
<property name="excludeKey" value="user.password.encoders.exclude" />
|
||||
<property name="orderConfigDefault" value="SHA256SALT,MD5,LDAP,PLAINTEXT" />
|
||||
<property name="orderConfigDefault" value="PBKDF2,SHA256SALT,MD5,LDAP,SAML2,PLAINTEXT" />
|
||||
</bean>
|
||||
|
||||
<bean id="securityCheckersRegistry"
|
||||
|
||||
@ -71,6 +71,7 @@
|
||||
<module>storage-allocators/random</module>
|
||||
<module>user-authenticators/ldap</module>
|
||||
<module>user-authenticators/md5</module>
|
||||
<module>user-authenticators/pbkdf2</module>
|
||||
<module>user-authenticators/plain-text</module>
|
||||
<module>user-authenticators/saml2</module>
|
||||
<module>user-authenticators/sha256salted</module>
|
||||
|
||||
29
plugins/user-authenticators/pbkdf2/pom.xml
Normal file
29
plugins/user-authenticators/pbkdf2/pom.xml
Normal file
@ -0,0 +1,29 @@
|
||||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>cloud-plugin-user-authenticator-pbkdf2</artifactId>
|
||||
<name>Apache CloudStack Plugin - User Authenticator PBKDF2-SHA-256</name>
|
||||
<parent>
|
||||
<groupId>org.apache.cloudstack</groupId>
|
||||
<artifactId>cloudstack-plugins</artifactId>
|
||||
<version>4.5.0-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
</project>
|
||||
@ -0,0 +1,18 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
name=pbkdf2
|
||||
parent=api
|
||||
@ -0,0 +1,32 @@
|
||||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:context="http://www.springframework.org/schema/context"
|
||||
xmlns:aop="http://www.springframework.org/schema/aop"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans
|
||||
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
|
||||
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
|
||||
http://www.springframework.org/schema/context
|
||||
http://www.springframework.org/schema/context/spring-context-3.0.xsd"
|
||||
>
|
||||
<bean id="PBKDF2UserAuthenticator" class="org.apache.cloudstack.server.auth.PBKDF2UserAuthenticator">
|
||||
<property name="name" value="PBKDF2"/>
|
||||
</bean>
|
||||
</beans>
|
||||
@ -0,0 +1,143 @@
|
||||
// Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
// contributor license agreements. See the NOTICE file distributed with
|
||||
// this work for additional information regarding copyright ownership.
|
||||
// The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
// (the "License"); you may not use this file except in compliance with
|
||||
// the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package org.apache.cloudstack.server.auth;
|
||||
|
||||
import com.cloud.server.auth.DefaultUserAuthenticator;
|
||||
import com.cloud.server.auth.UserAuthenticator;
|
||||
import com.cloud.user.UserAccount;
|
||||
import com.cloud.user.dao.UserAccountDao;
|
||||
import com.cloud.utils.ConstantTimeComparator;
|
||||
import com.cloud.utils.Pair;
|
||||
import com.cloud.utils.exception.CloudRuntimeException;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.apache.log4j.Logger;
|
||||
import org.bouncycastle.crypto.PBEParametersGenerator;
|
||||
import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator;
|
||||
import org.bouncycastle.crypto.params.KeyParameter;
|
||||
import org.bouncycastle.util.encoders.Base64;
|
||||
|
||||
import javax.ejb.Local;
|
||||
import javax.inject.Inject;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.util.Map;
|
||||
|
||||
import static java.lang.String.format;
|
||||
|
||||
@Local({UserAuthenticator.class})
|
||||
public class PBKDF2UserAuthenticator extends DefaultUserAuthenticator {
|
||||
public static final Logger s_logger = Logger.getLogger(PBKDF2UserAuthenticator.class);
|
||||
private static final int s_saltlen = 64;
|
||||
private static final int s_rounds = 100000;
|
||||
private static final int s_keylen = 512;
|
||||
|
||||
@Inject
|
||||
private UserAccountDao _userAccountDao;
|
||||
|
||||
public Pair<Boolean, UserAuthenticator.ActionOnFailedAuthentication> authenticate(String username, String password, Long domainId, Map<String, Object[]> requestParameters) {
|
||||
if (s_logger.isDebugEnabled()) {
|
||||
s_logger.debug("Retrieving user: " + username);
|
||||
}
|
||||
boolean isValidUser = false;
|
||||
UserAccount user = this._userAccountDao.getUserAccount(username, domainId);
|
||||
if (user != null) {
|
||||
isValidUser = true;
|
||||
} else {
|
||||
s_logger.debug("Unable to find user with " + username + " in domain " + domainId);
|
||||
}
|
||||
|
||||
byte[] salt = new byte[0];
|
||||
int rounds = s_rounds;
|
||||
try {
|
||||
if (isValidUser) {
|
||||
String[] storedPassword = user.getPassword().split(":");
|
||||
if ((storedPassword.length != 3) || (!StringUtils.isNumeric(storedPassword[2]))) {
|
||||
s_logger.warn("The stored password for " + username + " isn't in the right format for this authenticator");
|
||||
isValidUser = false;
|
||||
} else {
|
||||
// Encoding format = <salt>:<password hash>:<rounds>
|
||||
salt = decode(storedPassword[0]);
|
||||
rounds = Integer.parseInt(storedPassword[2]);
|
||||
}
|
||||
}
|
||||
boolean result = false;
|
||||
if (isValidUser && validateCredentials(password, salt)) {
|
||||
result = ConstantTimeComparator.compareStrings(user.getPassword(), encode(password, salt, rounds));
|
||||
}
|
||||
|
||||
UserAuthenticator.ActionOnFailedAuthentication action = null;
|
||||
if ((!result) && (isValidUser)) {
|
||||
action = UserAuthenticator.ActionOnFailedAuthentication.INCREMENT_INCORRECT_LOGIN_ATTEMPT_COUNT;
|
||||
}
|
||||
return new Pair(Boolean.valueOf(result), action);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new CloudRuntimeException("Unable to hash password", e);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new CloudRuntimeException("Unable to hash password", e);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new CloudRuntimeException("Unable to hash password", e);
|
||||
} catch (InvalidKeySpecException e) {
|
||||
throw new CloudRuntimeException("Unable to hash password", e);
|
||||
}
|
||||
}
|
||||
|
||||
public String encode(String password)
|
||||
{
|
||||
try
|
||||
{
|
||||
return encode(password, makeSalt(), s_rounds);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new CloudRuntimeException("Unable to hash password", e);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new CloudRuntimeException("Unable to hash password", e);
|
||||
} catch (InvalidKeySpecException e) {
|
||||
s_logger.error("Exception in EncryptUtil.createKey ", e);
|
||||
throw new CloudRuntimeException("Unable to hash password", e);
|
||||
}
|
||||
}
|
||||
|
||||
public String encode(String password, byte[] salt, int rounds)
|
||||
throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeySpecException {
|
||||
PKCS5S2ParametersGenerator generator = new PKCS5S2ParametersGenerator();
|
||||
generator.init(PBEParametersGenerator.PKCS5PasswordToBytes(
|
||||
password.toCharArray()),
|
||||
salt,
|
||||
rounds);
|
||||
return format("%s:%s:%d", encode(salt),
|
||||
encode(((KeyParameter)generator.generateDerivedParameters(s_keylen)).getKey()), rounds);
|
||||
}
|
||||
|
||||
public static byte[] makeSalt() throws NoSuchAlgorithmException {
|
||||
SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
|
||||
byte[] salt = new byte[s_saltlen];
|
||||
sr.nextBytes(salt);
|
||||
return salt;
|
||||
}
|
||||
|
||||
private static boolean validateCredentials(String plainPassword, byte[] hash) {
|
||||
return !(plainPassword == null || plainPassword.isEmpty() || hash == null || hash.length == 0);
|
||||
}
|
||||
|
||||
private static String encode(byte[] input) {
|
||||
return new String(Base64.encode(input));
|
||||
}
|
||||
|
||||
private static byte[] decode(String input) throws UnsupportedEncodingException {
|
||||
return Base64.decode(input.getBytes("UTF-8"));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
// Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
// contributor license agreements. See the NOTICE file distributed with
|
||||
// this work for additional information regarding copyright ownership.
|
||||
// The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
// (the "License"); you may not use this file except in compliance with
|
||||
// the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package org.apache.cloudstack.server.auth;
|
||||
|
||||
import com.cloud.server.auth.UserAuthenticator;
|
||||
import com.cloud.user.UserAccountVO;
|
||||
import com.cloud.user.dao.UserAccountDao;
|
||||
import com.cloud.utils.Pair;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class PBKD2UserAuthenticatorTest {
|
||||
@Mock
|
||||
UserAccountDao dao;
|
||||
|
||||
@Test
|
||||
public void encodePasswordTest() {
|
||||
PBKDF2UserAuthenticator authenticator = new PBKDF2UserAuthenticator();
|
||||
String encodedPassword = authenticator.encode("password123ABCS!@#$%");
|
||||
Assert.assertTrue(encodedPassword.length() < 255 && encodedPassword.length() >= 182);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saltTest() throws NoSuchAlgorithmException {
|
||||
byte[] salt = new PBKDF2UserAuthenticator().makeSalt();
|
||||
Assert.assertTrue(salt.length > 16);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateValidTest() throws IllegalAccessException, NoSuchFieldException {
|
||||
PBKDF2UserAuthenticator authenticator = new PBKDF2UserAuthenticator();
|
||||
Field daoField = PBKDF2UserAuthenticator.class.getDeclaredField("_userAccountDao");
|
||||
daoField.setAccessible(true);
|
||||
daoField.set(authenticator, dao);
|
||||
UserAccountVO account = new UserAccountVO();
|
||||
account.setPassword("FMDMdx/2QjrZniqNRAgOAC1ai/CY/C+2kmKhp3vo+98pkqhO+AR6hCyUl0bOXtkq3XWqNiSQTwbi7KTiwuWhyw==:+u8T5LzCtikCPvKnUDn6JDezf1Hg2bood/ke5Oo93pz9s1eD9k/JLsa497Z3h9QWfOQfq0zvCRmkzfXMF913vQ==:4096");
|
||||
Mockito.when(dao.getUserAccount(Mockito.anyString(), Mockito.anyLong())).thenReturn(account);
|
||||
Pair<Boolean, UserAuthenticator.ActionOnFailedAuthentication> pair = authenticator.authenticate("admin", "password", 1l, null);
|
||||
Assert.assertTrue(pair.first());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticateInValidTest() throws IllegalAccessException, NoSuchFieldException {
|
||||
PBKDF2UserAuthenticator authenticator = new PBKDF2UserAuthenticator();
|
||||
Field daoField = PBKDF2UserAuthenticator.class.getDeclaredField("_userAccountDao");
|
||||
daoField.setAccessible(true);
|
||||
daoField.set(authenticator, dao);
|
||||
UserAccountVO account = new UserAccountVO();
|
||||
account.setPassword("5f4dcc3b5aa765d61d8327deb882cf99");
|
||||
Mockito.when(dao.getUserAccount(Mockito.anyString(), Mockito.anyLong())).thenReturn(account);
|
||||
Pair<Boolean, UserAuthenticator.ActionOnFailedAuthentication> pair = authenticator.authenticate("admin", "password", 1l, null);
|
||||
Assert.assertFalse(pair.first());
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user