Is there a way to add salted hashing to my user authentication without breaking my former login server

You have conflicting requirements here. The compatibility requirements forces you to keep the old hashes. The security requirements forces you to drop them. You will have to make a choice here about what requirements to fulfill.

If you decide to keep the backwards compatibility, try making the best out of a bad situation:

  • The old hash and the new hash should be stored in different tables, and the database user that the web application uses should not have read access to the old hash. Use table and/or column permissions for this.
  • As soon as you no longer need the old application, drop the table with the old hash. Ashley Madison famously failed at this point - they upgraded to bcrypt, and then for some idiotic reason they left the old MD5 hashes lying around in the database. When the database was leaked, that fancy bcrypt did not help much...

Or, alternatively, if you are not afraid to create a bit of a mess:

  • Drop the old hashes. In the new application, add on option "Create temporary password for old application". It gives you a long, random password that is hashed in the old way and only kept in the database for X minutes. The user can then logg in to the old application, and the password is then automatically deleted.

Is there a secure way to add salt (and pepper) to our authentication database while maintaining the old application's ability to authenticate users?

Yes, this can be done. Below are high-level implementation instructions. The basic technique is to hash all the passwords, then MITM the connection between the client and the legacy server in order to replace the unhashed password with a hashed password. Note that you'll need to come up with a roll-out plan; blindly running step 1 in production will break everything.

Step 1: Salt all existing passwords, then store the salt somewhere. Overwrite the password field of the legacy database with the salted password.

Step 2: Create a shim. The shim will accept identical parameters as the legacy APIs.
So, if the Legacy API is implemented as:

LegacyDoStuff(username,password,argument)
{
    if(!VerifyCredentials(username,password)) return AuthenticationError();
    result = DoStuff(argument);
    return result;
}

The new API is implemented as:

NewDoStuff(username,password,argument)
{
    hashedpassword = DoHash(password+getSalt(username))
    return LegacyDoStuff(username,hashedpassword,argument);
}

Step 3: Point the legacy client at the shim, instead of the main server (or equivalently, move the legacy server to new IP/DNS, then put the shim at the old IP/DNS).

This approach does allow you to treat the internals of the legacy code as a black box, but it requires you to be aware of the public surface area of the legacy code, since your shim will need to send requests/responses between the client and the legacy server.

This approach, unlike the approaches described in other answers, completely avoids storing the old password. However, this approach is much more difficult to do and is far more likely to introduce bugs.


Is there a secure way to add salt (and pepper) to our authentication database while maintaining the old application's ability to authenticate users?

No. The reason for salting and hashing passwords is so that if the user database is hacked/leaked/compromised, the users' passwords are not accessible to the attacker (see What is the point of hashing passwords?). In the solution that you describe, the users' passwords are still stored in the user database, in an adjacent column, in plaintext. This completely defeats the purpose of salting and hashing passwords.