Monday, July 21, 2014

Disable Spring Boot Production Ready Services

For the past two months I've had the pleasure of working with The Lampo Group developing Hypermedia-Driven REST services using Spring Boot. Spring Boot makes it super simple to get started writing REST services. One of its biggest advantages is by default it embeds tomcat as the servlet container, allowing the developer to focus on other important things. Another great thing about Spring Boot is it includes the ability to easily enable what they call Production-Ready or Production-Grade Services. These services allow you to monitor and manage your application when its pushed to production and it's as easy as adding a dependency to your project. Unfortunately, it wasn't well documented on how to disable some of these services. But before I show you how to disable them, let me first show you how to enable them.

Enabling Spring Boot's Production-Ready Services
One of the reasons we wanted to enable some of the production-ready services was our target production environment was Amazon Web Services (AWS). As a part of that they support Elastic Load Balancing which allows one to configure a health check endpoint. It's basically an endpoint you configure in AWS that gets pinged to make sure the EC2 instance is up and running. As luck would have it, one of the services included in Spring Boot's production-ready services was a health endpoint.

To enable the production-ready services all you have to do is add a dependency to your project. If you are using maven all you have to do is add the following to your pom.xml.

<dependency>
    <groupid>org.springframework.boot</groupid>
    <artifactid>spring-boot-starter-actuator</artifactid>
    <version>1.1.4.RELEASE</version>
</dependency>


After you make the change to your pom.xml, just rebuild your project and you should be able to access http://localhost:8080/health and see something like this:



Not only does it add a health endpoint, but it also adds: autoconfig, beans, configprops, dump, env, info, metrics, mappings, shutdown (not enabled by default over HTTP), and trace. Like us, you might not want to expose all these endpoints in a production environment. In fact, all we wanted to enable was the health and info endpoints. The following will show you how to disable each service individually and how to re-enable them dynamically at runtime.

Disabling Spring Boot's Production-Ready Services
When you include the spring-boot-starter-actuator dependency in your project, it automatically exposes 11 different endpoints in your project. What I wanted to be able to do was disable all endpoints except health and info, but also have the ability to enable the other services at runtime via environment variables.

To disable some of the production-ready services add the following to your /src/main/resources file. These will be your default settings for your project.

endpoints.autoconfig.enabled=false
endpoints.beans.enabled=false
endpoints.configprops.enabled=false
endpoints.dump.enabled=false
endpoints.env.enabled=false
endpoints.health.enabled=true
endpoints.info.enabled=true
endpoints.metrics.enabled=false
endpoints.mappings.enabled=false
endpoints.shutdown.enabled=false
endpoints.trace.enabled=false


To enable/disable endpoints externally at runtime you can follow any one of these steps. Since our project is following the 12-factor app rules we needed the ability to enable/disable endpoints by setting environment variables. So for example, if I wanted to enable the metrics endpoint at runtime, I would set the following environment variable.

export ENDPOINTS_METRICS_ENABLED=true


After restarting Sprint Boot you can access the metrics endpoint at http://localhost:8080/metrics. Note, that as of version 1.0.1.RELEASE, you are unable to disable the mappings endpoint, but this was quickly fixed in 1.1.3.RELEASE.

In summary, I've been very impressed with how easy it is to work with Spring Boot and the production-ready services. In another article I'll cover how to use maven filtering, along with the git-commit-id-plugin, to display project information in the info endpoint.

Tuesday, July 23, 2013

Remember Target URL with Spring Security and Jasig CAS

I recently ran into a unique issue when combining Spring Security, Jasig CAS, and the Ozone Widget Framework (OWF). Basically, the original target URL was not being remembered with all the redirects from the CAS client filters, to CAS server, and then back to Spring Security. For example, if a user browsed to https://localhost/myapp/widget.jsp, they would be redirected to https://localhost/cas. Upon successful login, the user would be incorrectly redirected back to https://localhost/myapp instead of the original target URL.

