How to properly configure service URL in Spring CAS service properties

When working with Spring Security + CAS, I continue to delete the small road block with the callback URL that is sent to CAS, i.e. service property. I looked through many examples, such as this and this, but they all use hard-coded URLs (even Spring CAS docs ). A typical snipe 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 hardcode the server name or port, since I want this WAR to be deployed anywhere, and I don’t want my application to bind to a specific DNS record at compile time. Secondly, I don’t understand why Spring cannot automatically detect the application context and the request URL to automatically assemble the URL. The first part of this statement is still standing, but as Raghuram said below with this link , we cannot trust the HTTP host header from the client for security reasons.

Ideally, I would like the service URL to be exactly what the user requested (as long as the request is valid, for example, the mycompany.com subdomain), so it is seamless or, at least, I would like to indicate only some paths relative to mine application context root and 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 this possible or easy or am I missing the obvious?

+8
java spring spring-security cas
source share
5 answers

In Spring 2.6.5 Spring, you can extend org.springframework.security.ui.cas.ServiceProperties

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

You can use the host header to calculate the required domain and make it more secure by confirming that only the domains / subdomains under your control are used. Then add some custom value to this.

Of course, you risk that your implementation was unsafe, though ... so be careful.

It might look like this:

 <bean id="serviceProperties" class="my.ServiceProperties"> <property name="serviceRelativeUrl" value="/my_cas_callback" /> <property name="validDomainPattern" value="*.mydomain.com" /> </bean> 
+4
source share

I know this is a little outdated, but I just had to solve this problem and could not find anything in the new stacks.

We have several environments that use the same CAS service (think dev, qa, uat and local development environments); we have the ability to hit each environment from multiple URLs (through the client side web server through the reverse proxy server and directly to the internal server). This means that specifying a single URL is difficult at best. There may be a way to do this, but be able to use dynamic ServiceProperties.getService() . I will probably add some kind of server suffix check to make sure the URL was not captured at some point.

Here I have made the underlying CAS stream work regardless of the URL used to access the protected resource ...

  • Cancel the CasAuthenticationFilter .
  • Cancel the CasAuthenticationProvider .
  • setAuthenticateAllArtifacts(true) on ServiceProperties .

Here is a long form of my spring bean configuration:

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

Just plain spring bean configuration.

 @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 external configuration options.

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

The keyword above is a call to setAuthenticateAllArtifacts(true) . This will be done by a service validator that uses the AuthenticationDetailsSource implementation, rather than a hard-coded call to ServiceProperties.getService()

 @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 Binding to 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; } 

The dynamicServiceResolver() setting is specified here.

 @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 a service url from the makeDynamicUrlFromRequest() method. This bit is used when checking tickets.

 @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 what you do. I just passed in the ServiceProperties to store the URI of the service for which we are configured. We use HATEAOS on the back and have an implementation, for example:

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

Edit: this is 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(); } 
+5
source share

use maven, add property placeholder and configure it during the build process

+2
source share

I tried a subclass of CasAuthenticationProvider, as Pablojim suggests, but the solution is very simple! with Spring Expression Language (SPEL) you can get the URL dynamically.

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

0
source share

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

-one
source share

Source: https://habr.com/ru/post/650524/


All Articles