SEC-965: Added support for CAS proxy ticket authentication on any URL

This commit is contained in:
Rob Winch 2011-04-03 18:54:02 -05:00
parent 373d07ce46
commit a76a947b12
13 changed files with 865 additions and 26 deletions

View File

@ -38,6 +38,8 @@ public class ServiceProperties implements InitializingBean {
private String service;
private boolean authenticateAllArtifacts;
private boolean sendRenew = false;
private String artifactParameter = DEFAULT_CAS_ARTIFACT_PARAMETER;
@ -47,7 +49,9 @@ public class ServiceProperties implements InitializingBean {
//~ Methods ========================================================================================================
public void afterPropertiesSet() throws Exception {
Assert.hasLength(this.service, "service must be specified.");
if(!authenticateAllArtifacts) {
Assert.hasLength(this.service, "service must be specified unless authenticateAllArtifacts is true.");
}
Assert.hasLength(this.artifactParameter, "artifactParameter cannot be empty.");
Assert.hasLength(this.serviceParameter, "serviceParameter cannot be empty.");
}
@ -115,4 +119,19 @@ public class ServiceProperties implements InitializingBean {
public final void setServiceParameter(final String serviceParameter) {
this.serviceParameter = serviceParameter;
}
public final boolean isAuthenticateAllArtifacts() {
return authenticateAllArtifacts;
}
/**
* If true, then any non-null artifact (ticket) should be authenticated.
* Additionally, the service will be determined dynamically in order to
* ensure the service matches the expected value for this artifact.
*
* @param authenticateAllArtifacts
*/
public final void setAuthenticateAllArtifacts(final boolean authenticateAllArtifacts) {
this.authenticateAllArtifacts = authenticateAllArtifacts;
}
}

View File

@ -15,6 +15,8 @@
package org.springframework.security.cas.authentication;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jasig.cas.client.validation.Assertion;
import org.jasig.cas.client.validation.TicketValidationException;
import org.jasig.cas.client.validation.TicketValidator;
@ -28,6 +30,7 @@ import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.web.CasAuthenticationFilter;
import org.springframework.security.cas.web.authentication.ServiceAuthenticationDetails;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.SpringSecurityMessageSource;
@ -50,6 +53,9 @@ import org.springframework.util.Assert;
* @author Scott Battaglia
*/
public class CasAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
//~ Static fields/initializers =====================================================================================
private static final Log logger = LogFactory.getLog(CasAuthenticationProvider.class);
//~ Instance fields ================================================================================================
@ -72,7 +78,6 @@ public class CasAuthenticationProvider implements AuthenticationProvider, Initia
Assert.notNull(this.statelessTicketCache, "A statelessTicketCache must be set");
Assert.hasText(this.key, "A Key is required so CasAuthenticationProvider can identify tokens it previously authenticated");
Assert.notNull(this.messages, "A message source must be set");
Assert.notNull(this.serviceProperties, "serviceProperties is a required field.");
}
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
@ -132,7 +137,7 @@ public class CasAuthenticationProvider implements AuthenticationProvider, Initia
private CasAuthenticationToken authenticateNow(final Authentication authentication) throws AuthenticationException {
try {
final Assertion assertion = this.ticketValidator.validate(authentication.getCredentials().toString(), serviceProperties.getService());
final Assertion assertion = this.ticketValidator.validate(authentication.getCredentials().toString(), getServiceUrl(authentication));
final UserDetails userDetails = loadUserByAssertion(assertion);
userDetailsChecker.check(userDetails);
return new CasAuthenticationToken(this.key, userDetails, authentication.getCredentials(),
@ -142,6 +147,32 @@ public class CasAuthenticationProvider implements AuthenticationProvider, Initia
}
}
/**
* Gets the serviceUrl. If the {@link Authentication#getDetails()} is an
* instance of {@link ServiceAuthenticationDetails}, then
* {@link ServiceAuthenticationDetails#getServiceUrl()} is used. Otherwise,
* the {@link ServiceProperties#getService()} is used.
*
* @param authentication
* @return
*/
private String getServiceUrl(Authentication authentication) {
String serviceUrl;
if(authentication.getDetails() instanceof ServiceAuthenticationDetails) {
serviceUrl = ((ServiceAuthenticationDetails)authentication.getDetails()).getServiceUrl();
}else if(serviceProperties == null){
throw new IllegalStateException("serviceProperties cannot be null unless Authentication.getDetails() implements ServiceAuthenticationDetails.");
}else if(serviceProperties.getService() == null){
throw new IllegalStateException("serviceProperties.getService() cannot be null unless Authentication.getDetails() implements ServiceAuthenticationDetails.");
}else {
serviceUrl = serviceProperties.getService();
}
if(logger.isDebugEnabled()) {
logger.debug("serviceUrl = "+serviceUrl);
}
return serviceUrl;
}
/**
* Template method for retrieving the UserDetails based on the assertion. Default is to call configured userDetailsService and pass the username. Deployers
* can override this method and retrieve the user based on any criteria they desire.

View File

@ -65,6 +65,7 @@ public class CasAuthenticationEntryPoint implements AuthenticationEntryPoint, In
public void afterPropertiesSet() throws Exception {
Assert.hasLength(this.loginUrl, "loginUrl must be specified");
Assert.notNull(this.serviceProperties, "serviceProperties must be specified");
Assert.notNull(this.serviceProperties.getService(),"serviceProperties.getService() cannot be null.");
}
public final void commence(final HttpServletRequest servletRequest, final HttpServletResponse response,

View File

@ -17,41 +17,139 @@ package org.springframework.security.cas.web;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.jasig.cas.client.proxy.ProxyGrantingTicketStorage;
import org.jasig.cas.client.util.CommonUtils;
import org.jasig.cas.client.validation.TicketValidator;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.web.authentication.ServiceAuthenticationDetails;
import org.springframework.security.cas.web.authentication.ServiceAuthenticationDetailsSource;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.util.Assert;
/**
* Processes a CAS service ticket.
* Processes a CAS service ticket, obtains proxy granting tickets, and processes proxy tickets.
* <h2>Service Tickets</h2>
* <p>
* A service ticket consists of an opaque ticket string. It arrives at this filter by the user's browser successfully
* authenticating using CAS, and then receiving a HTTP redirect to a <code>service</code>. The opaque ticket string is
* presented in the <code>ticket</code> request parameter. This filter monitors the <code>service</code> URL so it can
* receive the service ticket and process it. The CAS server knows which <code>service</code> URL to use via the
* {@link ServiceProperties#getService()} method.
* presented in the <code>ticket</code> request parameter.
* <p>
* This filter monitors the <code>service</code> URL so it can
* receive the service ticket and process it. By default this filter processes the URL <tt>/j_spring_cas_security_check</tt>.
* When processing this URL, the value of {@link ServiceProperties#getService()} is used as the <tt>service</tt> when validating
* the <code>ticket</code>. This means that it is important that {@link ServiceProperties#getService()} specifies the same value
* as the <tt>filterProcessesUrl</tt>.
* <p>
* Processing the service ticket involves creating a <code>UsernamePasswordAuthenticationToken</code> which
* uses {@link #CAS_STATEFUL_IDENTIFIER} for the <code>principal</code> and the opaque ticket string as the
* <code>credentials</code>.
* <h2>Obtaining Proxy Granting Tickets</h2>
* <p>
* If specified, the filter can also monitor the <code>proxyReceptorUrl</code>. The filter will respond to requests matching
* this url so that the CAS Server can provide a PGT to the filter. Note that in addition to the <code>proxyReceptorUrl</code> a non-null
* <code>proxyGrantingTicketStorage</code> must be provided in order for the filter to respond to proxy receptor requests. By configuring
* a shared {@link ProxyGrantingTicketStorage} between the {@link TicketValidator} and the CasAuthenticationFilter one can have the
* CasAuthenticationFilter handle the proxying requirements for CAS.
* <h2>Proxy Tickets</h2>
* <p>
* The filter can process tickets present on any url. This is useful when wanting to process proxy tickets. In order for proxy
* tickets to get processed {@link ServiceProperties#isAuthenticateAllArtifacts()} must return <code>true</code>. Additionally,
* if the request is already authenticated, authentication will <b>not</b> occur. Last, {@link AuthenticationDetailsSource#buildDetails(Object)}
* must return a {@link ServiceAuthenticationDetails}. This can be accomplished using the {@link ServiceAuthenticationDetailsSource}.
* In this case {@link ServiceAuthenticationDetails#getServiceUrl()} will be used for the service url.
* <p>
* Processing the proxy ticket involves creating a <code>UsernamePasswordAuthenticationToken</code> which
* uses {@link #CAS_STATELESS_IDENTIFIER} for the <code>principal</code> and the opaque ticket string as the
* <code>credentials</code>. When a proxy ticket is successfully authenticated, the FilterChain continues and the
* <code>authenticationSuccessHandler</code> is not used.
* <h2>Notes about the <code>AuthenticationManager</code></h2>
* <p>
* The configured <code>AuthenticationManager</code> is expected to provide a provider that can recognise
* <code>UsernamePasswordAuthenticationToken</code>s containing this special <code>principal</code> name, and process
* them accordingly by validation with the CAS server.
* them accordingly by validation with the CAS server. Additionally, it should be capable of using the result of
* {@link ServiceAuthenticationDetails#getServiceUrl()} as the service when validating the ticket.
* <h2>Example Configuration</h2>
* <p>
* By configuring a shared {@link ProxyGrantingTicketStorage} between the {@link TicketValidator} and the
* CasAuthenticationFilter one can have the CasAuthenticationFilter handle the proxying requirements for CAS. In addition, the
* URI endpoint for the proxying would also need to be configured (i.e. the part after protocol, hostname, and port).
* <p>
* By default this filter processes the URL <tt>/j_spring_cas_security_check</tt>.
* An example configuration that supports service tickets, obtaining proxy granting tickets, and proxy tickets is
* illustrated below:
*
* <pre>
* &lt;b:bean id=&quot;serviceProperties&quot;
* class=&quot;org.springframework.security.cas.ServiceProperties&quot;
* p:service=&quot;https://service.example.com/cas-sample/j_spring_cas_security_check&quot;
* p:authenticateAllArtifacts=&quot;true&quot;/&gt;
* &lt;b:bean id=&quot;casEntryPoint&quot;
* class=&quot;org.springframework.security.cas.web.CasAuthenticationEntryPoint&quot;
* p:serviceProperties-ref=&quot;serviceProperties&quot; p:loginUrl=&quot;https://login.example.org/cas/login&quot; /&gt;
* &lt;b:bean id=&quot;casFilter&quot;
* class=&quot;org.springframework.security.cas.web.CasAuthenticationFilter&quot;
* p:authenticationManager-ref=&quot;authManager&quot;
* p:serviceProperties-ref=&quot;serviceProperties&quot;
* p:proxyGrantingTicketStorage-ref=&quot;pgtStorage&quot;
* p:proxyReceptorUrl=&quot;/j_spring_cas_security_proxyreceptor&quot;&gt;
* &lt;b:property name=&quot;authenticationDetailsSource&quot;&gt;
* &lt;b:bean class=&quot;org.springframework.security.cas.web.authentication.ServiceAuthenticationDetailsSource&quot;/&gt;
* &lt;/b:property&gt;
* &lt;b:property name=&quot;authenticationFailureHandler&quot;&gt;
* &lt;b:bean class=&quot;org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler&quot;
* p:defaultFailureUrl=&quot;/casfailed.jsp&quot;/&gt;
* &lt;/b:property&gt;
* &lt;/b:bean&gt;
* &lt;!--
* NOTE: In a real application you should not use an in memory implementation. You will also want
* to ensure to clean up expired tickets by calling ProxyGrantingTicketStorage.cleanup()
* --&gt;
* &lt;b:bean id=&quot;pgtStorage&quot; class=&quot;org.jasig.cas.client.proxy.ProxyGrantingTicketStorageImpl&quot;/&gt;
* &lt;b:bean id=&quot;casAuthProvider&quot; class=&quot;org.springframework.security.cas.authentication.CasAuthenticationProvider&quot;
* p:serviceProperties-ref=&quot;serviceProperties&quot;
* p:key=&quot;casAuthProviderKey&quot;&gt;
* &lt;b:property name=&quot;authenticationUserDetailsService&quot;&gt;
* &lt;b:bean
* class=&quot;org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper&quot;&gt;
* &lt;b:constructor-arg ref=&quot;userService&quot; /&gt;
* &lt;/b:bean&gt;
* &lt;/b:property&gt;
* &lt;b:property name=&quot;ticketValidator&quot;&gt;
* &lt;b:bean
* class=&quot;org.jasig.cas.client.validation.Cas20ProxyTicketValidator&quot;
* p:acceptAnyProxy=&quot;true&quot;
* p:proxyCallbackUrl=&quot;https://service.example.com/cas-sample/j_spring_cas_security_proxyreceptor&quot;
* p:proxyGrantingTicketStorage-ref=&quot;pgtStorage&quot;&gt;
* &lt;b:constructor-arg value=&quot;https://login.example.org/cas&quot; /&gt;
* &lt;/b:bean&gt;
* &lt;/b:property&gt;
* &lt;b:property name=&quot;statelessTicketCache&quot;&gt;
* &lt;b:bean class=&quot;org.springframework.security.cas.authentication.EhCacheBasedTicketCache&quot;&gt;
* &lt;b:property name=&quot;cache&quot;&gt;
* &lt;b:bean class=&quot;net.sf.ehcache.Cache&quot;
* init-method=&quot;initialise&quot;
* destroy-method=&quot;dispose&quot;&gt;
* &lt;b:constructor-arg value=&quot;casTickets&quot;/&gt;
* &lt;b:constructor-arg value=&quot;50&quot;/&gt;
* &lt;b:constructor-arg value=&quot;true&quot;/&gt;
* &lt;b:constructor-arg value=&quot;false&quot;/&gt;
* &lt;b:constructor-arg value=&quot;3600&quot;/&gt;
* &lt;b:constructor-arg value=&quot;900&quot;/&gt;
* &lt;/b:bean&gt;
* &lt;/b:property&gt;
* &lt;/b:bean&gt;
* &lt;/b:property&gt;
* &lt;/b:bean&gt;
* </pre>
*
* @author Ben Alex
* @author Rob Winch
@ -81,26 +179,59 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil
private String artifactParameter = ServiceProperties.DEFAULT_CAS_ARTIFACT_PARAMETER;
private boolean authenticateAllArtifacts;
private AuthenticationFailureHandler proxyFailureHandler = new SimpleUrlAuthenticationFailureHandler();
//~ Constructors ===================================================================================================
public CasAuthenticationFilter() {
super("/j_spring_cas_security_check");
setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler());
}
//~ Methods ========================================================================================================
@Override
protected final void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
boolean continueFilterChain = proxyTicketRequest(serviceTicketRequest(request, response),request);
if(!continueFilterChain) {
super.successfulAuthentication(request, response, chain, authResult);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult);
}
SecurityContextHolder.getContext().setAuthentication(authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
chain.doFilter(request, response);
}
@Override
public Authentication attemptAuthentication(final HttpServletRequest request, final HttpServletResponse response)
throws AuthenticationException, IOException {
// if the request is a proxy request process it and return null to indicate the request has been processed
if(isProxyRequest(request)) {
if(proxyReceptorRequest(request)) {
logger.debug("Responding to proxy receptor request");
CommonUtils.readAndRespondToProxyReceptorRequest(request, response, this.proxyGrantingTicketStorage);
return null;
}
final String username = CAS_STATEFUL_IDENTIFIER;
String password = request.getParameter(this.artifactParameter);
final boolean serviceTicketRequest = serviceTicketRequest(request, response);
final String username = serviceTicketRequest ? CAS_STATEFUL_IDENTIFIER : CAS_STATELESS_IDENTIFIER;
String password = obtainArtifact(request);
if (password == null) {
logger.debug("Failed to obtain an artifact (cas ticket)");
password = "";
}
@ -111,11 +242,47 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil
return this.getAuthenticationManager().authenticate(authRequest);
}
/**
* If present, gets the artifact (CAS ticket) from the {@link HttpServletRequest}.
* @param request
* @return if present the artifact from the {@link HttpServletRequest}, else null
*/
protected String obtainArtifact(HttpServletRequest request) {
return request.getParameter(artifactParameter);
}
/**
* Overridden to provide proxying capabilities.
*/
@Override
protected boolean requiresAuthentication(final HttpServletRequest request, final HttpServletResponse response) {
return isProxyRequest(request) || super.requiresAuthentication(request, response);
final boolean serviceTicketRequest = serviceTicketRequest(request, response);
final boolean result = serviceTicketRequest || proxyReceptorRequest(request) || (proxyTicketRequest(serviceTicketRequest, request));
if(logger.isDebugEnabled()) {
logger.debug("requiresAuthentication = "+result);
}
return result;
}
/**
* Sets the {@link AuthenticationFailureHandler} for proxy requests.
* @param proxyFailureHandler
*/
public final void setProxyAuthenticationFailureHandler(
AuthenticationFailureHandler proxyFailureHandler) {
Assert.notNull(proxyFailureHandler,"proxyFailureHandler cannot be null");
this.proxyFailureHandler = proxyFailureHandler;
}
/**
* Wraps the {@link AuthenticationFailureHandler} to distinguish between
* handling proxy ticket authentication failures and service ticket
* failures.
*/
@Override
public final void setAuthenticationFailureHandler(
AuthenticationFailureHandler failureHandler) {
super.setAuthenticationFailureHandler(new CasAuthenticationFailureHandler(failureHandler));
}
public final void setProxyReceptorUrl(final String proxyReceptorUrl) {
@ -129,15 +296,97 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil
public final void setServiceProperties(final ServiceProperties serviceProperties) {
this.artifactParameter = serviceProperties.getArtifactParameter();
this.authenticateAllArtifacts = serviceProperties.isAuthenticateAllArtifacts();
}
/**
* Indicates if the request is eligible to be processed as a proxy request.
* Indicates if the request is elgible to process a service ticket. This method exists for readability.
* @param request
* @param response
* @return
*/
private boolean serviceTicketRequest(final HttpServletRequest request, final HttpServletResponse response) {
boolean result = super.requiresAuthentication(request, response);
if(logger.isDebugEnabled()) {
logger.debug("serviceTicketRequest = "+result);
}
return result;
}
/**
* Indicates if the request is elgible to process a proxy ticket.
* @param request
* @return
*/
private boolean isProxyRequest(final HttpServletRequest request) {
final String requestUri = request.getRequestURI();
return this.proxyGrantingTicketStorage != null && !CommonUtils.isEmpty(this.proxyReceptorUrl) && requestUri.endsWith(this.proxyReceptorUrl);
private boolean proxyTicketRequest(final boolean serviceTicketRequest, final HttpServletRequest request) {
if(serviceTicketRequest) {
return false;
}
final boolean result = authenticateAllArtifacts && obtainArtifact(request) != null && !authenticated();
if(logger.isDebugEnabled()) {
logger.debug("proxyTicketRequest = "+result);
}
return result;
}
}
/**
* Determines if a user is already authenticated.
* @return
*/
private boolean authenticated() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return authentication != null && authentication.isAuthenticated() && !(authentication instanceof AnonymousAuthenticationToken);
}
/**
* Indicates if the request is elgible to be processed as the proxy receptor.
* @param request
* @return
*/
private boolean proxyReceptorRequest(final HttpServletRequest request) {
final String requestUri = request.getRequestURI();
final boolean result = proxyReceptorConfigured() && requestUri.endsWith(this.proxyReceptorUrl);
if(logger.isDebugEnabled()) {
logger.debug("proxyReceptorRequest = "+result);
}
return result;
}
/**
* Determines if the {@link CasAuthenticationFilter} is configured to handle the proxy receptor requests.
*
* @return
*/
private boolean proxyReceptorConfigured() {
final boolean result = this.proxyGrantingTicketStorage != null && !CommonUtils.isEmpty(this.proxyReceptorUrl);
if(logger.isDebugEnabled()) {
logger.debug("proxyReceptorConfigured = "+result);
}
return result;
}
/**
* A wrapper for the AuthenticationFailureHandler that will flex the {@link AuthenticationFailureHandler} that is used. The value
* {@link CasAuthenticationFilter#setProxyAuthenticationFailureHandler(AuthenticationFailureHandler) will be used for proxy requests
* that fail. The value {@link CasAuthenticationFilter#setAuthenticationFailureHandler(AuthenticationFailureHandler)} will be used for
* service tickets that fail.
*
* @author Rob Winch
*/
private class CasAuthenticationFailureHandler implements AuthenticationFailureHandler {
private final AuthenticationFailureHandler serviceTicketFailureHandler;
public CasAuthenticationFailureHandler(AuthenticationFailureHandler failureHandler) {
Assert.notNull(failureHandler,"failureHandler");
this.serviceTicketFailureHandler = failureHandler;
}
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception) throws IOException,
ServletException {
if(serviceTicketRequest(request, response)) {
serviceTicketFailureHandler.onAuthenticationFailure(request, response, exception);
}else {
proxyFailureHandler.onAuthenticationFailure(request, response, exception);
}
}
}
}

