Instead of the classic login/password way to access to a secured application, it’s possible to authenticate through a certificate.
What’s more, you can also link a ROLE to each certificate.
⚠️ This tutorial is now obsolete and better solution can be used.
how to activate the SSL secured by a certificate for our Tomcat
how to secure the URL pattern with Spring Security
how to access with a browser
how to access with a third party application
In order to fully comprehend the tutorial, I will demonstrate it with a concrete example.
The specifications of the application
You have to build an internal application (let’s call it foobar) that needs to be a bit secured.
This application will mainly be used as a web service application where other web applications will be able to plug in foobar. Let’s be crazy and say that Paypal and Amazon are the web applications that will communicate with our great app.
Simple right?
Now, you want to enable access to foobar only to Paypal and Amazon.
There are several solutions to do that (same network, OAuth, Basic Auth, and so on…), but you came for certificate authenticate, arent’t you?
The basics
The principle is quite simple. It’s a mutual verification:
The client checks if the certificate given by the server is valid or not (through a certification authority that signs certificates).
The server also checks the certificate given by the client.
The keystores stores the private keys aimed to encrypt the data before emitting
The trustores stores the public keys aimed to identify the transmitter and then to decrypt their message
SSL activation
Generating the certificate
First and foremost, we will generate our own Certification Authority:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
######################################################## Certification Authority ######################################################### Since we do not have any certification authority, we will generate our own.
openssl genrsa -out foobarCA.key 1024
openssl req -new -x509 -days 3650 -key foobarCA.key -out foobarCA.crt -subj "/C=FR/ST=IDF/L=Paris/O=FoobarCA/OU=/CN=*.local.fr"
mkdir -p demoCA/newcerts
touch demoCA/index.txt
echo'01' > demoCA/serial
# Add the root certificate to cacerts of your JVM
keytool -delete -noprompt -trustcacerts -alias foobarCA -keystore ${JAVA_HOME}/jre/lib/security/cacerts -storepass changeit
keytool -import -noprompt -trustcacerts -alias foobarCA -file foobarCA.crt -keystore ${JAVA_HOME}/jre/lib/security/cacerts -storepass changeit
# Create the trustore with the root certificate in it
keytool -import -keystore cacerts.jks -storepass cacertspassword -alias foobarCA -file foobarCA.crt -noprompt
Let’s generate the keystore that will be used by Tomcat by executing the following commands:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
######################################################## Foobar certificate ######################################################### Generate the keystore
keytool -genkey -v -alias foobar -keyalg RSA -validity 3650 -keystore foobar.jks -dname "CN=foobar.local.fr, OU=, O=Foobar, L=Paris, ST=IDF, C=FR" -storepass foobarpwd -keypass foobarpwd
# Then, generate the CSR to sign:
keytool -certreq -alias foobar -file foobar.csr -keystore foobar.jks -storepass foobarpwd
# Sign the certificate to the CA:
openssl ca -batch -keyfile foobarCA.key -cert foobarCA.crt -policy policy_anything -out foobar.crt -infiles foobar.csr
# Add the root certificate to the keystores
keytool -importcert -alias foobarCA -file foobarCA.crt -keystore foobar.jks -storepass foobarpwd -noprompt
# Add signed certificate to the keystores
keytool -importcert -alias foobar -file demoCA/newcerts/01.pem -keystore foobar.jks -storepass foobarpwd -noprompt
Configuring with Tomcat
Edit the server.xml of your Tomcat and add the following connector (change the path to your jks file):
Note:Brian Bonner points out that in this configuration, a 403 error may show up.
In that cas, you will need to change the clientAuth="false" to clientAuth="want".
Securing the application
Ok, now we finished configuring our Tomcat. Let’s start implementing the security in our application.
So first, add dependency to Spring security with Maven:
<?xml version="1.0" encoding="UTF-8"?><web-appxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns="http://java.sun.com/xml/ns/javaee"xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"version="3.0"><display-name>Foobar Application</display-name><context-param><param-name>contextConfigLocation</param-name><param-value>
WEB-INF/foobar-web-app-context.xml
</param-value></context-param><!-- Filter to ensure spring gets to handle requests and enforce security --><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.web.context.ContextLoaderListener</listener-class></listener><servlet><servlet-name>foobarSpringDispatchServlet</servlet-name><servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class><init-param><param-name>contextConfigLocation</param-name><param-value>WEB-INF/epayment-web-app-context.xml</param-value></init-param><load-on-startup>1</load-on-startup></servlet><servlet-mapping><servlet-name>foobarSpringDispatchServlet</servlet-name><url-pattern>/api/*</url-pattern></servlet-mapping><welcome-file-list><welcome-file>index.html</welcome-file><welcome-file>index.htm</welcome-file><welcome-file>index.jsp</welcome-file><welcome-file>default.html</welcome-file><welcome-file>default.htm</welcome-file><welcome-file>default.jsp</welcome-file></welcome-file-list><security-constraint><web-resource-collection><web-resource-name>All users</web-resource-name><url-pattern>/favicon.ico</url-pattern></web-resource-collection></security-constraint><security-constraint><web-resource-collection><web-resource-name>AuthUser</web-resource-name><url-pattern>/*</url-pattern></web-resource-collection><auth-constraint><role-name>AUTH_USER</role-name></auth-constraint><user-data-constraint><transport-guarantee>CONFIDENTIAL</transport-guarantee></user-data-constraint></security-constraint><login-config><auth-method>CLIENT-CERT</auth-method></login-config><security-role><role-name>AUTH_USER</role-name></security-role></web-app>
Edit the foobar-web-app-context.xml with the following:
@ComponentpublicclassX509CustomAuthenticationProviderextendsAbstractUserDetailsAuthenticationProvider{@InjectCertificateUserServicecertificateUserService;@OverrideprotectedvoidadditionalAuthenticationChecks(UserDetailsuserDetails,UsernamePasswordAuthenticationTokenauthentication)throwsAuthenticationException{//Do nothing
}@OverrideprotectedX509CustomUserretrieveUser(Stringusername,UsernamePasswordAuthenticationTokenauthentication)throwsAuthenticationException{List<GrantedAuthority>grantedAuths=newArrayList<>();X509Certificatecertificate=(X509Certificate)authentication.getPrincipal();// Use your service to fetch the certificate user from your DB, or LDAP or anywhere you like
CertificateUsercertificateUser=certificateUserService.findByCertificateId(certificate.getSubjectDN().getName());// Convert in a DTO that can be exploited
X509CustomUseruser=newX509CustomUser(certificateUser.getCertificateId(),"",grantedAuths);BeanUtils.copyProperties(certificateUser,user);returnuser;}@Overridepublicbooleansupports(Class<?>authentication){return(X509AuthenticationToken.class.isAssignableFrom(authentication));}}
publicclassX509CustomFilterextendsGenericFilterBean{publicstaticfinalStringX509="javax.servlet.request.X509Certificate";privateAuthenticationManagerauthenticationManager;@OverridepublicvoiddoFilter(ServletRequestrequest,ServletResponseresponse,FilterChainchain)throwsIOException,ServletException{if(!(requestinstanceofHttpServletRequest&&responseinstanceofHttpServletResponse)){chain.doFilter(request,response);return;}if(request.getAttribute(X509)==null){chain.doFilter(request,response);return;}X509Certificate[]certificates=(X509Certificate[])request.getAttribute(X509);if(certificates.length>0){//Using the first certificate, we don't know how to identify several at once
doAuthenticate((HttpServletRequest)request,(HttpServletResponse)response,certificates[0]);}chain.doFilter(request,response);}privatevoiddoAuthenticate(HttpServletRequestrequest,HttpServletResponseresponse,X509Certificatecertificate){AuthenticationauthResult;if(certificate==null){if(logger.isDebugEnabled()){logger.debug("No certificate found in request");}return;}if(logger.isDebugEnabled()){logger.debug("preAuthenticatedPrincipal = "+certificate+", trying to authenticate");}try{X509AuthenticationTokenauthRequest=newX509AuthenticationToken(certificate,getPreAuthenticatedCredentials(request));authResult=authenticationManager.authenticate(authRequest);successfulAuthentication(request,response,authResult);}catch(AuthenticationExceptionfailed){unsuccessfulAuthentication(request,response,failed);throwfailed;}}/**
* Sets authentication manager.
*
* @param authenticationManager the authentication manager
*/publicvoidsetAuthenticationManager(AuthenticationManagerauthenticationManager){this.authenticationManager=authenticationManager;}/**
* Gets pre authenticated credentials.
*
* @param request the request
* @return the pre authenticated credentials
* @see org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter#getPreAuthenticatedPrincipal(javax.servlet.http.HttpServletRequest)
*/protectedObjectgetPreAuthenticatedCredentials(HttpServletRequestrequest){return"N/A";}/**
* Unsuccessful authentication.
*
* @param request the request
* @param response the response
* @param failed the failed
*/protectedvoidunsuccessfulAuthentication(HttpServletRequestrequest,HttpServletResponseresponse,AuthenticationExceptionfailed){SecurityContextHolder.clearContext();if(logger.isDebugEnabled()){logger.debug("Cleared security context due to exception",failed);}request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION,failed);}/**
* Puts the <code>Authentication</code> instance returned by the authentication manager into the secure context.
* @param request the request
* @param response the response
* @param authResult the auth result
*/protectedvoidsuccessfulAuthentication(HttpServletRequestrequest,HttpServletResponseresponse,AuthenticationauthResult){if(logger.isDebugEnabled()){logger.debug("Authentication success: "+authResult);}SecurityContextHolder.getContext().setAuthentication(authResult);}}
Annnnnnd, we’re done with the server.
Accessing with a browser
Generating the certificate and allow access
Let’s generate the keystore that will be used to authenticate to the application:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
######################################################## Certificate used in the browser ######################################################### Generate the keystore
keytool -genkey -v -alias browser -keyalg RSA -validity 3650 -keystore browser.jks -dname "CN=browser.local.fr, OU=, O=browser, L=Paris, ST=IDF, C=FR" -storepass browserpwd -keypass browserpwd
# Then, generate the CSR to sign:
keytool -certreq -alias browser -file browser.csr -keystore browser.jks -storepass browserpwd
# Sign the certificate to the CA:
openssl ca -batch -keyfile foobarCA.key -cert foobarCA.crt -policy policy_anything -out browser.crt -infiles browser.csr
# Add the root certificate tot the keystores
keytool -importcert -alias foobarCA -file foobarCA.crt -keystore browser.jks -storepass browserpwd -noprompt
# Add signed certificate to the keystores
keytool -importcert -alias browser -file demoCA/newcerts/02.pem -keystore browser.jks -storepass browserpwd -noprompt
# Export certificates in PKCS12 format for test use (in browser)
keytool -importkeystore -srckeystore browser.jks -destkeystore browser.p12 -srcstoretype JKS -deststoretype PKCS12 -srcstorepass browserpwd -deststorepass browserpwd -srcalias browser -destalias browserKey -srckeypass browserpwd -destkeypass browserpwd -noprompt
Import certificate
With Chrome:
Go to Settings > HTTPS/SSL > Manage certificates
Click on import and select the browser.p12 file (password is browserpwd)
You are now granted to use your app with your browser.
Accessing with an another web application
If you need to access with another web application (using an HTTP client), you need to add the following parameters in your VM options of your web app (not the secured one, but the one that will make the call):