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

viernes, 26 de febrero de 2016

HTML Login Form - Disable autocomplete and password remember

Most users use the autocomplete a password remember options of their browsers. In this way, if you try to login in a site where you previously were logged, the browser autocompletes the login, and then suggests the password.
But recently, in one critical web app, we have a requirement to disable this behaviour. What seemed a simple task, resulted in a small head ache.

We start with this basic login page.
<body>
 <div th:if="${param.error}">
  Invalid username and password.
 </div>
 
 <div th:if="${param.logout}">
  You have been logged out.
 </div>
 
 <form th:action="@{/login}" method="post">
  <div><label> User Name : <input type="text" name="username"/> </label></div>
  <div><label> Password: <input type="password" name="password"/> </label></div>
  <div><input type="submit" value="Sign In"/></div>
 </form>
</body>

In this page, the browser will ask you if you want to remember the password. Any way, the next time you visit this page, the browser will autocomplete the login field.

Step 1, disable autocomplete

If you only need to disable autocomplete (but you can afford the browser ask for password remember) there is a HTML attribute which should work... autocomplete="off"
<body>
 <div th:if="${param.error}">
  Invalid username and password.
 </div>
 
 <div th:if="${param.logout}">
  You have been logged out.
 </div>
 
 <form th:action="@{/login}" method="post" autocomplete="off">
  <div><label> User Name : <input type="text" name="username"/> </label></div>
  <div><label> Password: <input type="password" name="password"/> </label></div>
  <div><input type="submit" value="Sign In"/></div>
 </form>
</body>

Step 2, disable password remember

If you want to force the browser to not ask for password remember, this is a bit more complex. After a lot of  test, the solution I found was to create a hidden form with hidden fields. The user fills a fake form, and when she presses the submit button, these fields and the submit event are transferred to the true login form.
<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;
  }
 </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"/>
  <input id="password" type="hidden" name="password"/>       
 </form>
  
 
 <div class="simulateForm">
  <div><label> User Name : <input id="usernameFake" type="text" /> </label></div>
  <div><label> Password: <input id="passwordFake" type="password"/> </label></div>
  <div> <button type="button" onclick="submitRealForm()">Click Me!</button></div>
 </div>
</body>


Step 3, caputre enter in password

When the user are inside an HTML form and she presses enter key, the browser send a click event to the first button in this form.
With the solution I made, when user press enter, nothing happens. To solve this problem, with javascript you can listen this event and trigger the submit manually.
<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"/>
  <input id="password" type="hidden" name="password"/>       
 </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()">Click Me!</button></div>
 </div>
</body>

