In this post I describe how to configure a Spring Boot application, using Spring Security, to authenticate the user with his personal certificate. The certificate will be requested by Apache Web server and sent to the app via AJP.
This project is based (a.k.a copied) from the Spring getting started guides:
https://spring.io/guides/gs/securing-web/. In this post I´ll only talk about the classes I changed.
In order to complete the modification, we need to make several things:
- Configure the Spring Boot app to use the user certificate
- Configure the embedded tomcat container of the spring boot app to expose an AJP port
- Configure apache to serve as the front-end
Configure the Spring Boot app to use the user certificate
This is very easy. You only need to add the x509 config to the HttpSecurity element.
In this example the authentication is doing with an in-memory repository, so you have to add the user certificate as a valid application user.
WebSecurityConfig.jave file:
package hello;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
http.x509().subjectPrincipalRegex("CN=(.*?),");
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("user").password("password").roles("USER")
.and()
.withUser("sisifo").password("not_used").roles("USER");
}
}
The x509 filter will search the user certificate in the request (in the
'javax.servlet.request.X509Certificate' attribute). When it exists, the filter will extract the "user name" and try to authenticate it against the userDetailsService, in this case the in-memory repository.
But the user will be able to loggin by using an username/password, so in the loggin HTML page you need to put a link to force him to send his certificate.
Login.html page:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example </title>
</head>
<body>
<script type="text/javascript">
var copyFields = function() {
document.getElementById('username').value = document.getElementById('usernameFake').value;
document.getElementById('password').value = document.getElementById('passwordFake').value;
}
var submitRealForm = function() {
copyFields();
document.getElementById('realLoginForm').submit();
return false;
}
var submitFormOnEnter = function(event) {
if (event.keyCode == 13) {
submitRealForm();
}
}
</script>
<div th:if="${param.error}">
Invalid username and password.
</div>
<div th:if="${param.logout}">
You have been logged out.
</div>
<form id="realLoginForm" th:action="@{/login}" method="post" autocomplete="off" style="display:none">
<input id="username" type="hidden" name="username" readonly="readonly"/>
<input id="password" type="hidden" name="password" readonly="readonly"/>
</form>
<div class="simulateForm">
<div><label> User Name : <input id="usernameFake" type="text" onkeydown="submitFormOnEnter(event)"/> </label></div>
<div><label> Password: <input id="passwordFake" type="password" onkeydown="submitFormOnEnter(event)"/> </label></div>
<div> <button type="button" onclick="submitRealForm()">Login user/pass</button></div>
<div> <a th:href="@{/logincert}">Login certificate</a></div>
</div>
</body>
</html>
When the user clicks in the Login certificate link he will be sent to https://serverhost/logincert. Apache will take care that in this URL the user will be asked for a valid certificate, and then Apache will sent this certificate to the app.
Configure the embedded tomcat container of the Spring Boot app to expose an AJP port
Apache will talk with the Spring Boot app by using the AJP protocol. The app is configured to use an embedded tomcat server, so you have to configure it.
This part is copied from this post
http://www.appsdev.is.ed.ac.uk/blog/?p=525.
Basically, you create your own Connector based on three config properties located in application.properties file.
Application.java file:
package hello;
import org.apache.catalina.connector.Connector;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class Application {
@Value("${tomcat.ajp.port}")
private int ajpPort;
@Value("${tomcat.ajp.remoteauthentication}")
private String remoteAuthentication;
@Value("${tomcat.ajp.enabled}")
private boolean tomcatAjpEnabled;
@Bean
public EmbeddedServletContainerFactory servletContainer() {
TomcatEmbeddedServletContainerFactory tomcat = new TomcatEmbeddedServletContainerFactory();
if (tomcatAjpEnabled) {
Connector ajpConnector = new Connector("AJP/1.3");
ajpConnector.setProtocol("AJP/1.3");
ajpConnector.setPort(ajpPort);
ajpConnector.setSecure(false);
ajpConnector.setAllowTrace(false);
ajpConnector.setScheme("http");
tomcat.addAdditionalTomcatConnectors(ajpConnector);
}
return tomcat;
}
public static void main(String[] args) throws Throwable {
SpringApplication.run(Application.class, args);
}
}
And this is the properties file. Note that the server port was changed to 9080 instead 8080.
server.port = 9080
tomcat.ajp.port=9090
tomcat.ajp.remoteauthentication=false
tomcat.ajp.enabled=true
Configure apache to serve as the front-end
The final step is to configure the web server. I created two virtual host:
- The http virtual host will only redirect to the https virtual host.
- The https virtual host will configure the SSL and serve as proxy to the Spring Boot app.
In the https virtual host you need two location, one of them will ask for the user certificate. The URL location match the link placed in the login page.
<VirtualHost springBoot.local:80>
ServerName springBoot.local:80
serverAlias springBoot.local.80
Redirect permanent / https://springBoot.local/
</VirtualHost>
<VirtualHost springBoot.local:443>
DocumentRoot "C:/Program Files (x86)/Apache Software Foundation/Apache2.2/htdocs"
ServerName springBoot.local:443
serverAlias springBoot.local.433
ErrorLog "C:/Program Files (x86)/Apache Software Foundation/Apache2.2/logs/error_SpringBoot.log"
TransferLog "C:/Program Files (x86)/Apache Software Foundation/Apache2.2/logs/access_StpringBoot.log"
SSLEngine on
SSLCipherSuite ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP:+eNULL
SSLCertificateFile conf/keystores/desarr.local.cer
SSLCertificateKeyFile "conf/keystores/desarr.local.key
SSLCACertificateFile conf/keystores/desarr.local-cas.pem
<FilesMatch "\.(cgi|shtml|phtml|php)$">
SSLOptions +StdEnvVars
</FilesMatch>
<Directory "C:/Program Files (x86)/Apache Software Foundation/Apache2.2/cgi-bin">
SSLOptions +StdEnvVars
</Directory>
BrowserMatch ".*MSIE.*" \
nokeepalive ssl-unclean-shutdown \
downgrade-1.0 force-response-1.0
CustomLog "C:/Program Files (x86)/Apache Software Foundation/Apache2.2/logs/ssl_request_springBoot.log" "%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b"
ProxyRequests Off
ProxyPreserveHost On
SSLProxyEngine on
<Location /logincert>
SSLRequireSSL
SSLVerifyClient require
SSLVerifyDepth 2
SSLOptions +StdEnvVars +ExportCertData +StrictRequire +OptRenegotiate
ProxyPass ajp://localhost:9090/
ProxyPassReverse ajp://localhost:9090/
</Location>
<Location />
SSLRequireSSL
ProxyPass http://localhost:9080/
ProxyPassReverse http://localhost:9080/
</Location>
</VirtualHost>
The source code is in my github:
https://github.com/evazquezma/JEE6/tree/master/gs-security-web