Buscar este blog

sábado, 27 de febrero de 2016

Spring Boot - Spring Security - Certificate authentication behind Apache Web Server

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:
  1. Configure the Spring Boot app to use the user certificate
  2. Configure the embedded tomcat container of the spring boot app to expose an AJP port
  3. 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

No hay comentarios:

Publicar un comentario