Buscar este blog

Mostrando entradas con la etiqueta Spring Boot. Mostrar todas las entradas
Mostrando entradas con la etiqueta Spring Boot. Mostrar todas las entradas

sábado, 27 de agosto de 2016

Windows Server - Spring Security Kerberos authentication example

This post is a step by step guide to configure a Kerberos service which is accesible for human users (by using a web browser) and for other java applications (by using a HTTP client).
This post is almost entirely based in the Spring Security Kerberos reference documentation. I just have taken the parts I needed to develop a funcionality in one of my applications.

The overview of the solution is as follows:
  • There is a Windows Server 2008 R2 machine called WIN-GG8QUO4LVI8.test.com.
  • In this server the Active Directory and KDC (Key Distribution Center) are running.
  • The AD has three users: tomcat, user1 and humanUser1.
  • The root domain (and forest, as there is just one domain) is TEST.COM.
  • There is a second machine outside the domain. A Windows 7 machine.
  • In this PC, the sec-server-spnego-form-auth application is deployed.
  • This service will be called neo.example.org.
  • In this PC, the rest client sc-client-rest-template will also be "deployed".


The objectives are:
  • When humanUser is logged in WIN-GG8QUO4LVI8.test.com, he can access the tomcat app without introducing his username neither password. His identity is loaded from the windows session.
  • When humanUser is logged in the other PC, he can access the tomcat app by introducing his username and password.
  • The rest client can access the tomcat app by identifying itself with a username (user1) and his keytab file.


This image summarize this infrastructure:


So, I have my Active Directory configured with de domain "test.com" in a computer named "WIN-GG8QUO4LVI8", ie, my Domain Controller.


Based on this information, I can create a kerberos conf file (or .ini). The content of krb5.conf file is:
[libdefaults]
 default_realm = TEST.COM
 default_tkt_enctypes = aes128-cts rc4-hmac des3-cbc-sha1 des-cbc-md5 des-cbc-crc
 default_tgs_enctypes = aes128-cts rc4-hmac des3-cbc-sha1 des-cbc-md5 des-cbc-crc
 permitted_enctypes   = aes128-cts rc4-hmac des3-cbc-sha1 des-cbc-md5 des-cbc-crc

[realms]
 TEST.COM  = {
  kdc = WIN-GG8QUO4LVI8.test.com
  default_domain = TEST.COM
 }

[domain_realm]
 .TEST.COM = TEST.COM

In the Active Directory I have the three necessary users. For example:



The steps  I'll follow to complete the deployments are as follow:
  1. Configure and install sec-server-spnego-form-auth
  2. Test the tomcat app from the Other PC with humanUser1
  3. Test the tomcat app from WIN-GG8QUO4LVI8 with humanUser1
  4. Configure and install sec-client-rest-template

1. Configure and install sec-server-spnego-form-auth

Configuration in WIN-GG8QUO4LVI8
First we need to setup a Service Principal Name (SPN) for the sec-server-spnego-form-auth.
A service is any process that do something, in this case, the tomcat app. Services are identified uniquely in the form of "<service class>/<host>:<port>[/<service name>]", and are they asociated with a login account name.

Our SPN will be "HTTP/neo.example.org" and the user associated will be "tomcat".
> setspn -A HTTP/neo.example.org tomcat

> setspn -L tomcat
Registered ServicePrincipalNames for CN=tomcat tomcat.,CN=Users,DC=test,DC=com:
        HTTP/neo.example.org

Now we need to generate a keytab file for this service and map the SPN created previously. Basically, we are saying that this keytab file belongs to this SPN, which is also asociated with a user.
You need to use the ktpass command:
> ktpass -out ./tomcat.keytab -mapuser tomcat@TEST.COM -princ HTTP/neo.example.org@TEST.COM -pass Password# -ptype KRB5_NT_PRINCIPAL -crypto ALL


> "c:\Program Files (x86)\Java\jre1.8.0_101\bin\klist.exe" -k -t -k tomcat.keytab