View File

@ -0,0 +1,131 @@
/*
* Copyright 2011 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.cas.web.authentication;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.util.Assert;
/**
* A default implementation of {@link ServiceAuthenticationDetails} that figures
* out the value for {@link #getServiceUrl()} by inspecting the current
* {@link HttpServletRequest} and using the current URL minus the artifact and
* the corresponding value.
*
* @author Rob Winch
*/
final class DefaultServiceAuthenticationDetails extends WebAuthenticationDetails implements ServiceAuthenticationDetails {
private static final long serialVersionUID = 6192409090610517700L;
//~ Instance fields ================================================================================================
private final String serviceUrl;
//~ Constructors ===================================================================================================
/**
* Creates a new instance
* @param request
* the current {@link HttpServletRequest} to obtain the
* {@link #getServiceUrl()} from.
* @param artifactPattern
* the {@link Pattern} that will be used to clean up the query
* string from containing the artifact name and value. This can
* be created using {@link #createArtifactPattern(String)}.
*/
DefaultServiceAuthenticationDetails(HttpServletRequest request, Pattern artifactPattern) {
super(request);
final String query = getQueryString(request,artifactPattern);
this.serviceUrl = UrlUtils.buildFullRequestUrl(request.getScheme(),
request.getServerName(), request.getServerPort(),
request.getRequestURI(), query);
}
//~ Methods ========================================================================================================
/**
* Returns the current URL minus the artifact parameter and its value, if present.
* @see org.springframework.security.cas.web.authentication.ServiceAuthenticationDetails#getServiceUrl()
*/
public String getServiceUrl() {
return serviceUrl;
}
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result
+ serviceUrl.hashCode();
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!super.equals(obj) || !(obj instanceof DefaultServiceAuthenticationDetails)) {
return false;
}
ServiceAuthenticationDetails that = (ServiceAuthenticationDetails) obj;
return serviceUrl.equals(that.getServiceUrl());
}
@Override
public String toString() {
StringBuilder result = new StringBuilder();
result.append(super.toString());
result.append("ServiceUrl: ");
result.append(serviceUrl);
return result.toString();
}
/**
* If present, removes the artifactParameterName and the corresponding value from the query String.
* @param request
* @return the query String minus the artifactParameterName and the corresponding value.
*/
private String getQueryString(final HttpServletRequest request, final Pattern artifactPattern) {
final String query = request.getQueryString();
if(query == null) {
return null;
}
final String result = artifactPattern.matcher(query).replaceFirst("");
if(result.length() == 0) {
return null;
}
// strip off the trailing & only if the artifact was the first query param
return result.startsWith("&") ? result.substring(1) : result;
}
/**
* Creates a {@link Pattern} that can be passed into the constructor. This
* allows the {@link Pattern} to be reused for every instance of
* {@link DefaultServiceAuthenticationDetails}.
*
* @param artifactParameterName
* @return
*/
static Pattern createArtifactPattern(String artifactParameterName) {
Assert.hasLength(artifactParameterName);
return Pattern.compile("&?"+Pattern.quote(artifactParameterName)+"=[^&]*");
}
}

