Reputation: 133
I'm new to Spring (and to web development in general), but I've managed to create a simple web application and now I'm trying to add authentication support to it so that users are required to sign in to access its features.
I'm using Spring MVC 3.2.2, Spring Security 3.1.4, and Tomcat 7.
For testing purposes, I was able to add authentication support using hardcoded users and everything worked correctly. Now, I must use an existing PostgreSQL database to authenticate users. I would like to emphasize that my application does not support the creation of users; users are already stored in the database.
The following is the problem:
(1) The application must authenticate users against a PostgreSQL database, which I cannot modify in any way.
(2) Passwords in the database are hashed using crypt('plain text password', gen_salt(md5)).
(3) Since I've made heavy use of annotations and xml configuration, basically most of the hard work is done by Spring, which means that a lot of things are going on behind the scenes, which I'm not aware of. As a result, I'm now stuck setting up the salt the password encoder must use to authenticate the users. Such a salt has to be the already hashed password stored in the database.
The question is:
How do I tell Spring Security to use the hashed password stored in the database as the salt of the password encoder? Is there any way to do this from xml or do I need to go ahead and implement certain class(es)?
I've searched extensively on-line and all of the examples I've found so far deviate considerably from what I have done, mainly because in the examples most of the functionally is implemented by the developer instead of mostly relying on built-in features.
This is the security.xml file:
<http use-expressions="true">
<intercept-url pattern="/resources/images/**" access="permitAll" requires-channel="any"/>
<intercept-url pattern="/resources/css/**" access="permitAll" requires-channel="any"/>
<intercept-url pattern="/login*" access="permitAll" requires-channel="any"/>
<intercept-url pattern="/**" access="hasAnyRole('ROLE_Processer', 'ROLE_Verifier', 'ROLE_Approver', 'ROLE_Supervisor', 'ROLE_Admin')" requires-channel="any"/>
<session-management>
<concurrency-control max-sessions="1" expired-url=/login?expired=true" session-registry-alias="sessionRegistry"/>
</session-management>
<form-login
login-page="/login"
default-target-url="/"
always-use-default-target="true"
authentication-failure-url="/login?error=true"/>
<logout logout-success-url="/login" invalidate-session="true" delete-cookies="JSESSIONID"/>
</http>
<authentication-manager>
<authentication-provider>
<jdbc-user-service
data-source-ref="dataSource"
users-by-username-query="SELECT id AS username, password, enabled FROM users WHERE id=?;"
authorities-by-username-query="SELECT id AS username, role FROM users WHERE id=?;"
role-prefix="ROLE_"/>
<password-encoder hash="md5">
<salt-source ???/>
</password-encoder>
</authentication-provider>
</authentication-manager>
This is the relevant excerpt from the web.xml file:
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<listener>
<listener-class>
org.springframework.security.web.session.HttpSessionEventPublisher
</listener-class>
</listener>
This is the relevant excerpt from the application-context.xml file:
<beans:bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
<beans:property name="driverClass" value="org.postgresql.Driver"/>
<beans:property name="url" value="jdbc:postgresql://host:port/database"/>
<beans:property name="username" value="username"/>
<beans:property name="password" value="password"/>
</beans:bean>
In terms of implementation, this is the only relevant controller LoginController.java (all others handle the actual functionality provided by the app):
@Controller
public class LoginController {
private static final Logger LOGGER = LoggerFactory.getLogger(LoginController.class);
/**
* Displays the Login form.
*/
@RequestMapping(value="/login", method=RequestMethod.GET)
public String displayLoginForm(@RequestParam(value="error", required=false) String error, @RequestParam(value="expired", required=false) String expired, Model model) {
LOGGER.info("Displaying Login form:: displayLoginForm(Model)");
if (error != null) {
LOGGER.info("Invalid username or password.");
model.addAttribute("error", "Invalid username or password.");
}
if (expired != null) {
LOGGER.info("Session has been expired (possibly due to multiple concurrent logins being attempted as the same user)");
model.addAttribute("expired", "Session has been expired (possibly due to multiple concurrent logins being attempted as the same user).");
}
return "login";
}}
Any pointers will be greatly appreciated. I would like to thank you all in advance for your time and help.
Edit:
I tried with
<password-encoder hash="md5">
<salt-source user-property="password"/>
</password-encoder>
since, from what I understood by reading the documentation, this is the user's password stored in the database. However, it doesn't work. Every time I try to log in, I get an invalid credentials message; however, the credentials are indeed valid.
Perhaps any of the events described in the Hashing and Authentication section of this page might be the reason why it is not working. I'm wondering how to write the appropriate test as suggested.
Another Edit:
As a test, I commented out the password-encoder element and tried the authentication with a plain text password (i.e., I created a test user and stored a plain text password in the database). It worked. So, the problem is definitely with the way Spring Security is encoding the password entered by the user, which does not match the hashed password stored in the database.
Upvotes: 4
Views: 4165
Reputation: 133
Well, to solve my problem I implemented a custom password encoder. Basically, I overrode the matches method of the PasswordEncoder interface:
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
//encoding raw password: the given encodedPassword serves as the salt
String sql = "SELECT crypt(?, ?) as hashedpwd;";
String hashedPassword = aJdbcTemplate.query(sql, new String[] {rawPassword.toString(), encodedPassword}, new ResultSetExtractor<String>() {
@Override
public String extractData(ResultSet rs) throws SQLException, DataAccessException {
rs.next();
return rs.getString("hashedpwd");
}
});
return encodedPassword.equals(hashedPassword.toString());
}
Upvotes: 3