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.

Tuesday, June 25, 2013

Resource Filtering with Gradle

My team has recently started a new Java web application project and we picked gradle as our build tool. Most of us were extremely familiar with maven, but decided to give gradle a try.
Today I had to figure out how to do resource filtering in gradle. And to be honest it wasn't as easy as I thought it should be; as least coming from a maven background. I eventually figured it out, but wanted to post my solution to make it easier for others.

What is Resource Filtering?
First, for those that may not know, what is resource filtering? It's basically a way to avoid hard coding values in files and make them more dynamic. For example, I may want to display the version of my application in my application. The version is usually defined in your build file and this value can be injected or replaced in your configuration file during assembly. So I could have a file called config.properties under src/main/resources with the following content:
application.version=${application.version}. With resource filtering the ${application.version} value gets replaced with 1.0.0 during assembly, then my application can load config.properties and display the application version.

It's an extremely valuable and powerful feature in build tools like maven and one that I took advantage of often.

Resource Filtering in Gradle
With this being my first gradle project, I needed to find the recommended way to enable resource filtering in gradle. My first problem I had to figure out was where to define the property. In maven this would typically be defined in the project's pom.xml file as a maven property:

<properties>
    <application.version>1.0.0</application>
</properties>

For gradle the appropriate place seemed to be the project's gradle.properties file. So you would add the following to your project's gradle.properties file (Note, I'm not suggesting you would hardcode the modules version in the gradle.properties file. Obviously the value would be derived from the version property in your project. I'm just using this for a simple example):

application.version=1.0.0

The next, and most difficult, problem I had to track down was how to actually enable resource filtering. I was hoping to just set some enableFiltering option and define the includes/excludes list, but that doesn't seem to be the case (extra tip: don't do filtering on binary files like images). I did find some resources online, but this one seemed to be the best approach. So you will need to add the following to your build.gradle file:

import org.apache.tools.ant.filters.*

processResources {
    filter ReplaceTokens, tokens: [
        "application.version": project.property("application.version")
    ]
}

Next you need to update your resource file. So put a config.properties file under src/main/resources and add this:

application.version=@application.version@

Note, the use of @ instead of ${}. This is because gradle is based on ant, and ant by default uses the @ character as the token identifier whereas maven uses ${}.

Finally, if you build your project you can look under build/resources/main and you should see a config.properties file with a value of 1.0.0. You can also open up your artifact and see the same result.

Dot notation
One thing to note is I typically use a period or dot to separate words for properties: application.version instead of applicationVersion. So you will notice the surrounding quotes around "application.version" in the build.gradle file. This is required as failing to surround the key by quotes will fail the build. Probably because groovy's dynamic nature thinks you are traversing an object.

Overriding
I also investigated the best approach to overriding properties in gradle, as this appeared to be slightly different then how it's done in maven. In maven, properties can be overridden by properties defined in the user's setting.xml file or on the command line with the -D option. To override application.version in gradle on the command line I had to run the following:

gradle assemble -Papplication.version=2.0.0

If you want to override it for all projects you can add the property in your gradle.properties file under /user_home/.gradle.

Also, if you are overriding the value via the command line and your property value contains special characters like a single quote, you can wrap the value with double quotes like the following to get it to work:

gradle assemble -Papplication.version="2.0.0'6589"

Summary
Well I hope this helps and if anyone from the gradle community sees a better way to perform resource filtering I'd love to hear about it. I'd also like to see something as important as resource filtering becoming easier to perform in gradle. I think it's crazy having to add an import statement to perform something so simple.