Note.
This example is based on the Spring Security getting started (https://spring.io/guides/gs/securing-web/). Their source code is in https://github.com/spring-guides/gs-securing-web, I just played with de login.html page.

domingo, 21 de febrero de 2016

XMLHttpRequest - Download PDF and show it in iframe

These snippets show how to download a PDF from a Servlet and how to show it inside a iframe.
The immediate solution would be to invoke the servlet by GET, and to put this URL in de src attribute of the iframe, but in this case I want to invoke the servlet by POST.

Servlet code:
@WebServlet(urlPatterns="/sello")
public class SelloRegistroServlet extends HttpServlet {
 private static final long serialVersionUID = 1L;

 private final SelloService selloService = new SelloServiceImpl();

 @Override
 protected void doPost(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
     final DatosSello datos = recuperarDatosSello(request);
     final ConfiguracionSello config = recuperarDatosConfig(request);

     final ByteArrayOutputStream baos = selloService.generarSello(datos, config);
  
            response.setContentType("application/pdf");
            response.setContentLength(baos.size());
            response.setHeader("Content-Disposition", "inline;filename=\"selo.pdf\"");
            response.setHeader("filename", "selo.pdf");

            final OutputStream outStream = response.getOutputStream();
            outStream.write(baos.toByteArray());
            outStream.flush();
            outStream.close();
 }
 
 private DatosSello recuperarDatosSello(final HttpServletRequest request) {
     (...)
 }


 private ConfiguracionSello recuperarDatosConfig(final HttpServletRequest request) {
     (...)
 }
}

HTML code:
<html>
<head>          
    <script type="text/javascript">
        var urlSello = 'http://localhost:8080/registro-itext/sello';
        
        var generarSello = function() {          
            var data = {
                    numeroRegistro:             document.getElementsByName("numReg")[0].value,
                    fechaRegistro:              document.getElementsByName("fechaReg")[0].value,
                    codigoOficina:              document.getElementsByName("codigoOfi")[0].value,
                    nombreOficina:              document.getElementsByName("nombreOfi")[0].value,
                    codigoUnidad:               document.getElementsByName("codigoUnidad")[0].value,
                    nombreOficina:              document.getElementsByName("nombreOfi")[0].value
            };
            
            var params = Object.keys(data).map(function(key) {
                return key + '=' +  encodeURI(data[key]);
            }).join('&');
            
            var xhr = new XMLHttpRequest();           
            xhr.onreadystatechange = function(){                
                if (this.readyState == 4 && this.status == 200){
                    console.log(this.response, typeof this.response);               
                    var urlBuilder = window.URL || window.webkitURL;
                    document.getElementById("iFramePDF").src = urlBuilder.createObjectURL(this.response);
                    document.getElementById("iFramePDF").style.display = 'block';               
                }
            };    
            xhr.open('POST', urlSello, true);
            xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
            xhr.setRequestHeader("Content-length", params.length);
            xhr.responseType = 'blob';
            xhr.send(params);                       
        };                
    </script>
</head>

<body>
    <div id="contenido">     
        <button id="btnSello" type="button" onclick="generarSello();">Generar Sello</button>
                  
        <table>
            <tr><td>Numero de registro: </td> <td> <input name="numReg"  type="text" value="899998"></td></tr>
            <tr><td>Fecha de registro</td> <td> <input name="fechaReg"   type="text" value="2015/01/01 10:10:00"></td></tr>
            <tr><td>Codigo de oficina</td> <td> <input name="codigoOfi"  type="text" value="000"></td></tr>
            <tr><td>Nombre de oficina</td> <td> <input name="nombreOfi"  type="text" value="Rexistro Xeral \nProbas"></td></tr>
            <tr><td>Codigo de unidad</td> <td> <input name="codigoUnidad" type="text" value="V0"></td></tr>           
        </table>
    </div>
    

    <iframe id="iFramePDF" name="iFramePDF" src=""  width="100%" height="100%" frameBorder="0" styele="display:none"></iframe>

</body>
</html>

The important parts are:
  1. You create an object with all the params to serialize it in the way "name=value&" and to send the outcome string to the servlet.
  2. You invoke the servlet by using XMLHttpRequest. In the callback function, you convert the whole response to a new URL and pass it to the iframe

domingo, 7 de febrero de 2016

Spring Controller and Future Tasks

Suppose you have a web application in which the user can ask for reports. The application collects some data and then generate a PDF (or CSV, or whatever) and returns it to him.

Depending on what kind of report and what kind of filter data, the time the app requires to build the report may vary. So, for the same report, the user can get the file immediately, or may need to wait some seconds to get it.

Now, you dont want to force the user to wait for generated the report when it takes much time. If the application detects that the report is lasting much, instead the file, it returns a message telling him that his file will be sent by other means (for example, by sending an email later).

This behaviour can be achieved with Asynchronous tasks, and Futures.

We have a Controller which receives the user request and then calls a service. The service constructs the report, returns it to the controller, and finally the controller returns it to the user. This is the basic flow of much Spring MVC applications.

In this example I have a Service called InvoiceReportService which returns a InvoiceReport.
public class InvoiceReport {
 private byte[] content;

 public byte[] getContent() {
  return content;
 }

 public void setContent(byte[] content) {
  this.content = content;
 }
}

The service will be executed asynchronously so, instead returns a InvoiceReport object it will return a Future<InvoiceReport>:
public interface InvoiceReportService {

 Future<InvoiceReport> generateReport();

}

This is the service class. It just reads a PDF file from the classpath and returns it. I added a random delay to simulate some heavy tasks running. Note that the service method is annotated with @Async.
@Service
public class InvoiceReportServiceImpl implements InvoiceReportService {
 
 @Override
 @Async
 public Future<InvoiceReport> generateReport() {
  Resource resource = new ClassPathResource("spring-boot-reference.pdf");    
  byte[] bytes = null;
  try {
   bytes = IOUtils.toByteArray(resource.getInputStream());
  } catch (IOException e) {
   //Nothing     
  }
  
  simulateDelay();
  
  InvoiceReport report = new InvoiceReport();
  report.setContent(bytes);
  return new AsyncResult<InvoiceReport>(report);
 }

 
 private void simulateDelay() {
  int secondsSleep = new Random().nextInt(2);
  try {
   System.out.println("Going to sleep " + secondsSleep + " seconds");
   TimeUnit.SECONDS.sleep(secondsSleep);
  } catch (InterruptedException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }
  System.out.println("Awake!!");
 }
}

Working with futures you can specify a maximum time you want to wait for the result. If the result is not ready in this time, a TimeOutException is raised, but the underlying task is not interrupted.
So, the controller could be something like this:
@Controller
public class InvoiceController {
 
 @Autowired
 private InvoiceReportService invoiceReportSerive;
 
 @RequestMapping(value="/report", method=RequestMethod.POST)
 public HttpEntity<byte[]> getReport(HttpServletResponse response) throws IOException {
  Future<InvoiceReport> reportFutre = invoiceReportSerive.generateReport();
  
  InvoiceReport invoiceReport = null;
  try {
   invoiceReport = reportFutre.get(1, TimeUnit.SECONDS);
   System.out.println("In time");  
   return responseNow(invoiceReport);
  }
  catch(TimeoutException e) {
   System.out.println("Too late");  
   response.sendRedirect("/laterPlease");
   return null;
  }
  catch(Exception e) {
   //Handle exception
   return null;
  }
 }

 
 private HttpEntity<byte[]> responseNow(InvoiceReport invoiceReport) { 
     HttpHeaders header = new HttpHeaders();
     header.setContentType(new MediaType("application", "pdf"));
     header.set("Content-Disposition", "attachment; filename= \"report.pdf\"");
     header.setContentLength(invoiceReport.getContent().length);

     return new HttpEntity<byte[]>(invoiceReport.getContent(), header); 
 }
}

The controller waits, as much, as one second for the report. If the service does not respond in that time, then shows the user a info page. Otherwise, the controller returns the file.

But this has one problem. How does the user get the report when it is not completed in the specified timeout? We need other task which wait for report as long as necessary, because the service will be running even when the controller returns the info page.

To solve this I created a second service which is the one who will wait until the first one ends.
@Service
public class WaitUntilFinishService<T> {
 
 @Async
 public void waitUntilFinish (Future<T> future, AfterWaitComand<T> afterWaitCommand){
  try {
   T result = future.get();
   afterWaitCommand.docommand(result);
  } catch (InterruptedException | ExecutionException e) {
   System.out.println("The task was interrupted");
   e.printStackTrace();
  }
 }
}
This service receives the Future result and just waits for it. It also receives a call back service, so when the task is completed, it calls this call back service in order to complete the primary action.

This callback service implements a common interface, so you can wire any custom implementation you need. In this case, for example, the callback could retrieve the file and send it to the user by mail.
public interface AfterWaitComand<T> {
 void docommand(T result);
}

@Service
public class SendMailAfterWaitCommand implements AfterWaitComand<InvoiceReport> {

 @Override
 public void docommand(InvoiceReport result) {
  System.out.println("Sending invoice result by mail");
  System.out.println(result);  
 }

}

Whit thease improvements, the controller will be as follows:
@Controller
public class InvoiceController {
 
 @Autowired
 private InvoiceReportService invoiceReportSerive;
 
 @Autowired
 private WaitUntilFinishService<InvoiceReport> waitUntilFinishService;
 
 @Autowired
 private AfterWaitComand<InvoiceReport> sendMailAfterWaitCommand;
 
 
 
 @RequestMapping(value="/report", method=RequestMethod.POST)
 public HttpEntity<byte[]> getReport(HttpServletResponse response) throws IOException {
  Future<InvoiceReport> reportFutre = invoiceReportSerive.generateReport();
  
  InvoiceReport invoiceReport = null;
  try {
   invoiceReport = reportFutre.get(1, TimeUnit.SECONDS);
   System.out.println("In time");  
   return responseNow(invoiceReport);
  }
  catch(TimeoutException e) {
   System.out.println("Too late");
   waitUntilFinishService.waitUntilFinish(reportFutre, sendMailAfterWaitCommand);   
   response.sendRedirect("/laterPlease");
   return null;
  }
  catch(Exception e) {
   //Handle exception
   return null;
  }
 }

 
 private HttpEntity<byte[]> responseNow(InvoiceReport invoiceReport) { 
     HttpHeaders header = new HttpHeaders();
     header.setContentType(new MediaType("application", "pdf"));
     header.set("Content-Disposition", "attachment; filename= \"report.pdf\"");
     header.setContentLength(invoiceReport.getContent().length);

     return new HttpEntity<byte[]>(invoiceReport.getContent(), header); 
 }
}


You can download the source from my github: https://github.com/evazquezma/JEE6/tree/master/async