<bean id="daoAuthenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
<property name="userDetailsService" ref="userDetailsService" />
+ <property name="saltSource" ref="saltSource"/>
<property name="passwordEncoder">
<bean class="org.springframework.security.authentication.encoding.ShaPasswordEncoder">
<constructor-arg value="256"/>
</bean>
</property>
</bean>
+
+ <bean id="saltSource" class="org.collectionspace.authentication.CSpaceSaltSource">
+ <property name="userPropertyToUse" value="salt" />
+ </bean>
<bean id="userDetailsService" class="org.collectionspace.authentication.spring.CSpaceUserDetailsService">
<constructor-arg>
<util:map>
<entry key="dsJndiName" value="CspaceDS" />
<entry key="principalsQuery" value="select passwd from users where username=?" />
+ <entry key="saltQuery" value="select salt from users where username=?" />
<entry key="rolesQuery" value="select r.rolename from roles as r, accounts_roles as ar where ar.user_id=? and ar.role_id=r.csid" />
<entry key="tenantsQueryWithDisabled" value="select t.id, t.name from accounts_common as a, accounts_tenants as at, tenants as t where a.userid=? and a.csid = at.TENANTS_ACCOUNTS_COMMON_CSID and at.tenant_id = t.id order by t.id" />
<entry key="tenantsQueryNoDisabled" value="select t.id, t.name from accounts_common as a, accounts_tenants as at, tenants as t where a.userid=? and a.csid = at.TENANTS_ACCOUNTS_COMMON_CSID and at.tenant_id = t.id and NOT t.disabled order by t.id" />
<artifactId>property-listener-injector</artifactId>
</dependency>
<!-- External dependencies -->
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-java</artifactId>
- </dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
+ <version>${postgres.driver.version}</version>
</dependency>
<!-- CollectionSpace dependencies -->
<dependency>
</componentProperties>
</configuration>
<dependencies>
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-java</artifactId>
- </dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
+ <version>${postgres.driver.version}</version>
</dependency>
</dependencies>
</plugin>
throw new BadRequestException(e.getMessage());
}
String secEncPasswd = SecurityUtils.createPasswordHash(
- userId, new String(password));
+ userId, new String(password), null);
return secEncPasswd;
}
}
package org.collectionspace.services.account.storage.csidp;
import java.util.Date;
+import java.util.UUID;
+
import javax.persistence.Query;
import org.collectionspace.services.authentication.User;
public User create(String userId, byte[] password) throws Exception {
User user = new User();
user.setUsername(userId);
- user.setPasswd(getEncPassword(userId, password));
+ String salt = UUID.randomUUID().toString();
+ user.setPasswd(getEncPassword(userId, password, salt));
+ user.setSalt(salt);
user.setCreatedAtItem(new Date());
return user;
}
throws DocumentNotFoundException, Exception {
User userFound = get(jpaTransactionContext, userId);
if (userFound != null) {
- userFound.setPasswd(getEncPassword(userId, password));
+ userFound.setPasswd(getEncPassword(userId, password, userFound.getSalt()));
userFound.setUpdatedAtItem(new Date());
if (logger.isDebugEnabled()) {
logger.debug("updated user=" + JaxbUtils.toString(userFound, User.class));
}
}
- private String getEncPassword(String userId, byte[] password) throws BadRequestException {
+ private String getEncPassword(String userId, byte[] password, String salt) throws BadRequestException {
//jaxb unmarshaller already unmarshal xs:base64Binary, no need to b64 decode
//byte[] bpass = Base64.decodeBase64(accountReceived.getPassword());
try {
throw new BadRequestException(e.getMessage());
}
String secEncPasswd = SecurityUtils.createPasswordHash(
- userId, new String(password));
+ userId, new String(password), salt);
return secEncPasswd;
}
}
</xs:appinfo>
</xs:annotation>
</xs:element>
+ <xs:element name="salt" type="xs:string" minOccurs="1" maxOccurs="1">
+ <xs:annotation>
+ <xs:appinfo>
+ <hj:basic>
+ <orm:column name="salt" length="128" nullable="false" default=""/>
+ </hj:basic>
+ </xs:appinfo>
+ </xs:annotation>
+ </xs:element>
<xs:element name="createdAt" type="xs:dateTime">
<xs:annotation>
<xs:appinfo>
</xs:appinfo>
</xs:annotation>
</xs:element>
-
+ <xs:element name="lastLogin" type="xs:dateTime">
+ <xs:annotation>
+ <xs:appinfo>
+ <hj:basic>
+ <orm:column name="lastLogin" />
+ </hj:basic>
+ </xs:appinfo>
+ </xs:annotation>
+ </xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<property name="src.hibernate.cfg" value="${basedir}/src/test/resources/hibernate.cfg.xml"/>
<property name="dest.hibernate.cfg" value="${basedir}/target/test-classes/hibernate.cfg.xml"/>
<delete file="${dest.hibernate.cfg}" verbose="true" />
- <filter token="DB_CSPACE_URL" value="${db.jdbc.cspace.url}" />
+ <filter token="DB_CSPACE_URL" value="${db.jdbc.cspace.url.encoded}" />
<filter token="DB_DRIVER_CLASS" value="${db.jdbc.driver.class}" />
<filter token="DB_CSPACE_USER" value="${db.cspace.user}" />
<filter token="DB_CSPACE_PASSWORD" value="${env.DB_CSPACE_PASSWORD}" /> <!-- double-sub from ${db.cspace.user.password} fails -->
</componentProperties>
</configuration>
<dependencies>
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-java</artifactId>
- </dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
+ <version>${postgres.driver.version}</version>
</dependency>
</dependencies>
</plugin>
CREATE TABLE IF NOT EXISTS users (
username VARCHAR(128) NOT NULL PRIMARY KEY,
created_at TIMESTAMP NOT NULL,
+ lastLogin TIMESTAMP,
passwd VARCHAR(128) NOT NULL,
+ salt VARCHAR(128)
updated_at TIMESTAMP
);
http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_1_0.xsd" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:orm="http://java.sun.com/xml/ns/persistence/orm" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<persistence-unit name="org.collectionspace.services.authentication">
<class>org.collectionspace.services.authentication.User</class>
+ <class>org.collectionspace.services.authentication.Token</class>
<properties>
<property name="hibernate.ejb.cfgfile" value="hibernate.cfg.xml"/>
<version>${spring.security.oauth2.version}</version>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>org.postgresql</groupId>
+ <artifactId>postgresql</artifactId>
+ <scope>provided</scope>
+ </dependency>
</dependencies>
<build>
--- /dev/null
+package org.collectionspace.authentication;
+
+import org.springframework.security.authentication.dao.ReflectionSaltSource;
+import org.springframework.security.core.userdetails.UserDetails;
+
+public class CSpaceSaltSource extends ReflectionSaltSource {
+
+ @Override
+ public Object getSalt(UserDetails user) {
+ return super.getSalt(user);
+ }
+
+}
private Set<CSpaceTenant> tenants;
private CSpaceTenant primaryTenant;
+ private String salt;
/**
* Creates a CSpaceUser with the given username, hashed password, associated
* @param tenants the tenants associated with the user
* @param authorities the authorities that have been granted to the user
*/
- public CSpaceUser(String username, String password,
+ public CSpaceUser(String username, String password, String salt,
Set<CSpaceTenant> tenants,
Set<? extends GrantedAuthority> authorities) {
authorities);
this.tenants = tenants;
+ this.salt = salt;
if (!tenants.isEmpty()) {
primaryTenant = tenants.iterator().next();
}
}
-
+
/**
* Retrieves the tenants associated with the user.
*
return primaryTenant;
}
+ /**
+ * Returns a "salt" string to use when encrypting a user's password
+ * @return
+ */
+ public String getSalt() {
+ return salt != null ? salt : "";
+ }
+
}
* Interface for the CollectionSpace realm.
*/
public interface CSpaceRealm {
+
+ /**
+ * Retrieves the "salt" used to encrypt the user's password
+ * @param username
+ * @return
+ * @throws AccountException
+ */
+ public String getSalt(String username) throws AccountException;
/**
* Retrieves the hashed password used to authenticate a user.
import org.collectionspace.authentication.AuthN;
import org.collectionspace.authentication.CSpaceTenant;
import org.collectionspace.authentication.realm.CSpaceRealm;
+import org.postgresql.util.PSQLState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private String datasourceName;
private String principalsQuery;
+ private String saltQuery;
private String rolesQuery;
private String tenantsQueryNoDisabled;
private String tenantsQueryWithDisabled;
if (tmp != null) {
principalsQuery = tmp.toString();
}
+ tmp = options.get("saltQuery");
+ if (tmp != null) {
+ saltQuery = tmp.toString();
+ }
tmp = options.get("rolesQuery");
if (tmp != null) {
rolesQuery = tmp.toString();
return result;
}
+
+ @Override
+ public String getSalt(String username) throws AccountException {
+ String salt = null;
+ Connection conn = null;
+ PreparedStatement ps = null;
+ ResultSet rs = null;
+ try {
+ conn = getConnection();
+ // Get the salt
+ if (logger.isDebugEnabled()) {
+ logger.debug("Executing query: " + saltQuery + ", with username: " + username);
+ }
+ ps = conn.prepareStatement(saltQuery);
+ ps.setString(1, username);
+ rs = ps.executeQuery();
+ if (rs.next() == false) {
+ if (logger.isDebugEnabled()) {
+ logger.debug(saltQuery + " returned no matches from db");
+ }
+ throw new AccountNotFoundException("No matching username found");
+ }
+
+ salt = rs.getString(1);
+ } catch (SQLException ex) {
+ // Assuming PostgreSQL
+ if (PSQLState.UNDEFINED_COLUMN.getState().equals(ex.getSQLState())) {
+ String msg = "'USERS' table is missing 'salt' column for password encyrption. Assuming existing passwords are unsalted.";
+ logger.warn(msg);
+ } else {
+ AccountException ae = new AccountException("Authentication query failed: " + ex.getLocalizedMessage());
+ ae.initCause(ex);
+ throw ae;
+ }
+ } catch (AccountNotFoundException ex) {
+ throw ex;
+ } catch (Exception ex) {
+ AccountException ae = new AccountException("Unknown Exception");
+ ae.initCause(ex);
+ throw ae;
+ } finally {
+ if (rs != null) {
+ try {
+ rs.close();
+ } catch (SQLException e) {
+ }
+ }
+ if (ps != null) {
+ try {
+ ps.close();
+ } catch (SQLException e) {
+ }
+ }
+ if (conn != null) {
+ try {
+ conn.close();
+ } catch (SQLException ex) {
+ }
+ }
+ }
+
+ return salt;
+ }
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String password = null;
+ String salt = null;
Set<CSpaceTenant> tenants = null;
Set<GrantedAuthority> grantedAuthorities = null;
try {
password = realm.getPassword(username);
+ salt = realm.getSalt(username);
tenants = getTenants(username);
grantedAuthorities = getAuthorities(username);
}
throw new AuthenticationServiceException(e.getMessage(), e);
}
- return
+ CSpaceUser cspaceUser =
new CSpaceUser(
username,
password,
+ salt,
tenants,
grantedAuthorities);
+
+ return cspaceUser;
}
protected Set<GrantedAuthority> getAuthorities(String username) throws AccountException {
<property name="src.hibernate.cfg" value="${basedir}/src/test/resources/hibernate.cfg.xml"/>
<property name="dest.hibernate.cfg" value="${basedir}/target/test-classes/hibernate.cfg.xml"/>
<delete file="${dest.hibernate.cfg}" verbose="true" />
- <filter token="DB_CSPACE_URL" value="${db.jdbc.cspace.url}" />
+ <filter token="DB_CSPACE_URL" value="${db.jdbc.cspace.url.encoded}" />
<filter token="DB_DRIVER_CLASS" value="${db.jdbc.driver.class}" />
<filter token="DB_CSPACE_USER" value="${db.cspace.user}" />
<filter token="DB_CSPACE_PASSWORD" value="${env.DB_CSPACE_PASSWORD}" /> <!-- double-sub from ${db.cspace.user.password} fails -->
</componentProperties>
</configuration>
<dependencies>
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-java</artifactId>
- </dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
+ <version>${postgres.driver.version}</version>
</dependency>
</dependencies>
</plugin>
HashSet<CSpaceTenant> tenantSet = new HashSet<CSpaceTenant>();
tenantSet.add(tenant);
- CSpaceUser principal = new CSpaceUser(user, password, tenantSet, grantedAuthorities);
+ CSpaceUser principal = new CSpaceUser(user, password, null, tenantSet, grantedAuthorities);
Authentication authRequest = new UsernamePasswordAuthenticationToken(principal, password, grantedAuthorities);
SecurityContextHolder.getContext().setAuthentication(authRequest);
final private static String QUERY_USERS_SQL =
"SELECT username FROM users WHERE username LIKE '"
+TENANT_ADMIN_ACCT_PREFIX+"%' OR username LIKE '"+TENANT_READER_ACCT_PREFIX+"%'";
- final private static String INSERT_USER_SQL =
- "INSERT INTO users (username,passwd, created_at) VALUES (?,?, now())";
+ final private static String INSERT_USER_SQL =
+ "INSERT INTO users (username,passwd,salt, created_at) VALUES (?,?,?, now())";
final private static String INSERT_ACCOUNT_SQL =
"INSERT INTO accounts_common "
+ "(csid, email, userid, status, screen_name, metadata_protection, roles_protection, created_at) "
for(String tName : tenantInfo.values()) {
String adminAcctName = getDefaultAdminUserID(tName);
if(!usersInRepo.contains(adminAcctName)) {
+ String salt = UUID.randomUUID().toString();
String secEncPasswd = SecurityUtils.createPasswordHash(
- adminAcctName, DEFAULT_ADMIN_PASSWORD);
+ adminAcctName, DEFAULT_ADMIN_PASSWORD, salt);
pstmt.setString(1, adminAcctName); // set username param
pstmt.setString(2, secEncPasswd); // set passwd param
+ pstmt.setString(3, salt);
if (logger.isDebugEnabled()) {
logger.debug("createDefaultUsersAndAccounts adding user: "
+adminAcctName+" for tenant: "+tName);
String readerAcctName = getDefaultReaderUserID(tName);
if(!usersInRepo.contains(readerAcctName)) {
+ String salt = UUID.randomUUID().toString();
String secEncPasswd = SecurityUtils.createPasswordHash(
- readerAcctName, DEFAULT_READER_PASSWORD);
+ readerAcctName, DEFAULT_READER_PASSWORD, salt);
pstmt.setString(1, readerAcctName); // set username param
pstmt.setString(2, secEncPasswd); // set passwd param
+ pstmt.setString(3, salt);
if (logger.isDebugEnabled()) {
logger.debug("createDefaultUsersAndAccounts adding user: "
+readerAcctName+" for tenant: "+tName);
}
rs.close();
if(!foundTMgrUser) {
+ String salt = UUID.randomUUID().toString();
pstmt = conn.prepareStatement(INSERT_USER_SQL); // create a statement
String secEncPasswd = SecurityUtils.createPasswordHash(
- TENANT_MANAGER_USER, DEFAULT_TENANT_MANAGER_PASSWORD);
+ TENANT_MANAGER_USER, DEFAULT_TENANT_MANAGER_PASSWORD, salt);
pstmt.setString(1, TENANT_MANAGER_USER); // set username param
pstmt.setString(2, secEncPasswd); // set passwd param
+ pstmt.setString(3, salt);
if (logger.isDebugEnabled()) {
logger.debug("findOrCreateTenantManagerUserAndAccount adding tenant manager user: "
+TENANT_MANAGER_USER);
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+
+import org.springframework.security.authentication.encoding.BasePasswordEncoder;
import org.jboss.crypto.digest.DigestCallback;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.security.Base64Encoder;
import org.jboss.security.Base64Utils;
+/**
+ * Extends Spring Security's base class for encoding passwords. We use only the
+ * mergePasswordAndSalt() method.
+ * @author remillet
+ *
+ */
+class CSpacePasswordEncoder extends BasePasswordEncoder {
+ public CSpacePasswordEncoder() {
+ //Do nothing
+ }
+
+ String mergePasswordAndSalt(String password, String salt) {
+ return this.mergePasswordAndSalt(password, salt, false);
+ }
+
+ @Override
+ public String encodePassword(String rawPass, Object salt) {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public boolean isPasswordValid(String encPass, String rawPass, Object salt) {
+ // TODO Auto-generated method stub
+ return false;
+ }
+}
+
/**
*
* @author
public static final String RFC2617_ENCODING = "RFC2617";
private static char MD5_HEX[] = "0123456789abcdef".toCharArray();
-
/**
* createPasswordHash creates password has using configured digest algorithm
* and encoding
* @param password in cleartext
* @return hashed password
*/
- public static String createPasswordHash(String username, String password) {
+ public static String createPasswordHash(String username, String password, String salt) {
//TODO: externalize digest algo and encoding
return createPasswordHash("SHA-256", //digest algo
"base64", //encoding
null, //charset
username,
- password);
+ password,
+ salt);
}
/**
return result;
}
- public static String createPasswordHash(String hashAlgorithm, String hashEncoding, String hashCharset, String username, String password)
+ public static String createPasswordHash(String hashAlgorithm, String hashEncoding, String hashCharset,
+ String username, String password, String salt)
{
- return createPasswordHash(hashAlgorithm, hashEncoding, hashCharset, username, password, null);
+ return createPasswordHash(hashAlgorithm, hashEncoding, hashCharset, username, password, salt, null);
}
- public static String createPasswordHash(String hashAlgorithm, String hashEncoding, String hashCharset, String username, String password, DigestCallback callback)
+ public static String createPasswordHash(String hashAlgorithm, String hashEncoding, String hashCharset,
+ String username, String password, String salt, DigestCallback callback)
{
+ CSpacePasswordEncoder passwordEncoder = new CSpacePasswordEncoder();
+ String saltedPassword = passwordEncoder.mergePasswordAndSalt(password, salt); //
+
String passwordHash = null;
byte passBytes[];
try
{
if(hashCharset == null)
- passBytes = password.getBytes();
+ passBytes = saltedPassword.getBytes();
else
- passBytes = password.getBytes(hashCharset);
+ passBytes = saltedPassword.getBytes(hashCharset);
}
catch(UnsupportedEncodingException uee)
{
logger.error((new StringBuilder()).append("charset ").append(hashCharset).append(" not found. Using platform default.").toString(), uee);
- passBytes = password.getBytes();
+ passBytes = saltedPassword.getBytes();
}
try
{