How to correctly set the service URL in Spring's CAS service properties
Asked Answered
G

5

8

When working with Spring Security + CAS I keep hitting a small road block with the callback URL that is sent to CAS, ie the service property. I've looked at a bunch of examples such as this and this but they all use hard coded URLs (even Spring's CAS docs). A typical snip looks something like this...

  <bean id="serviceProperties" class="org.springframework.security.ui.cas.ServiceProperties">
    <property name="service" value="http://localhost:8080/click/j_spring_cas_security_check" />
  </bean>

First, I don't want to hard code the server name or the port since I want this WAR to be deployable anywhere and I don't want my application tied to a particular DNS entry at compile time. Second, I don't understand why Spring can't auto detect my application's context and the request's URL to automagically build the URL. The first part of that statement still stand but As Raghuram pointed out below with this link, we can't trust the HTTP Host Header from the client for security reasons.

Ideally I would like service URL to be exactly what the user requested (as long as the request is valid such as a sub domain of mycompany.com) so it is seamless or at the very least I would like to only specify some path relative my applications context root and have Spring determine the service URL on the fly. Something like the following...

  <bean id="serviceProperties" class="org.springframework.security.ui.cas.ServiceProperties">
    <property name="service" value="/my_cas_callback" />
  </bean>

OR...

  <bean id="serviceProperties" class="org.springframework.security.ui.cas.ServiceProperties">
    <property name="service" value="${container.and.app.derived.value.here}" />
  </bean>

Is any of this possible or easy or have I missed the obvious?

Grani answered 29/12, 2010 at 15:8 Comment(3)
Perhaps this link is related and gives some insights into your requirement/problem?Fontanel
Well, I certainly learned something and eliminated one possible solution. Since I can't rely on the HTTP request I would still like to set the service via some derived values at deploy time which should be secure.Grani
I am using spring 3; note the link to the spring security 3 docsGrani
B
4

In Spring 2.6.5 spring you could extend org.springframework.security.ui.cas.ServiceProperties

In spring 3 the method is final you could get around this by subclassing the CasAuthenticationProvider and CasEntryPoint and then use with your own version of ServiceProperties and override the getService() method with a more dynamic implementation.

You could use the host header to calculate the the required domain and make it more secure by validating that only domains/subdomains under your control are used. Then append to this some configurable value.

Of course you would be at risk that your implementation was insecure though... so be careful.

It could end up looking like:

<bean id="serviceProperties" class="my.ServiceProperties">
    <property name="serviceRelativeUrl" value="/my_cas_callback" />
    <property name="validDomainPattern" value="*.mydomain.com" />
</bean>
Bert answered 9/1, 2011 at 19:28 Comment(3)
As per the linked document I am using Spring Security 3 which has all of those methods tagged as final (static.springsource.org/spring-security/site/docs/3.0.x/apidocs/…)Grani
So then as the issue states you could just subclass the CasAuthenticationProvider and CasEntryPoint and provide you own version of Service properties. I've updated the answer to make it more explicitBert
I think you're probably correct. I haven't had a chance to try this out yet but when I do, and unless a better answer comes around, this is looking like it's going to be the best answer.Grani
C
7

I know this is a bit old but I just had to solve this very problem and couldn't really find anything in the newer stacks.

We have multiple environments sharing the same CAS service (think dev, qa, uat and local development environments); we have the ability to hit each environment from more than one url (via the client side web server over a reverse proxy and directly to the back-end server itself). This means that specifying a single url is difficult at best. Maybe there's a way to do this but being able to use a dynamic ServiceProperties.getService(). I'll probably add some kind of server suffix check to ensure that the url isn't hijacked at some point.

Here's what I did to get the basic CAS flow working regardless of the URL used to access the secured resource...

  1. Override the CasAuthenticationFilter.
  2. Override the CasAuthenticationProvider.
  3. setAuthenticateAllArtifacts(true) on the ServiceProperties.