Key tab: tomcat.keytab, 5 entries found.

[1] Service principal: HTTP/neo.example.org@TEST.COM
         KVNO: 3
         Time stamp: Jan 01, 1970 01:00:00
[2] Service principal: HTTP/neo.example.org@TEST.COM
         KVNO: 3
         Time stamp: Jan 01, 1970 01:00:00
[3] Service principal: HTTP/neo.example.org@TEST.COM
         KVNO: 3
         Time stamp: Jan 01, 1970 01:00:00
[4] Service principal: HTTP/neo.example.org@TEST.COM
         KVNO: 3
         Time stamp: Jan 01, 1970 01:00:00
[5] Service principal: HTTP/neo.example.org@TEST.COM
         KVNO: 3
         Time stamp: Jan 01, 1970 01:00:00

The java klist command allows to list the entries in the keytab file. A keytab file contains pairs of kerberos principals an encrypted keys. In this case, the principal is the service.

Configuration in the Other PC
Once we have all the kerberos configuration done, we can start to prepare our tomcat application sec-server-spnego-form-auth.
This spring boot application has a configuration file, application.yml, with two importan keys:
  • service-principal. The name of the service that this application is providing.
  • keytab-localtion. The path to the keytab file which contains the keys wich authenticate this application in the KDC.

With de information above, the application.yml file would be:
server:
    port: 8080
app:
    service-principal: HTTP/neo.example.org@TEST.COM
    keytab-location: D:/kerberos/tomcat.keytab

Remember that we also have the krb5.conf with de global configuration. We can tell our app to use it with the argument -Djava.security.krb5.conf=/path/to/krb5.conf.

In order to start this application you have to execute the following command:
java -Djava.security.krb5.conf=D:/kerberos/krb5.conf -jar sec-server-spnego-form-auth-1.0.2.BUILD-SNAPSHOT.jar

Ok. Our tomcat app is up and running.

2. Test the tomcat app from the Other PC with humanUser1

In the Other PC, I had the following IPs in my hosts file:
127.0.0.1   neo.example.org

192.168.65.129 WIN-GG8QUO4LVI8
192.168.65.129 WIN-GG8QUO4LVI8.test.com

When a user tries to access to http://neo.example.org:8080/hello, he will get the auth form. This is because this user is outside the domain.


3. Test the tomcat app from WIN-GG8QUO4LVI8 with humanUser1

Now, humanUser1 logs in WIN-GG8QUO4LVI8, which is a PC inside the domain (well, it is the server domain...).
When this user tries to access to http://neo.example.org:8080/hello, he will enter automatically.

I will use firefox, so you need to configure the following:
  • Open Firefox.
  • At address field, type about:config.
  • In filter/search, type negotiate.
  • Set parameter network.negotiate-auth.trusted-uris to http://neo.example.org:8080 (if you already have other URL, you need to separate them by using ",").

I also added this URL to my local intranet sites in the Internet Explorer configuration.

4. Configure and install sec-client-rest-template

Fine. The human users have access to the web application. If the user is loged in inside the domain, he doesn´t need to autenticate again (Single Sign On), but if the user try to access from outside, he needs to use his login and password.

Now I want to authenticate a java application which will make a http connection (a GET request) to the secure page in the sec-server-spnego-form-auth application.

Configuration in WIN-GG8QUO4LVI8
You need to create a second keytab file. In this case, this file will contain the keys for user1, which will be the user used by the sec-client-rest-template.
Previously we created a keytabfile for the tomcat user, which is the acconut name asociated with the service, by using the ktpass command. Now we just need to create the ketabfile for a external user, without to associate this user with any service.

We use the ktab tool, shipped with Java.
> "c:\Program Files (x86)\Java\jre1.8.0_101\bin\ktab.exe" -a user1 Password# -k user1.keytab

> "c:\Program Files (x86)\Java\jre1.8.0_101\bin\klist.exe" -k -t -K user1.keytab

Key tab: user1.keytab, 3 entries found.

