I am going to describe a procedure to validate against spanish DNIe which uses client certificate using JSF 2.2 and Spring Security 4.0, although it is possible to authenticate without using Spring Security.
You said you have enabled Tomcat's SSL client authentication, so I guess it implies you have already configured keyStore with ROOT certificate. If you don't I can provide you with valid instructions for Tomcat 7.
So, once Tomcat is properly configured to require client certificate, and once the handshake has finished, this is what you have to do in your application to read client certificate:
Configure dependencies in pom.xml
Add following dependencies to pom.xml
:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>4.0.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>4.0.1.RELEASE</version>
</dependency>
It is worth to say that Spring Security 4.0.1 is bound to Spring 4.1.X
Configure web.xml
to delegate in Spring Security
Tell servlet container, security is delegated into Spring Security
<security-constraint>
<web-resource-collection>
<web-resource-name>Secured</web-resource-name>
<url-pattern>/*</url-pattern>
</web-resource-collection>
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
It must ask for a client certificate:
<login-config>
<auth-method>CLIENT-CERT</auth-method>
<realm-name>certificate</realm-name>
</login-config>
Configure Spring Security filter
<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>*.jsf</url-pattern>
</filter-mapping>
Don't forget to add spring security file descriptor into contextConfigLocation
context param.
Configure Spring Security
Following is a large file which configure Spring Security to validate against client-certificate.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:sec="http://www.springframework.org/schema/security"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">
<bean id="userDetailsService" class="your.own.UserDetailService">
<property name="dao" ref="userDao" />
</bean>
<bean id="dniPrincipalExtractor" class="your.own.DniePrincipalExtractor">
</bean>
<bean id="x509Filter" class="org.springframework.security.web.authentication.preauth.x509.X509AuthenticationFilter">
<property name="authenticationManager" ref="authManager" />
<property name="principalExtractor" ref="dniPrincipalExtractor" />
</bean>
<bean id="preauthAuthenticationProvider"
class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider">
<property name="preAuthenticatedUserDetailsService" ref="authenticationUserDetailsService" />
</bean>
<bean id="authenticationUserDetailsService"
class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
<property name="userDetailsService" ref="userDetailsService" />
</bean>
<sec:http pattern="/css/**" security="none" />
<sec:http pattern="/error/**" security="none" />
<sec:http pattern="/icons/**" security="none" />
<sec:http pattern="/imgs/**" security="none" />
<sec:http
auto-config="true"
use-expressions="true"
entry-point-ref="forbiddenAuthEP">
<sec:intercept-url pattern="/*" access="permitAll" />
<sec:intercept-url pattern="/xhtml/**" access="hasRole('ROLE_BASIC')" />
<sec:custom-filter ref="x509Filter" position="X509_FILTER"/>
</sec:http>
<sec:authentication-manager alias="authManager">
<sec:authentication-provider ref="preauthAuthenticationProvider" />
</sec:authentication-manager>
</beans>
This file creates a security context that requires login by means of a x509Filter
. This filter needs a
dniPrincipalExtractor
which a class you need to find and extract dni from user certificate.
userDetailsService
which knows how to find the user into data base, using userDao
.
With this configuration, once a client certificate is received, a pre-authentication service acts to extract DNI and load user from data base (or whatever) building the
This code implies you have to build three classes of your own:
your.own.UserDao
your.own.UserDetailService
it has to implements org.springframework.security.core.userdetails.UserDetailsService
. Here you have to retrieve roles or groups assigned to user to build List<GrantedAuthority>
for the user and create a valid org.springframework.security.core.userdetails.User
.
your.own.DniePrincipalExtractor
it has to implements org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor
Sample UserDetailService
package your.own.package;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import your.own.UserDAO;
public class UserDetailService implements UserDetailsService {
private UserDAO dao = null;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String error = null;
UserDetails result = null;
if(StringUtils.isNotEmpty(username)){
if(dao.findById(username) != null){
List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("ROLE_BASIC", "ROLE_ADMIN");
result = new org.springframework.security.core.userdetails.User(username, "", authorities);
}
}else{
error = "No se ha especificado login para el usuario.";
}
if(result == null){
if(StringUtils.isEmpty(error)){
error = String.format("No se encuentra ningún usuario con login %s", username);
}
throw new UsernameNotFoundException(error);
}
return result;
}
}
For simplicity I put roles by hand, obviously you will have to change this.
Get authenticated user
Now you can get authenticated user this way, in session bean or whatever
SecurityContextHolder.getContext().getAuthentication().getName()
If you need, I can provide you with more information, but I didn't want to write a very large answer. Things that have been not covered in this answer:
- DNI principal extractor: If you search google you will find several implementations, but I can provide you one.
- OCSP validation: You have to manage cert validation against Policia Nacional OCSP server. I can tell you how to do.
- JSF Tag Libs to ask for user grants.
- Tomcat full configuration.
- Implementing a fallback mechanism to launch a form validation page if there is no client certificate. It is possible with Spring Security
My first implementation didn't use Spring Security, but as far I was concern with the need to provide a fallback mechanism, I move forward Spring Security, although I didn't show you how to do here, for simplicity.
Hope it helps!