Here's the long form of my spring configuration bean:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, jsr250Enabled = true)
public class CasSecurityConfiguration extends WebSecurityConfigurerAdapter {

Just the usual spring configuration bean.

@Value("${cas.server.url:https://localhost:9443/cas}")
private String casServerUrl;

@Value("${cas.service.validation.uri:/webapi/j_spring_cas_security_check}")
private String casValidationUri;

@Value("${cas.provider.key:whatever_your_key}")
private String casProviderKey;

Some externalized configuration parameters.

@Bean
public ServiceProperties serviceProperties() {
    ServiceProperties serviceProperties = new ServiceProperties();
    serviceProperties.setService(casValidationUri);
    serviceProperties.setSendRenew(false);
    serviceProperties.setAuthenticateAllArtifacts(true);
    return serviceProperties;
}

The key thing above is the setAuthenticateAllArtifacts(true) call. This will make the service ticket validator use the AuthenticationDetailsSource implementation rather than a hard-coded ServiceProperties.getService() call

@Bean
public Cas20ServiceTicketValidator cas20ServiceTicketValidator() {
    return new Cas20ServiceTicketValidator(casServerUrl);
}

Standard ticket validator..

@Resource
private UserDetailsService userDetailsService;

@Bean
public AuthenticationUserDetailsService authenticationUserDetailsService() {
    return new AuthenticationUserDetailsService() {
        @Override
        public UserDetails loadUserDetails(Authentication token) throws UsernameNotFoundException {
            String username = (token.getPrincipal() == null) ? "NONE_PROVIDED" : token.getName();
            return userDetailsService.loadUserByUsername(username);
        }
    };
}

Standard hook to an existing UserDetailsService

@Bean
public CasAuthenticationProvider casAuthenticationProvider() {
    CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider();
    casAuthenticationProvider.setAuthenticationUserDetailsService(authenticationUserDetailsService());
    casAuthenticationProvider.setServiceProperties(serviceProperties());
    casAuthenticationProvider.setTicketValidator(cas20ServiceTicketValidator());
    casAuthenticationProvider.setKey(casProviderKey);
    return casAuthenticationProvider;
}

Standard authentication provider

@Bean
public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
    CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();
    casAuthenticationFilter.setAuthenticationManager(authenticationManager());
    casAuthenticationFilter.setServiceProperties(serviceProperties());
    casAuthenticationFilter.setAuthenticationDetailsSource(dynamicServiceResolver());
    return casAuthenticationFilter;
}

Key here is the dynamicServiceResolver() setting..

@Bean
AuthenticationDetailsSource<HttpServletRequest,
        ServiceAuthenticationDetails> dynamicServiceResolver() {
    return new AuthenticationDetailsSource<HttpServletRequest, ServiceAuthenticationDetails>() {
        @Override
        public ServiceAuthenticationDetails buildDetails(HttpServletRequest context) {
            final String url = makeDynamicUrlFromRequest(serviceProperties());
            return new ServiceAuthenticationDetails() {
                @Override
                public String getServiceUrl() {
                    return url;
                }
            };
        }
    };
}

Dynamically creates the service url from the makeDynamicUrlFromRequest() method. This bit is used upon ticket validation.

@Bean
public CasAuthenticationEntryPoint casAuthenticationEntryPoint() {

    CasAuthenticationEntryPoint casAuthenticationEntryPoint = new CasAuthenticationEntryPoint() {
        @Override
        protected String createServiceUrl(final HttpServletRequest request, final HttpServletResponse response) {
            return CommonUtils.constructServiceUrl(null, response, makeDynamicUrlFromRequest(serviceProperties())
                    , null, serviceProperties().getArtifactParameter(), false);
        }
    };
    casAuthenticationEntryPoint.setLoginUrl(casServerUrl + "/login");
    casAuthenticationEntryPoint.setServiceProperties(serviceProperties());
    return casAuthenticationEntryPoint;
}

This part uses the same dynamic url creator when CAS wants to redirect to the login screen.

private String makeDynamicUrlFromRequest(ServiceProperties serviceProperties){
    return "https://howeverYouBuildYourOwnDynamicUrl.com";
}