View File

@ -0,0 +1,43 @@
/*
* Copyright 2011 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.cas.web.authentication;
import java.io.Serializable;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.authentication.CasAuthenticationProvider;
import org.springframework.security.core.Authentication;
/**
* In order for the {@link CasAuthenticationProvider} to provide the correct
* service url to authenticate the ticket, the returned value of
* {@link Authentication#getDetails()} should implement this interface when
* tickets can be sent to any URL rather than only
* {@link ServiceProperties#getService()}.
*
* @author Rob Winch
*
* @see ServiceAuthenticationDetailsSource
*/
public interface ServiceAuthenticationDetails extends Serializable {
/**
* Gets the absolute service url (i.e. https://example.com/service/).
*
* @return the service url. Cannot be <code>null</code>.
*/
String getServiceUrl();
}

View File

@ -0,0 +1,71 @@
/*
* Copyright 2011 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.cas.web.authentication;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.cas.ServiceProperties;
/**
* The {@code AuthenticationDetailsSource} that is set on the
* {@code CasAuthenticationFilter} should return a value that implements
* {@code ServiceAuthenticationDetails} if the application needs to authenticate
* dynamic service urls. The
* {@code ServiceAuthenticationDetailsSource#buildDetails(HttpServletRequest)}
* creates a default {@code ServiceAuthenticationDetails}.
*
* @author Rob Winch
*/
public class ServiceAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest,
ServiceAuthenticationDetails> {
//~ Instance fields ================================================================================================
private final Pattern artifactPattern;
//~ Constructors ===================================================================================================
/**
* Creates an implementation that uses the default CAS artifactParameterName.
*/
public ServiceAuthenticationDetailsSource() {
this(ServiceProperties.DEFAULT_CAS_ARTIFACT_PARAMETER);
}
/**
* Creates an implementation that uses the specified artifactParameterName
*
* @param artifactParameterName
* the artifactParameterName that is removed from the current
* URL. The result becomes the service url. Cannot be null and
* cannot be an empty String.
*/
public ServiceAuthenticationDetailsSource(final String artifactParameterName) {
this.artifactPattern = DefaultServiceAuthenticationDetails.createArtifactPattern(artifactParameterName);
}
//~ Methods ========================================================================================================
/**
* @param context the {@code HttpServletRequest} object.
* @return the {@code ServiceAuthenticationDetails} containing information about the current request
*/
public ServiceAuthenticationDetails buildDetails(HttpServletRequest context) {
return new DefaultServiceAuthenticationDetails(context,artifactPattern);
}
}

