1

Since google killed the NPAPI plugin and we cannot load applets in Chrome our client certificate based web app authentication scheme is not working anymore.

While looking for alternatives I found out that, because we are using SSL on our private content, there is a way to automatically ask for an SSL client certificate, by enabling SSL client authentication on the web server (In this case Tomcat).

My question is: Once I've enabled Tomcat's SSL client authentication to request this certificate from the client browser's personal certificate store:

How can I obtain the certificate info on my JSF web application so I can register a user with that info on first login and associate it with a user id?

(I am assuming that I don't need to worry about fake or expired certificates since a properly configured Tomcat will reject them, so I don't need to bother with the authentication / certificate validation, but just retrieve the info: Tomcat handles rejections / authentication errors by redirecting to the source page with an error code)

NotGaeL
  • 8,344
  • 5
  • 40
  • 70
  • You are right! Thank you, that's what I actually needed :-) Nevertheless malaguna provided a very good answer to my (originally broader) question that solves the problem all through and it will be useful for a larger group of programmers interested on the issue so I am going to accept it and maybe we can leave both questions posted :-) – NotGaeL Sep 18 '15 at 17:48
  • Bad choice to accept an answer which contains a very specific framework as a solution. I can post similar answers for different frameworks (shiro, picketlink and more) that are as good. There isn't even a direct relation to your basic question as you stated in the comment. I'm going to mark this question as a duplicate of the other one anyways. Did you know the 'duplicate' is even found in the Tomcat Docs? – Kukeltje Sep 18 '15 at 18:02
  • @kukeltje, my answer is related with tomcat and jsf. I add extra spring security because it is fully compatible wuth jsf, but i offered elcodedocle an aswer only related with tomcat and jsf. – malaguna Sep 18 '15 at 19:00

1 Answers1

4

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!

malaguna
  • 4,183
  • 1
  • 17
  • 33
  • 1
    This is a perfectly valid and very thorough answer to the question in the title. The only problem is I cannot switch my authentication scheme to spring-security. I need to be able to extract the x509 client certificate using JSF and nothing else; I can pick it up from there. Can you show me how to do that? – NotGaeL Sep 18 '15 at 17:02
  • 1
    @elcodedocle, i already switched apps from jsf auth to spring security auth and it is easy and worthy. You have ti consider that the other answer don't manage cert validación. So i consider this is not duplicated. Don't forget to consume Policía Nacional OCSP server. – malaguna Sep 18 '15 at 19:04
  • Have you managed to extract the certificate from a DNIe using an USB DNIe card reader? When I enable the SSL cert auth in tomcat my browser only shows a list of certificates in the browser certificate store, but ignores the DNIe inside the reader. – NotGaeL Sep 22 '15 at 07:34
  • Yes I did, both systems Windows-GNU/Linux and tested in Firefox (both), IExplorer >8 and Chrome in Windows, although I think it will work as well on GNU/Linux. Nothing special to be done, but having correctly installed usb reader driver and browser plugin. – malaguna Sep 22 '15 at 07:44
  • which browser plugin? – NotGaeL Sep 22 '15 at 10:46
  • Well, it is not a real plugin sorry, it is the PKCS module that browser needs to read DNIe certificate from USB cardreader. In Firefox you must setup a new security device that loads `libpkcs11-dnie.so` module. I use a C3P0 LTC31usb reader. – malaguna Sep 22 '15 at 10:51
  • Great work! One question, @malaguna: With this configuration, ¿the client cert is only requested in the private pages configured in Spring or the first time you connect to the web page? ¿Does Tomcat need special configuration besides setting the HTTPS Connector? – Juan Calero Jul 15 '16 at 10:12
  • 1
    Hi @JuanCalero, thanks! The cert is only requested on first access to private pages. You define what pages are private by means of Spring Security. The only thing you have to configure in Tomcat (or another container) is certificate keystores and also HTTPS. – malaguna Jul 18 '16 at 20:07