SavedRequestAwareAuthenticationSuccessHandler almost worked
I thought I had found a solution by using Spring Security's SavedRequestAwareAuthenticationSuccessHandler, and that did work for single requests, but it did not work in an environment like OWF where multiple widgets are loaded simultaneously. The reason is because SavedRequestAwareAuthenticationSuccessHandler extracts the original target URL from the session, which is originally set by the ExceptionTranslationFilter. Since it used the session there could only be one original target URL. So if I had a workspace in OWF that loaded two different widgets from the same WAR (/myapp/widget1.jsp and /myapp/widget2.jsp), then there will only be one target URL saved in the session and both widgets would load the last saved URL, which is obviously not good.

There were several ideas out there, but I really didn't like any of them. Some required you to modify the CAS login form or return some weird javascript in one of the responses. What I wanted was the ability to preserve the original target URL within all the redirect URLs via a parameter. The only problem was, to my knowledge, nothing like this existed in Spring Security. So the following will show you how I extended Spring Security to preserve the original target URL via a parameter. As a simple example, here is a sequence of URLs that I was attempting to support (Note, the URL in the params are typically encoded but I kept them decoded for readability):

  1. User browses to: https://localhost/myapp/widget.jsp
  2. CAS Client filters redirect to: https://localhost/cas/login?service=https://localhost/myapp/j_spring_cas_security_check&spring-security-redirect=/widget.jsp
  3. After authentication user is redirected to the URL defined in the service paramter https://localhost/myapp/j_spring_cas_security_check&spring-security-redirect=/widget.jsp which is monitored by Spring Security.
  4. Once Spring Security does it's thing, it needs to redirect to the value in the spring-security-redirect parameter.
The following example works with Spring Security 3.1.4.RELEASE and Tomcat 7.0.21.
Spring Security Application Context File
What Spring Security example wouldn't be complete without some XML? The following is the application context file (note, for simplicity I have hardcoded the CAS urls and other values, but these would typically be read in from a properties file):

<sec:http use-expressions="true" entry-point-ref="casEntryPoint">
    <sec:intercept-url pattern="/css/**" access="permitAll" />
    <sec:intercept-url pattern="/images/**" access="permitAll" />
    <sec:intercept-url pattern="/scripts/**" access="permitAll" />
    <sec:intercept-url pattern="/**" access="hasRole('ROLE_USER')" requires-channel="https"/>
    
    <sec:custom-filter ref="casFilter" after="CAS_FILTER"/>
</sec:http>

<sec:authentication-manager alias="authenticationManager">
    <sec:authentication-provider ref="casAuthProvider" />
</sec:authentication-manager>

<bean id="casEntryPoint" class="com.example.security.cas.web.RememberCasAuthenticationEntryPoint">
 <property name="loginUrl" value="https://localhost/cas/login" />
 <property name="serviceProperties" ref="serviceProperties" />
 <property name="targetUrlParameter" value="spring-security-redirect" />
</bean>

<bean id="casAuthProvider" class="com.example.security.cas.authentication.RememberCasAuthenticationProvider">
 <property name="userDetailsService" ref="userService" />
 <property name="serviceProperties" ref="serviceProperties" />
 <property name="ticketValidator" ref="ticketValidator" />
 <property name="key" value="an_id_for_this_auth_provider_only" />
 <property name="targetUrlParameter" value="spring-security-redirect" />
</bean>