[1] Service principal: user1@TEST.COM
         KVNO: 1
         Key: 0x97bb5f77eb114814e2c1e639ec914429
         Time stamp: Aug 26,  2016 16:09:43
[2] Service principal: user1@TEST.COM
         KVNO: 1
         Key: 0x4f5ea81531e5136807d9fe9d75d0f2e6cbd9b6eacd546d7f
         Time stamp: Aug 26,  2016 16:09:43
[3] Service principal: user1@TEST.COM
         KVNO: 1
         Key: 0x63245e61ad75fbc0b9360b4e380d83d8
         Time stamp: Aug 26,  2016 16:09:43

Configuration in the Other PC
Once we have keytab file we can start to prepare our tomcat application sec-client-rest-template.
This spring boot application has a configuration file, application.yml, with two important keys:
  • user-principal. The user that this application will use to authenticate itself.
  • keytab-localtion. The path to the keytab file wich contains the keys wich authenticate this application in the KDC.
  • access-url: The URL of our service.

With the information above, the application.yml file would be:
app:
    user-principal: user1@TEST.COM
    keytab-location: D:/kerberos/user1.keytab
    access-url: http://neo.example.org:8080/hello


Remember that we also have the krb5.conf with de global configuration. We can tell our app to use it with de argument -Djava.security.krb5.conf=/path/to/krb5.conf.

In order to start this application you have to execute the following command:
java -Djava.security.krb5.conf=D:/kerberos/krb5.conf -jar sec-client-rest-template-1.0.2.BUILD-SNAPSHOT.jar

sábado, 2 de abril de 2016

Camel - CXF - Spring Boot - Web Service proxy Camel Route

This is a simple Spring Boot application, based on Apache Camel which act as a web service proxy. It is useful when you want to make accessible a web service allocated in a non accessible network. You can deploy it in an intermediate machine accessible from both sides.

This is a project based on this post in Java Code Geeks, but making it bootiful ;).

The project is very simple, you only need one Camel Route:
@Configuration
public class CamelConfig {

 @Bean
 public RouteBuilder routeBuilder() {
  return new RouteBuilder() {
   @Override
   public void configure() throws Exception {
    from("cxf:bean:servicePublisher?dataFormat=POJO")
    .log(LoggingLevel.INFO,"Doing some stuff")
    .to("cxf:bean:serviceConsumer?dataFormat=POJO");
   }
  };
 }
}

The route uses two CXF Endpoints, by publishing and by consuming the same web service contract.
This is the CXF config:
@Configuration
@ImportResource("classpath:/spring/conduit-config.xml")
public class CxfConfig {

 @Value("${service.publish.localEndpoint}")
 private String localEndPoint;

 @Value("${service.client.remoteEndpoint}")
 private String remoteEndPoint;


 @Bean
 public LoggingInInterceptor loggingInInterceptor() {
  final LoggingInInterceptor loggingInInterceptor = new LoggingInInterceptor();
  loggingInInterceptor.setPrettyLogging(true);
  return loggingInInterceptor;
 }


 @Bean
 public LoggingOutInterceptor loggingOutInterceptor() {
  final LoggingOutInterceptor loggingOutInterceptor = new LoggingOutInterceptor();
  loggingOutInterceptor.setPrettyLogging(true);
  return loggingOutInterceptor;
 }


 @Bean
 public GZIPInInterceptor gzipInInterceptor() {
  return new GZIPInInterceptor();
 }


 @Bean
 public CxfEndpoint servicePublisher(final CamelContext camelContext) {
   final CxfComponent cxfComponent = new CxfComponent(camelContext);

   final CxfEndpoint serviceEndpoint = new CxfEndpoint("", cxfComponent);
   serviceEndpoint.setServiceClass(ProductPortImpl.class);
   serviceEndpoint.setAddress(localEndPoint);
   serviceEndpoint.setWsdlURL("/wsdl/service/productService.wsdl");

   serviceEndpoint.setServiceNameString("{http://ws.javacodegeeks.com/product-service}ProductService");
   serviceEndpoint.setEndpointNameString("ProductPort");

   serviceEndpoint.setDataFormat(DataFormat.POJO);

   serviceEndpoint.setLoggingFeatureEnabled(true);
   serviceEndpoint.getInInterceptors().add(gzipInInterceptor());
   serviceEndpoint.getInInterceptors().add(loggingInInterceptor());
   serviceEndpoint.getOutInterceptors().add(loggingOutInterceptor());

   return serviceEndpoint;
 }