This is whatever you make of it. I only passed in the ServiceProperties to hold the URI of the service that we're configured for. We use HATEAOS on the back-side and have an implementation like:

return UriComponentsBuilder.fromHttpUrl(
            linkTo(methodOn(ExposedRestResource.class)
                    .aMethodOnThatResource(null)).withSelfRel().getHref())
            .replacePath(serviceProperties.getService())
            .build(false)
            .toUriString();

Edit: here's what I did for the list of valid server suffixes..

private List<String> validCasServerHostEndings;

@Value("${cas.valid.server.suffixes:company.com,localhost}")
private void setValidCasServerHostEndings(String endings){
    validCasServerHostEndings = new ArrayList<>();
    for (String ending : StringUtils.split(endings, ",")) {
        if (StringUtils.isNotBlank(ending)){
            validCasServerHostEndings.add(StringUtils.trim(ending));
        }
    }
}

private String makeDynamicUrlFromRequest(ServiceProperties serviceProperties){
    UriComponents url = UriComponentsBuilder.fromHttpUrl(
            linkTo(methodOn(ExposedRestResource.class)
                    .aMethodOnThatResource(null)).withSelfRel().getHref())
            .replacePath(serviceProperties.getService())
            .build(false);
    boolean valid = false;
    for (String validCasServerHostEnding : validCasServerHostEndings) {
        if (url.getHost().endsWith(validCasServerHostEnding)){
            valid = true;
            break;
        }
    }
    if (!valid){
        throw new AccessDeniedException("The server is unable to authenticate the requested url.");
    }
    return url.toString();
}
Chinchin answered 18/2, 2015 at 19:55 Comment(1)
Most useful info I could find on this post. This is amazing. I was really struggling with implementing something like this (for the same reasons you were) in our complex system.Carisacarissa
B
4

In Spring 2.6.5 spring you could extend org.springframework.security.ui.cas.ServiceProperties

In spring 3 the method is final you could get around this by subclassing the CasAuthenticationProvider and CasEntryPoint and then use with your own version of ServiceProperties and override the getService() method with a more dynamic implementation.

You could use the host header to calculate the the required domain and make it more secure by validating that only domains/subdomains under your control are used. Then append to this some configurable value.

Of course you would be at risk that your implementation was insecure though... so be careful.

It could end up looking like:

<bean id="serviceProperties" class="my.ServiceProperties">
    <property name="serviceRelativeUrl" value="/my_cas_callback" />
    <property name="validDomainPattern" value="*.mydomain.com" />
</bean>
Bert answered 9/1, 2011 at 19:28 Comment(3)
As per the linked document I am using Spring Security 3 which has all of those methods tagged as final (static.springsource.org/spring-security/site/docs/3.0.x/apidocs/…)Grani
So then as the issue states you could just subclass the CasAuthenticationProvider and CasEntryPoint and provide you own version of Service properties. I've updated the answer to make it more explicitBert
I think you're probably correct. I haven't had a chance to try this out yet but when I do, and unless a better answer comes around, this is looking like it's going to be the best answer.Grani
I
2

use maven, add a property placeholder, and configure it in your build process

Inquiline answered 29/3, 2011 at 21:28 Comment(0)
F
0

I tried to subclass CasAuthenticationProvider as Pablojim suggest, but solution is very easier! with Spring Expression Language (SPEL) you can obtain the url dinamically.

Example: <property name="service" value="https://#{T(java.net.InetAddress).getLocalHost().getHostName()}:${application.port}${cas.service}/login/cascheck"/>

Frogman answered 6/9, 2013 at 9:38 Comment(1)
Just to clarify for anyone getting excited by this answer, the hostName this retrieves will be the actual server's name, not the hostname from the request. If you have different versions of the app running on different physical servers, this can be idealInquiline
C
-1

I have not tried this myself, but it seems Spring Security has a solution to this with the SavedRequestAwareAuthenticationSuccessHandler shown in the update of Bob's blog.

Conception answered 26/6, 2012 at 12:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.