<!--
 - This is the filter that monitors all incoming requests for the url /myapp/j_spring_cas_security_check (sequence #7).
 - Sets the targetUrlParameter to redirect to the target URL after authentication.
 - Also sets the authenticationDetailsSource to our custom one in order to have access to the HttpServletRequest
 - in RememberCasAuthenticationProvider for ticket validation.
 - Tried setting the authenticationSuccessHandler to SavedRequestAwareAuthenticationSuccessHandler, and that works
 - for a single request, but in OWF if you have two widgets loading different URLs, it doesn't work because
 - it loads the saved url from the session object, so both widgets load the same url.
-->
<bean id="casFilter" class="org.springframework.security.cas.web.CasAuthenticationFilter">
 <property name="authenticationManager" ref="authenticationManager" />
 <property name="authenticationDetailsSource">
  <bean class="com.example.security.web.authentication.RememberWebAuthenticationDetailsSource"/>
 </property>
 <property name="authenticationFailureHandler">
  <bean class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
   <property name="defaultFailureUrl" value="/cas_failed.jsp" />
  </bean>
 </property>
    <property name="authenticationSuccessHandler">
        <bean class="org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler">
            <property name="defaultTargetUrl" value="/" />
            <property name="targetUrlParameter" value="spring-security-redirect" />
        </bean>
    </property>
 <property name="proxyGrantingTicketStorage" ref="proxyGrantingTicketStorage" />
 <property name="proxyReceptorUrl" value="/secure/receptor" />
</bean>

CAS Entry Point
When dealing with Spring Security it all starts with the http entry-point-ref attribute. Here is the code for RememberCasAuthenticationEntryPoint:

package com.example.security.cas.web;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.jasig.cas.client.util.CommonUtils;

import org.springframework.security.cas.web.CasAuthenticationEntryPoint;

/**
 * Class which is responsible for remembering the original target url specified by the client.
 * Takes the original target url and appends that to the service param used by CAS.
 * This will later be used to redirect to the target URL after authentication.
 */
public class RememberCasAuthenticationEntryPoint extends CasAuthenticationEntryPoint {
    String targetUrlParameter = "spring-security-redirect";
    
    protected String createServiceUrl(final HttpServletRequest request, final HttpServletResponse response) {
        String service = this.serviceProperties.getService();
        
        String servletPath = request.getServletPath();        
        if (servletPath) {
            service += String.format("?%s=%s", this.targetUrlParameter, servletPath);
        }
        
        return CommonUtils.constructServiceUrl(null, response, service, null, this.serviceProperties.getArtifactParameter(), this.encodeServiceUrlWithSessionId);
    }
}

CAS Authentication Provider
Next up is the CAS authentication provider which we have defined as RememberCasAuthenticationProvider. This bean is given to the CAS Custom Filter under the alias authenticationManager. Here is the code for RememberCasAuthenticationProvider:

package com.example.security.cas.authentication;

import com.example.security.web.authentication.RememberWebAuthenticationDetails;

import org.jasig.cas.client.validation.Assertion;
import org.jasig.cas.client.validation.TicketValidationException;

import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.AccountStatusUserDetailsChecker;
import org.springframework.security.cas.authentication.CasAuthenticationProvider;
import org.springframework.security.cas.authentication.CasAuthenticationToken;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.web.CasAuthenticationFilter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
import org.springframework.security.core.userdetails.*;

/**
 * CasAuthenticationProvider that tries to remember the original target url requested by the client.
 * The trick is having access to the HttpServletRequest in the authenticateNow() method.
 * This is accomplished via the RememberWebAuthenticationDetails class.
 * Since authenticateNow() was marked as private in CasAuthenticationProvider I had to also override
 * the authenticate() method. Created spring security jira https://jira.springsource.org/browse/SEC-2188
 * to address making authenticateNow protected so we don't have to duplicate authenticate().
 */
public class RememberCasAuthenticationProvider extends CasAuthenticationProvider {

    UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker();
    ServiceProperties serviceProperties;
    GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
    String targetUrlParameter = "spring-security-redirect";

    /**
     * Straight copy and paste from CasAuthenticationProvider
     * @see spring security jira https://jira.springsource.org/browse/SEC-2188
     */
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        ...
    }

    /**
     * The service URL used in ticketValidator.validate() needs to match the service URL given to CAS when
     * the ticket was granted.
     */
    protected CasAuthenticationToken authenticateNow(final Authentication authentication) throws AuthenticationException {
        try {
            String targetPath = this.getTargetPath(authentication.getDetails());
        
            def service = String.format("%s?%s", serviceProperties.getService(), targetPath);
            
            final Assertion assertion = this.ticketValidator.validate(authentication.getCredentials().toString(), service);
            final UserDetails userDetails = loadUserByAssertion(assertion);
            userDetailsChecker.check(userDetails);
            return new CasAuthenticationToken(this.key, userDetails, authentication.getCredentials(),
                    authoritiesMapper.mapAuthorities(userDetails.getAuthorities()), userDetails, assertion);
        } catch (final TicketValidationException e) {
            throw new BadCredentialsException(e.getMessage(), e);
        }
    }
    
    /**
     * Extracts the original target url form the query string.
     * Example query string: spring-security-redirect=/widget.jsp&ticket=ST-112-RiRTVZmzghHO7az5gpJF-cas
     */
    protected String getTargetPath(Object authenticationDetails) {
        String targetPath = "";
        
        if (authenticationDetails instanceof RememberWebAuthenticationDetails) {
            RememberWebAuthenticationDetails details = (RememberWebAuthenticationDetails) authenticationDetails;
            String queryString = details.getQueryString();
            
            if (queryString) {
                int start = queryString.indexOf(this.targetUrlParameter);
                if (start >= 0) {
                    int end = queryString.indexOf("&", start);
                    if (end >= 0) {
                        targetPath = queryString.substring(start, end);
                    } else {
                        targetPath = queryString.substring(start);
                    }
                }
            }
        }
        
        return targetPath;
    }
}

