As a replacement for the originally provided DatabaseServerLoginModule
the
SaltedPasswordLoginModule
supports the same basic options, that is
dsJndiName
, principalsQuery
, rolesQuery
and
suspendResume
. The (required) dsJndiName
option identifies the datasource containing the tables
for users and roles. The principalsQuery
and rolesQuery
options refer to
the queries used to match the password and to retrieve the roles associated with the user principal.
The suspendResume
option indicates if any JTA transactions should be suspended during the
necessary DB operations executed by the SaltedPasswordLoginModule
. The
SaltedPasswordLoginModule
will assume
Base64 encoding for the hashed password and the salt.
The class SaltedPasswordLoginModule
overrides the following methods from its superclass
UsernamePasswordLoginModule
:
(i) | void initialize(Subject subject, CallbackHandler callbackHandler, Map<String,?> sharedState,
Map<String,?> options) |
(ii) | String getUsersPassword() |
(iii) | boolean validatePassword(String inputPassword, String expectedPassword) |
(iv) | Group[] getRoleSets() |
The method (i) is declared at the top of the login module class hierarchy, this is indeed the javax.security.auth.spi.LoginModule
interface from the JDK.
The JBoss interpretation (referring to picketbox 4.0.21.Final) of this method dictates that in the first place we have to inform the container about the valid options for this module before
calling the super class implementation. We are doing this by
invoking the method addValidOptions(final String[] validOptions)
defined within
AbstractServerLoginModule
. The container uses the fourth argument to pass in the configured options.
Apart from evaluating the given options we have no further business with this method.
Method (ii) is responsible for the retrieval of the expected (hashed) password from the database together with the randomly chosen salt.
The result type of the method is fixed - otherwise it wouldn't override its superclass implementation. That is we must the store the
value of the salt - as retrieved from the database - in an instance member for later use. The return value of method (ii) is fed into
the second parameter (expectedPassword
) of method (iii) by the container. The default SQL statement which is
used if no principalsQuery
is present is shown below. This means that an account is identified by its name and can be disabled.
A custom principalsQuery
should use aliases to indicate the columns containing password and salt if required.
1 |
SELECT password, salt |
2 |
FROM prinicipals |
3 |
WHERE name = ? AND disabled = 'N' |
We must consider the case that we didn't find any match in the database. An immediate login failure would lead to a shorter evaluation time than needed when evaluating a valid account. This would indicate an invalid or disabled account to an attacker. In order to eliminate timing attacks we note in this case that the account doesn't exist or is disabled in an instance member and return some default values for the salt and the expected hash.
As suggested by the name, method (iii) performs the validation of an inputPassword
against an expectation.
The value of inputPassword
must undergo the same transformations that originally led to the value of
expectedPassword
. These transformations are shown below:
1 |
byte[] bytes = inputPassword.getBytes("UTF-8"); |
2 |
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); |
3 |
messageDigest.update(bytes); |
4 |
Base64 codec = new Base64(); |
5 |
byte[] saltBytes = codec.decode(this.salt); |
6 |
byte[] checkSum = messageDigest.digest(saltBytes); |
7 |
String saltedInputPassword = codec.encodeToString(checkSum); |
This means that we first applying the UTF-8 encoding to get the raw bytes of the inputPassword
. Secondly we feed
this raw bytes into the SHA-256 hash algorithm. Next we must transform the Base64
encoded salt previously retrieved from the database into raw bytes. Now we concatenate these raw bytes with the current state of the
SHA-256 hash algorithm and obtain the final hash value raw bytes. We transform these raw bytes into a Base64 encoded string again and we are
ready to perform our validation against expectedPassword
.
Following a Defense in Depth-policy
we take care about potential timing attacks.
Therefore the final validation will be done in constant time, that is all characters will be compared regardless of early matching failures. Otherwise
an attacker could conclude from the execution time how many bytes at the beginning of the checksum do match.
Method (iv) retrieves the roles associated with an authenticated user. We are only interested in one Group named "Roles". That is method (iv) will
always return an array with one such Group element. This Group element contains principals which correspond to the roles owned by the authenticated user.
The default SQL statement which is used if no rolesQuery
option is defined is shown below:
1 |
SELECT a.name AS groupname |
2 |
FROM roles a, principalrole b, principals c |
3 |
WHERE a.name = b.groupname AND b.username = c.name AND c.name = ? |
That is the default statement assumes a many-to-many relationship between user principals and roles. A custom rolesQuery
should always define an alias 'groupname' for the column containing the roles within the query if required.