 @Bean
 public CxfEndpoint serviceConsumer(final CamelContext camelContext) {
   final CxfComponent cxfComponent = new CxfComponent(camelContext);

   final CxfEndpoint serviceEndpoint = new CxfEndpoint("", cxfComponent);
   serviceEndpoint.setServiceClass(Product.class);
   serviceEndpoint.setAddress(remoteEndPoint);
   serviceEndpoint.setWsdlURL("/wsdl/service/productService.wsdl");

   serviceEndpoint.setServiceNameString("{http://ws.javacodegeeks.com/product-service}ProductService");
   serviceEndpoint.setEndpointNameString("ProductPort");

   serviceEndpoint.setDataFormat(DataFormat.POJO);

   serviceEndpoint.setLoggingFeatureEnabled(true);
   serviceEndpoint.getInInterceptors().add(gzipInInterceptor());
   serviceEndpoint.getInInterceptors().add(loggingInInterceptor());
   serviceEndpoint.getOutInterceptors().add(loggingOutInterceptor());

   return serviceEndpoint;
 }
}

The end points configuration is very similar, but there is two differences:
  • The publisher service class references the service impl class. The consumer service class references the service interface class.
  • The publisher  service address references the full URL in which this project will publish the service. The consumer service address references the full URL in which the true service is published (by a third app)

You also need to keep in mind the SSL, so you need a http conduit for the client end point. I tried to configure it with java-based configuration, but I couldn't, so I finally  opted for XML-based configuration.
This is the conduit-config.xml file:
<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
 xmlns:cxf="http://camel.apache.org/schema/cxf"
 xmlns:sec="http://cxf.apache.org/configuration/security" 
 xmlns:http="http://cxf.apache.org/transports/http/configuration"
 xmlns:util="http://www.springframework.org/schema/util"
 xsi:schemaLocation="
     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
              http://camel.apache.org/schema/cxf http://camel.apache.org/schema/cxf/camel-cxf.xsd
              http://camel.apache.org/schema/spring http://camel.apache.org/schema/spring/camel-spring.xsd
              http://cxf.apache.org/configuration/security http://cxf.apache.org/schemas/configuration/security.xsd
              http://cxf.apache.org/transports/http/configuration http://cxf.apache.org/schemas/configuration/http-conf.xsd">

 
 <http:conduit name="{http://ws.javacodegeeks.com/product-service}ProductPort.http-conduit">
     <http:client Accept="text/xml" AcceptEncoding="gzip,deflate,sdch"  AllowChunking="true" AutoRedirect="true" CacheControl="No-Cache" ContentType="text/xml"/>
     
  <http:tlsClientParameters disableCNCheck="true">
   <sec:trustManagers>
    <sec:keyStore file="${service.client.truststore.path}" type="jks" password="${service.client.truststore.pass}" />
   </sec:trustManagers>
  </http:tlsClientParameters>
 </http:conduit>
  
</beans>

Some configuration values are environment-dependant, so I used a external properties file. This file also references a truststore used by the conduit config.
This is the PropertiesConfig file:
@Configuration
@PropertySources({
 @PropertySource("classpath:conf.properties"),
 @PropertySource("file:${propertyFile}")
})
public class PropertiesConfig {
 @Bean
 public PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
  return new PropertySourcesPlaceholderConfigurer();
 }
}

When you launch the project, you need to pass it the propertyFile key, for example as a command line argument.

The full source code is in my github: https://github.com/evazquezma/JEE6/tree/master/proxy-service

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