Authentication Details Source
Since I needed access to the requests query string in the RememberCasAuthenticationProvider.getTargetPath() method, I needed to provide a different WebAuthenticationDetails class. This was accomplished by setting the authenticationDetailsSource property on the CAS Filter. Here is the code for RememberWebAuthenticationDetailsSource:

package com.example.security.web.authentication;

import javax.servlet.http.HttpServletRequest;

import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.web.authentication.WebAuthenticationDetails

public class RememberWebAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {

    public WebAuthenticationDetails buildDetails(HttpServletRequest request) {
        return new RememberWebAuthenticationDetails(request);
    }
}

Here is the code for RememberWebAuthenticationDetails:

package com.example.security.web.authentication;

import javax.servlet.http.HttpServletRequest;

import org.springframework.security.web.authentication.WebAuthenticationDetails;

public class RememberWebAuthenticationDetails extends WebAuthenticationDetails {
    private final String queryString;

    public RememberWebAuthenticationDetails(HttpServletRequest request) {
        super(request);
        
        this.queryString = request.getQueryString();
    }
    
    public String getQueryString() {
        return this.queryString;
    }
}

Summary
That pretty much does it. I know it's a lot of code, but once I got familiar with Spring Security it really wasn't that much. Once you get this all configured it should just work and the users original target URL can be seen when getting redirected to the CAS login page and then after authentication, the user should be redirected back to the original target URL. And in an OWF environment where multiple URLs from the same WAR are being loaded simultaneously this solution seems to work.

I do want to mention that we have since noticed that there are conditions where the entire URL is not remembered. Simple URLs like /myapp/widget.jsp work great, but REST URLs like /myapp/api/events/1 are not completely preserved. Nor are params remembered either like /myapp/widget.jsp?id=1. At this time we really don't need that capability but I don't think it would be hard to add it. Most of the work has already been done.

One other thing. I think this experiment begs the question: why doesn't Spring Security already support something like this out of the box? It would seem like a very common use case. While researching it seems there might be a reluntance to support this feature due to security concerns of a malicious user gaining access to a resource they are not authorized for. I didn't do super extensive testing, but in the testing I did do, I was not able to gain access to resources I was not authorized for. Perhaps when/if I come back to these classes and add support for the noted issues above I might submit a patch back to Spring Security so this can get incorporated into Spring Security.

Friday, June 28, 2013

Setting Gradle home directory and proxy in Jenkins

Real quick. Spent the past few hours working around some nasty issues with gradle and jenkins. It seems due to a bug, the jenkins gradle plugin puts the dependency/artifact cache under the jobs workspace. This really isn't a good idea as every job would then download all of the projects artifacts taking up large amounts of space. At the same time, I also needed to setup the proxy information for gradle, which sadly doesn't reuse the jenkins proxy information.

I was able to finally figure out a good place to define the gradle user home and proxy information in a single place to prevent each job from having to define it.

Go into Manage Jenkins > Configure System. Under Global properties check Environment variables and fill in the following for name and value:

name: GRADLE_OPTS
value: -Dgradle.user.home=/home/tomcat/.gradle -Dhttp.proxyHost=101.10.10.10 -Dhttp.proxyPort=3128

For the gradle.user.home property, I tried using ~/.gradle, but that didn't work which means most likely my $HOME environment variable was not set for whatever reason. My guess is it has something to do with all the troubles I've had lately using the bitnami jenkins amazon ami. I also tried setting the environment variable GRADLE_USER_HOME, but that didn't seem to work. Either way, hopefully this will help others.