Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ Licensed to the Apache Software Foundation (ASF) under one
import java.util.WeakHashMap;



/**
* Default implementation for {@link UserManager}.
*
Expand All @@ -79,7 +80,7 @@ Licensed to the Apache Software Foundation (ASF) under one
public class DefaultUserManager implements UserManager {

private static final String USERDATABASE_PACKAGE = "org.apache.wiki.auth.user";
private static final String SESSION_MESSAGES = "profile";
public static final String SESSION_MESSAGES = "profile";
private static final String PARAM_EMAIL = "email";
private static final String PARAM_FULLNAME = "fullname";
private static final String PARAM_PASSWORD = "password";
Expand Down Expand Up @@ -344,25 +345,40 @@ public void validateProfile( final Context context, final UserProfile profile )
// passwords must match and can't be null

//this is the new password
final String password = profile.getPassword();
if( password == null ) {
final String newpassword = profile.getPassword();
if( newpassword == null ) {
session.addMessage( SESSION_MESSAGES, rb.getString( "security.error.blankpassword" ) );
} else {
final HttpServletRequest request = context.getHttpRequest();
//the existing password
final String password0 = ( request == null ) ? null : request.getParameter( "password0" );
final String existingPassword = ( request == null ) ? null : request.getParameter( "password0" );
//the new password confirmation
final String password2 = ( request == null ) ? null : request.getParameter( "password2" );
if( !password.equals( password2 ) ) {
final String passwordConfirmation = ( request == null ) ? null : request.getParameter( "password2" );
if (!newpassword.equals(passwordConfirmation)) {
//password confirmation does not match
session.addMessage( SESSION_MESSAGES, rb.getString( "security.error.passwordnomatch" ) );
}
if( !profile.isNew() && !getUserDatabase().validatePassword( profile.getLoginName(), password0 ) ) {
if( !profile.isNew() && (existingPassword==null || existingPassword.equals( newpassword ) ) ) {
//existing account and the existing password matches the new password
session.addMessage( SESSION_MESSAGES, "existing password matches the proposed new one" );
}
if( !profile.isNew() && !getUserDatabase().validatePassword( profile.getLoginName(), existingPassword ) ) {
//existing account and the provided password does not match what we currently have
session.addMessage( SESSION_MESSAGES, rb.getString( "security.error.passwordnomatch" ) );
}
List<String> msg = PasswordComplexityVeriffier.validate(password2, password0, context);
List<String> msg = PasswordComplexityVeriffier.validate(passwordConfirmation, existingPassword, context);
for (String s : msg) {
session.addMessage( SESSION_MESSAGES, s );
}
int reuseCount = Integer.parseInt(m_engine.getWikiProperties().getProperty("jspwiki.credentials.reuseCount", "-1"));
if (reuseCount > 0) {
//if it's set to 0 or less, we don't store it so we can skip this check
if (!m_database.validatePasswordReuse(profile.getLoginName(), passwordConfirmation)) {
//password reuse detected
session.addMessage(SESSION_MESSAGES,
MessageFormat.format(rb.getString("security.error.passwordReuseError"), reuseCount));
}
}
}
}

Expand All @@ -373,7 +389,7 @@ public void validateProfile( final Context context, final UserProfile profile )

// It's illegal to use as a full name someone else's login name
try {
otherProfile = getUserDatabase().find( fullName );
otherProfile = getUserDatabase().findByFullName(fullName );
if( otherProfile != null && !profile.equals( otherProfile ) && !fullName.equals( otherProfile.getFullname() ) ) {
final Object[] args = { fullName };
session.addMessage( SESSION_MESSAGES, MessageFormat.format( rb.getString( "security.error.illegalfullname" ), args ) );
Expand All @@ -382,7 +398,7 @@ public void validateProfile( final Context context, final UserProfile profile )

// It's illegal to use as a login name someone else's full name
try {
otherProfile = getUserDatabase().find( loginName );
otherProfile = getUserDatabase().findByLoginName(loginName );
if( otherProfile != null && !profile.equals( otherProfile ) && !loginName.equals( otherProfile.getLoginName() ) ) {
final Object[] args = { loginName };
session.addMessage( SESSION_MESSAGES, MessageFormat.format( rb.getString( "security.error.illegalloginname" ), args ) );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ public static List<String> validate(String pwd, String previousPwd, Context cont
//perhaps a regex pattern can be added in the future

List<String> problems = new ArrayList<>();
if (pwd == null) {
problems.add(MessageFormat.format(rb.getString("pwdcheck.tooshort"), minLength));
return problems;
}
if (pwd.length() > maxLength) {
problems.add(MessageFormat.format(rb.getString("pwdcheck.toolong"), maxLength));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,9 @@ public interface UserManager extends Initializable {

/**
* <p>
* Saves the {@link org.apache.wiki.auth.user.UserProfile} for the user in a wiki session. This method verifies that a user profile to
* be saved doesn't collide with existing profiles; that is, the login name or full name is already used by another profile. If the
* profile collides, a <code>DuplicateUserException</code> is thrown. After saving the profile, the user database changes are committed,
* Saves the {@link org.apache.wiki.auth.user.UserProfile} for the user in a wiki session.This method verifies that a user profile to
be saved doesn't collide with existing profiles; that is, the login name or full name is already used by another profile. If the
profile collides, a <code>DuplicateUserException</code> is thrown. After saving the profile, the user database changes are committed,
* and the user's credential set is refreshed; if custom authentication is used, this means the user will be automatically be logged in.
* </p>
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,41 @@ public boolean validatePassword( final String loginName, final String password )
}
return false;
}

@Override
public boolean validatePasswordReuse( final String loginName, final String password ) {
try {
final UserProfile profile = findByLoginName( loginName );

// If the password is stored as SHA-256 or SSHA, verify the hash

for (String storedPassword : profile.getPreviousHashedCredentials()) {
if (storedPassword.startsWith(SHA256_PREFIX) || storedPassword.startsWith(SSHA_PREFIX)) {
boolean match = CryptoUtil.verifySaltedPassword(password.getBytes(StandardCharsets.UTF_8), storedPassword);
if (match) {
return false;
}
}
if (storedPassword.startsWith(SHA_PREFIX)) {
String fragment = storedPassword.substring(SHA_PREFIX.length());
String hashedPassword = getShaHash(password);
boolean match = hashedPassword.equals(fragment);
if (match) {
return false;
}
}
}

return true;
} catch( final NoSuchPrincipalException e ) {
LOG.debug(e.getMessage(), e);
} catch( final NoSuchAlgorithmException e ) {
LOG.error( "Unsupported algorithm: " + e.getMessage() );
} catch( final WikiSecurityException e ) {
LOG.error( "Could not upgrade SHA password to SSHA because profile could not be saved. Reason: " + e.getMessage(), e );
}
return true;
}

/**
* Generates a new random user identifier (uid) that is guaranteed to be unique.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ Licensed to the Apache Software Foundation (ASF) under one

import jakarta.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


Expand All @@ -48,6 +50,12 @@ public final class DefaultUserProfile implements UserProfile {
private String password;
private String uid;
private String wikiname;
//oldest should be in position zero, size bounded via configuration
private List<String> previousHashedCredentials = new ArrayList<>();

public List<String> getPreviousHashedCredentials() {
return previousHashedCredentials;
}

/**
* Package constructor to allow direct instantiation only from package related classes (i.e., AbstractUserDatabase).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Licensed to the Apache Software Foundation (ASF) under one
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.apache.wiki.WikiEngine;

/**
* <p>
Expand Down Expand Up @@ -192,6 +193,8 @@ public class JDBCUserDatabase extends AbstractUserDatabase {
private static final String NOTHING = "";

public static final String DEFAULT_DB_ATTRIBUTES = "attributes";

public static final String DEFAULT_DB_OLD_HASHES = "oldhashes";

public static final String DEFAULT_DB_CREATED = "created";

Expand Down Expand Up @@ -220,6 +223,8 @@ public class JDBCUserDatabase extends AbstractUserDatabase {
public static final String DEFAULT_DB_WIKI_NAME = "wiki_name";

public static final String PROP_DB_ATTRIBUTES = "jspwiki.userdatabase.attributes";

public static final String PROP_DB_OLD_HASHES = "jspwiki.userdatabase.oldhashes";

public static final String PROP_DB_CREATED = "jspwiki.userdatabase.created";

Expand Down Expand Up @@ -298,6 +303,10 @@ public class JDBCUserDatabase extends AbstractUserDatabase {
private String m_modified;

private boolean m_supportsCommits;

private String m_oldPasswords;

private int m_passwordReusedCount = 0;

/**
* Looks up and deletes the first {@link UserProfile} in the user database
Expand Down Expand Up @@ -433,6 +442,8 @@ public void initialize( final Engine engine, final Properties props ) throws NoR
m_created = props.getProperty( PROP_DB_CREATED, DEFAULT_DB_CREATED );
m_modified = props.getProperty( PROP_DB_MODIFIED, DEFAULT_DB_MODIFIED );
m_attributes = props.getProperty( PROP_DB_ATTRIBUTES, DEFAULT_DB_ATTRIBUTES );
m_oldPasswords = props.getProperty( PROP_DB_OLD_HASHES, DEFAULT_DB_OLD_HASHES );
m_passwordReusedCount = Integer.parseInt(props.getProperty("jspwiki.credentials.reuseCount", "-1"));

m_findAll = "SELECT * FROM " + userTable;
m_findByEmail = "SELECT * FROM " + userTable + " WHERE " + m_email + "=?";
Expand All @@ -451,8 +462,9 @@ public void initialize( final Engine engine, final Properties props ) throws NoR
+ m_modified + ","
+ m_loginName + ","
+ m_attributes + ","
+ m_created
+ ") VALUES (?,?,?,?,?,?,?,?,?)";
+ m_created + ","
+ m_oldPasswords
+ ") VALUES (?,?,?,?,?,?,?,?,?,?)";

// The user update SQL prepared statement
m_updateProfile = "UPDATE " + userTable + " SET "
Expand All @@ -464,7 +476,8 @@ public void initialize( final Engine engine, final Properties props ) throws NoR
+ m_modified + "=?,"
+ m_loginName + "=?,"
+ m_attributes + "=?,"
+ m_lockExpiry + "=? "
+ m_lockExpiry + "=?,"
+ m_oldPasswords + "=? "
+ "WHERE " + m_loginName + "=?";

// Prepare the role insert SQL
Expand Down Expand Up @@ -562,6 +575,8 @@ public void rename( final String loginName, final String newName ) throws Duplic
}

/**
* @param profile
* @throws org.apache.wiki.auth.WikiSecurityException
* @see org.apache.wiki.auth.user.UserDatabase#save(org.apache.wiki.auth.user.UserProfile)
*/
@Override
Expand Down Expand Up @@ -592,6 +607,13 @@ public void save( final UserProfile profile ) throws WikiSecurityException {
// If password changed, hash it before we save
if( !Strings.CS.equals( password, existingPassword ) ) {
password = getHash( password );
//add the hashed password
profile.getPreviousHashedCredentials().add(password);
while (!profile.getPreviousHashedCredentials().isEmpty() &&
profile.getPreviousHashedCredentials().size() > m_passwordReusedCount) {
profile.getPreviousHashedCredentials().remove(0);
}

}

try( final Connection conn = m_ds.getConnection();
Expand Down Expand Up @@ -620,7 +642,9 @@ public void save( final UserProfile profile ) throws WikiSecurityException {
} catch ( final IOException e ) {
throw new WikiSecurityException( "Could not save user profile attribute. Reason: " + e.getMessage(), e );
}

ps1.setTimestamp( 9, ts );
ps1.setString(10, StringUtils.join(profile.getPreviousHashedCredentials(), "|"));
ps1.execute();

// Insert new role record
Expand Down Expand Up @@ -655,7 +679,9 @@ public void save( final UserProfile profile ) throws WikiSecurityException {
throw new WikiSecurityException( "Could not save user profile attribute. Reason: " + e.getMessage(), e );
}
ps4.setDate( 9, lockExpiry );
ps4.setString( 10, profile.getLoginName() );
ps4.setString(10, StringUtils.join(profile.getPreviousHashedCredentials(), "|"));
ps4.setString( 11, profile.getLoginName() );

ps4.execute();
}
// Set the profile mod time
Expand Down Expand Up @@ -731,6 +757,13 @@ private UserProfile findByPreparedStatement( final String sql, final Object inde
LOG.error( "Could not parse user profile attributes!", e );
}
}
String oldhashes = rs.getString(m_oldPasswords);
if (oldhashes != null && oldhashes.length() > 0) {
String[] parts = oldhashes.split("\\|");
for (String s : parts) {
profile.getPreviousHashedCredentials().add(s);
}
}
found = true;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ public interface UserDatabase {
* contain any profiles, this method will return a zero-length array.
*
* @return the WikiNames
* @throws org.apache.wiki.auth.WikiSecurityException
*/
Principal[] getWikiNames() throws WikiSecurityException;

Expand All @@ -77,6 +78,8 @@ public interface UserDatabase {
* that supplied the name is unknown.
*
* @param index the login name, full name, or wiki name
* @return User profile
* @throws org.apache.wiki.auth.NoSuchPrincipalException
*/
UserProfile find( String index ) throws NoSuchPrincipalException;

Expand All @@ -86,6 +89,7 @@ public interface UserDatabase {
*
* @param index the e-mail address of the desired user profile
* @return the user profile
* @throws org.apache.wiki.auth.NoSuchPrincipalException
*/
UserProfile findByEmail( String index ) throws NoSuchPrincipalException;

Expand All @@ -95,6 +99,7 @@ public interface UserDatabase {
*
* @param index the login name of the desired user profile
* @return the user profile
* @throws org.apache.wiki.auth.NoSuchPrincipalException
*/
UserProfile findByLoginName( String index ) throws NoSuchPrincipalException;

Expand All @@ -104,6 +109,7 @@ public interface UserDatabase {
*
* @param uid the unique identifier of the desired user profile
* @return the user profile
* @throws org.apache.wiki.auth.NoSuchPrincipalException
* @since 2.8
*/
UserProfile findByUid( String uid ) throws NoSuchPrincipalException;
Expand All @@ -114,6 +120,7 @@ public interface UserDatabase {
*
* @param index the wiki name of the desired user profile
* @return the user profile
* @throws org.apache.wiki.auth.NoSuchPrincipalException
*/
UserProfile findByWikiName( String index ) throws NoSuchPrincipalException;

Expand All @@ -123,15 +130,21 @@ public interface UserDatabase {
*
* @param index the fill name of the desired user profile
* @return the user profile
* @throws org.apache.wiki.auth.NoSuchPrincipalException
*/
UserProfile findByFullName( String index ) throws NoSuchPrincipalException;

/** Initializes the user database based on values from a Properties object. */
/** Initializes the user database based on values from a Properties object.
* @param engine
* @param props
* @throws org.apache.wiki.api.exceptions.NoRequiredPropertyException
* @throws org.apache.wiki.auth.WikiSecurityException */
void initialize( Engine engine, Properties props ) throws NoRequiredPropertyException, WikiSecurityException;

/**
* Factory method that instantiates a new user profile. The {@link UserProfile#isNew()} method of profiles created using
* this method should return <code>true</code>.
* @return user profile
*/
UserProfile newProfile();

Expand Down Expand Up @@ -181,4 +194,12 @@ public interface UserDatabase {
*/
boolean validatePassword( String loginName, String password );

/**
* validates that the proposed password has not been recently used.
* @param loginName
* @param password
* @return false if the password has been recently used, true otherwise
* @since 3.0.0
*/
boolean validatePasswordReuse( final String loginName, final String password );
}
Loading
Loading