View File

@ -0,0 +1,5 @@
/**
* Authentication processing mechanisms which respond to the submission of authentication
* credentials using CAS.
*/
package org.springframework.security.cas.web.authentication;

View File

@ -15,7 +15,7 @@
package org.springframework.security.cas.authentication;
import static org.mockito.Mockito.*;
import static org.junit.Assert.*;
import org.jasig.cas.client.validation.Assertion;
@ -23,11 +23,13 @@ import org.jasig.cas.client.validation.AssertionImpl;
import org.jasig.cas.client.validation.TicketValidationException;
import org.jasig.cas.client.validation.TicketValidator;
import org.junit.*;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.web.CasAuthenticationFilter;
import org.springframework.security.cas.web.authentication.ServiceAuthenticationDetails;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
@ -35,6 +37,7 @@ import org.springframework.security.core.userdetails.AuthenticationUserDetailsSe
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import java.util.*;
@ -148,6 +151,87 @@ public class CasAuthenticationProviderTests {
assertEquals("ST-456", newResult.getCredentials());
}
@Test
public void authenticateAllNullService() throws Exception {
String serviceUrl = "https://service/context";
ServiceAuthenticationDetails details = mock(ServiceAuthenticationDetails.class);
when(details.getServiceUrl()).thenReturn(serviceUrl);
TicketValidator validator = mock(TicketValidator.class);
when(validator.validate(any(String.class),any(String.class))).thenReturn(new AssertionImpl("rod"));
ServiceProperties serviceProperties = makeServiceProperties();
serviceProperties.setAuthenticateAllArtifacts(true);
CasAuthenticationProvider cap = new CasAuthenticationProvider();
cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator());
cap.setKey("qwerty");
cap.setTicketValidator(validator);
cap.setServiceProperties(serviceProperties);
cap.afterPropertiesSet();
String ticket = "ST-456";
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER, ticket);
Authentication result = cap.authenticate(token);
}
@Test
public void authenticateAllAuthenticationIsSuccessful() throws Exception {
String serviceUrl = "https://service/context";
ServiceAuthenticationDetails details = mock(ServiceAuthenticationDetails.class);
when(details.getServiceUrl()).thenReturn(serviceUrl);
TicketValidator validator = mock(TicketValidator.class);
when(validator.validate(any(String.class),any(String.class))).thenReturn(new AssertionImpl("rod"));
ServiceProperties serviceProperties = makeServiceProperties();
serviceProperties.setAuthenticateAllArtifacts(true);
CasAuthenticationProvider cap = new CasAuthenticationProvider();
cap.setAuthenticationUserDetailsService(new MockAuthoritiesPopulator());
cap.setKey("qwerty");
cap.setTicketValidator(validator);
cap.setServiceProperties(serviceProperties);
cap.afterPropertiesSet();
String ticket = "ST-456";
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER, ticket);
Authentication result = cap.authenticate(token);
verify(validator).validate(ticket, serviceProperties.getService());
serviceProperties.setAuthenticateAllArtifacts(true);
result = cap.authenticate(token);
verify(validator,times(2)).validate(ticket, serviceProperties.getService());
token.setDetails(details);
result = cap.authenticate(token);
verify(validator).validate(ticket, serviceUrl);
serviceProperties.setAuthenticateAllArtifacts(false);
serviceProperties.setService(null);
cap.setServiceProperties(serviceProperties);
cap.afterPropertiesSet();
result = cap.authenticate(token);
verify(validator,times(2)).validate(ticket, serviceUrl);
token.setDetails(new WebAuthenticationDetails(new MockHttpServletRequest()));
try {
cap.authenticate(token);
fail("Expected Exception");
}catch(IllegalStateException success) {}
cap.setServiceProperties(null);
cap.afterPropertiesSet();
try {
cap.authenticate(token);
fail("Expected Exception");
}catch(IllegalStateException success) {}
}
@Test(expected = BadCredentialsException.class)
public void missingTicketIdIsDetected() throws Exception {
CasAuthenticationProvider cap = new CasAuthenticationProvider();

View File

@ -16,19 +16,34 @@
package org.springframework.security.cas.web;
import static org.junit.Assert.*;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;
import java.lang.reflect.Method;
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.jasig.cas.client.proxy.ProxyGrantingTicketStorage;
import org.junit.After;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.NullRememberMeServices;
import org.springframework.util.ReflectionUtils;
/**
@ -40,6 +55,11 @@ import org.springframework.security.core.AuthenticationException;
public class CasAuthenticationFilterTests {
//~ Methods ========================================================================================================
@After
public void tearDown() {
SecurityContextHolder.clearContext();
}
@Test
public void testGettersSetters() {
CasAuthenticationFilter filter = new CasAuthenticationFilter();
@ -105,6 +125,31 @@ public class CasAuthenticationFilterTests {
assertFalse(filter.requiresAuthentication(request, response));
}
@Test
public void testRequiresAuthenticationAuthAll() {
ServiceProperties properties = new ServiceProperties();
properties.setAuthenticateAllArtifacts(true);
CasAuthenticationFilter filter = new CasAuthenticationFilter();
filter.setServiceProperties(properties);
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
request.setRequestURI(filter.getFilterProcessesUrl());
assertTrue(filter.requiresAuthentication(request, response));
request.setRequestURI("/other");
assertFalse(filter.requiresAuthentication(request, response));
request.setParameter(properties.getArtifactParameter(), "value");
assertTrue(filter.requiresAuthentication(request, response));
SecurityContextHolder.getContext().setAuthentication(new AnonymousAuthenticationToken("key", "principal", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")));
assertTrue(filter.requiresAuthentication(request, response));
SecurityContextHolder.getContext().setAuthentication(new TestingAuthenticationToken("un", "principal", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")));
assertTrue(filter.requiresAuthentication(request, response));
SecurityContextHolder.getContext().setAuthentication(new TestingAuthenticationToken("un", "principal", "ROLE_ANONYMOUS"));
assertFalse(filter.requiresAuthentication(request, response));
}
@Test
public void testAuthenticateProxyUrl() throws Exception {
CasAuthenticationFilter filter = new CasAuthenticationFilter();
@ -117,9 +162,42 @@ public class CasAuthenticationFilterTests {
assertNull(filter.attemptAuthentication(request, response));
}
@Test
public void testDoFilterAuthenticateAll() throws Exception {
AuthenticationSuccessHandler successHandler = mock(AuthenticationSuccessHandler.class);
AuthenticationManager manager = mock(AuthenticationManager.class);
Authentication authentication = new TestingAuthenticationToken("un", "pwd","ROLE_USER");
when(manager.authenticate(any(Authentication.class))).thenReturn(authentication);
ServiceProperties serviceProperties = new ServiceProperties();
serviceProperties.setAuthenticateAllArtifacts(true);
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("ticket", "ST-1-123");
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain chain = mock(FilterChain.class);
CasAuthenticationFilter filter = new CasAuthenticationFilter();
filter.setServiceProperties(serviceProperties);
filter.setAuthenticationSuccessHandler(successHandler);
filter.setProxyGrantingTicketStorage(mock(ProxyGrantingTicketStorage.class));
filter.setAuthenticationManager(manager);
filter.afterPropertiesSet();
filter.doFilter(request,response,chain);
assertFalse("Authentication should not be null",SecurityContextHolder.getContext().getAuthentication() == null);
verify(chain).doFilter(request, response);
verifyZeroInteractions(successHandler);
// validate for when the filterProcessUrl matches
filter.setFilterProcessesUrl(request.getRequestURI());
SecurityContextHolder.clearContext();
filter.doFilter(request,response,chain);
verifyNoMoreInteractions(chain);
verify(successHandler).onAuthenticationSuccess(request, response, authentication);
}
// SEC-1592
@Test
public void testChainNotInvokedForProxy() throws Exception {
public void testChainNotInvokedForProxyReceptor() throws Exception {
CasAuthenticationFilter filter = new CasAuthenticationFilter();
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();

View File

@ -35,6 +35,18 @@ public class ServicePropertiesTests {
sp.afterPropertiesSet();
}
@Test
public void allowNullServiceWhenAuthenticateAllTokens() throws Exception {
ServiceProperties sp = new ServiceProperties();
sp.setAuthenticateAllArtifacts(true);
sp.afterPropertiesSet();
sp.setAuthenticateAllArtifacts(false);
try {
sp.afterPropertiesSet();
fail("Expected Exception");
}catch(IllegalArgumentException success) {}
}
@Test
public void testGettersSetters() throws Exception {
ServiceProperties[] sps = {new ServiceProperties(), new SamlServiceProperties()};

View File

@ -0,0 +1,88 @@
/*
* Copyright 2011 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.cas.web.authentication;
import static org.junit.Assert.assertEquals;
import java.util.regex.Pattern;
import org.junit.Before;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.web.util.UrlUtils;
/**
*
* @author Rob Winch
*/
public class DefaultServiceAuthenticationDetailsTests {
private DefaultServiceAuthenticationDetails details;
private MockHttpServletRequest request;
private Pattern artifactPattern;
@Before
public void setUp() {
request = new MockHttpServletRequest();
request.setScheme("https");
request.setServerName("localhost");
request.setServerPort(8443);
request.setRequestURI("/cas-sample/secure/");
artifactPattern = DefaultServiceAuthenticationDetails.createArtifactPattern(ServiceProperties.DEFAULT_CAS_ARTIFACT_PARAMETER);
}
@Test
public void getServiceUrlNullQuery() throws Exception {
details = new DefaultServiceAuthenticationDetails(request,artifactPattern);
assertEquals(UrlUtils.buildFullRequestUrl(request),details.getServiceUrl());
}
@Test
public void getServiceUrlTicketOnlyParam() {
request.setQueryString("ticket=123");
details = new DefaultServiceAuthenticationDetails(request,artifactPattern);
String serviceUrl = details.getServiceUrl();
request.setQueryString(null);
assertEquals(UrlUtils.buildFullRequestUrl(request),serviceUrl);
}
@Test
public void getServiceUrlTicketFirstMultiParam() {
request.setQueryString("ticket=123&other=value");
details = new DefaultServiceAuthenticationDetails(request,artifactPattern);
String serviceUrl = details.getServiceUrl();
request.setQueryString("other=value");
assertEquals(UrlUtils.buildFullRequestUrl(request),serviceUrl);
}
@Test
public void getServiceUrlTicketLastMultiParam() {
request.setQueryString("other=value&ticket=123");
details = new DefaultServiceAuthenticationDetails(request,artifactPattern);
String serviceUrl = details.getServiceUrl();
request.setQueryString("other=value");
assertEquals(UrlUtils.buildFullRequestUrl(request),serviceUrl);
}
@Test
public void getServiceUrlTicketMiddleMultiParam() {
request.setQueryString("other=value&ticket=123&last=this");
details = new DefaultServiceAuthenticationDetails(request,artifactPattern);
String serviceUrl = details.getServiceUrl();
request.setQueryString("other=value&last=this");
assertEquals(UrlUtils.buildFullRequestUrl(request),serviceUrl);
}
}

View File

@ -215,7 +215,7 @@ public abstract class AbstractAuthenticationProcessingFilter extends GenericFilt
chain.doFilter(request, response);
}
successfulAuthentication(request, response, authResult);
successfulAuthentication(request, response, chain, authResult);
}
/**
@ -280,8 +280,35 @@ public abstract class AbstractAuthenticationProcessingFilter extends GenericFilt
* <li>Delegates additional behaviour to the {@link AuthenticationSuccessHandler}.</li>
* </ol>
*
* Subclasses can override this method to continue the {@link FilterChain} after successful authentication.
* @param request
* @param response
* @param chain
* @param authResult the object returned from the <tt>attemptAuthentication</tt> method.
* @throws IOException
* @throws ServletException
*/
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException{
successfulAuthentication(request, response, authResult);
}
/**
* Default behaviour for successful authentication.
* <ol>
* <li>Sets the successful <tt>Authentication</tt> object on the {@link SecurityContextHolder}</li>
* <li>Invokes the configured {@link SessionAuthenticationStrategy} to handle any session-related behaviour
* (such as creating a new session to protect against session-fixation attacks).</li>
* <li>Informs the configured <tt>RememberMeServices</tt> of the successful login</li>
* <li>Fires an {@link InteractiveAuthenticationSuccessEvent} via the configured
* <tt>ApplicationEventPublisher</tt></li>
* <li>Delegates additional behaviour to the {@link AuthenticationSuccessHandler}.</li>
* </ol>
*
* @param authResult the object returned from the <tt>attemptAuthentication</tt> method.
* @deprecated since 3.1. Use {@link #successfulAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, Authentication)} instead.
*/
@Deprecated
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
Authentication authResult) throws IOException, ServletException {