Skip to content

Added support for the CAS gateway feature. #9881

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright 2013-2013 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.authentication;

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

/**
* Exception used to indicate the {@link CasAuthenticationEntryPoint} to make a CAS gateway authentication request.
*
* @author Michael Remond
*
*/
public class TriggerCasGatewayException extends AuthenticationException {


private static final long serialVersionUID = 4864856111227081697L;

//~ Constructors ===================================================================================================

/**
* Constructs an {@code InitiateCasGatewayAuthenticationException} with the specified message and no root cause.
*
* @param msg the detail message
*/
public TriggerCasGatewayException(String msg) {
super(msg);
}

/**
* Constructs an {@code InitiateCasGatewayAuthenticationException} with the specified message and root cause.
*
* @param msg the detail message
* @param t the root cause
*/
public TriggerCasGatewayException(String msg, Throwable t) {
super(msg, t);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.authentication.TriggerCasGatewayException;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Among other tangles, this introduces a tangle between between org.springframework.security.cas.authentication and org.springframework.security.cas.web. In general, authentication package should not rely on web.

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.util.Assert;
Expand All @@ -39,6 +40,9 @@
* property. The <code>service</code> is a HTTP URL belonging to the current application.
* The <code>service</code> URL is monitored by the {@link CasAuthenticationFilter}, which
* will validate the CAS login was successful.
* <p>
* If the raised exception is {@link TriggerCasGatewayException}, the parameter <code>gateway</code> is set
* to true on the redirect url.
*
* @author Ben Alex
* @author Scott Battaglia
Expand Down Expand Up @@ -71,7 +75,7 @@ public void afterPropertiesSet() {
public final void commence(final HttpServletRequest servletRequest, HttpServletResponse response,
AuthenticationException authenticationException) throws IOException {
String urlEncodedService = createServiceUrl(servletRequest, response);
String redirectUrl = createRedirectUrl(urlEncodedService);
String redirectUrl = createRedirectUrl(servletRequest, response, authenticationException, urlEncodedService);
preCommence(servletRequest, response);
response.sendRedirect(redirectUrl);
}
Expand All @@ -94,11 +98,26 @@ protected String createServiceUrl(HttpServletRequest request, HttpServletRespons
* @param serviceUrl the service url that should be included.
* @return the redirect url. CANNOT be NULL.
*/
@Deprecated
protected String createRedirectUrl(String serviceUrl) {
return CommonUtils.constructRedirectUrl(this.loginUrl, this.serviceProperties.getServiceParameter(), serviceUrl,
this.serviceProperties.isSendRenew(), false);
return createRedirectUrl(null, null, null, serviceUrl);
}

/**
* Constructs the Url for Redirection to the CAS server. Default implementation relies on the CAS client to do the bulk of the work.
* Parameter gateway is infered from authenticationException class.
*
* @param request the HttpServletRequest
* @param response the HttpServletResponse
* @param authenticationException the authentication Exception that triggers CasAuthenticationEntryPoint
* @param serviceUrl the service url that should be included.
* @return the redirect url. CANNOT be NULL.
*/
protected String createRedirectUrl(HttpServletRequest request, HttpServletResponse response, final AuthenticationException authenticationException, final String serviceUrl) {
boolean gateway = authenticationException instanceof TriggerCasGatewayException;
return CommonUtils.constructRedirectUrl(this.loginUrl, this.serviceProperties.getServiceParameter(), serviceUrl, this.serviceProperties.isSendRenew(), gateway);
}

/**
* Template method for you to do your own pre-processing before the redirect occurs.
* @param request the HttpServletRequest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import org.jasig.cas.client.proxy.ProxyGrantingTicketStorage;
import org.jasig.cas.client.util.CommonUtils;
import org.jasig.cas.client.validation.TicketValidator;

import org.springframework.core.log.LogMessage;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.AuthenticationDetailsSource;
Expand All @@ -38,12 +37,18 @@
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
* Processes a CAS service ticket, obtains proxy granting tickets, and processes proxy
Expand Down Expand Up @@ -204,6 +209,10 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil

private AuthenticationFailureHandler proxyFailureHandler = new SimpleUrlAuthenticationFailureHandler();

private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

private RequestCache requestCache = new HttpSessionRequestCache();

public CasAuthenticationFilter() {
super("/login/cas");
setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler());
Expand All @@ -226,6 +235,25 @@ protected final void successfulAuthentication(HttpServletRequest request, HttpSe
chain.doFilter(request, response);
}

/**
* We override this method because we don't want to clear the rememberMe in case of an unsuccessful CAS gateway request.
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {

// if the request had a non empty artifact, we really encounter an unsuccessful authentication
// else it is probably an CAS gateway request with no SSO session, we redirect to the saved url
if (StringUtils.hasText(obtainArtifact(request))) {
super.unsuccessfulAuthentication(request, response, failed);
} else {
SavedRequest savedRequest = requestCache.getRequest(request, response);
if (savedRequest != null) {
redirectStrategy.sendRedirect(request, response, savedRequest.getRedirectUrl());
}
}
}

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException {
Expand Down Expand Up @@ -302,6 +330,16 @@ public final void setServiceProperties(final ServiceProperties serviceProperties
this.authenticateAllArtifacts = serviceProperties.isAuthenticateAllArtifacts();
}

public final void setRedirectStrategy(RedirectStrategy redirectStrategy) {
Assert.notNull(redirectStrategy, "redirectStrategy cannot be null");
this.redirectStrategy = redirectStrategy;
}

public final void setRequestCache(RequestCache requestCache) {
Assert.notNull(requestCache, "requestCache cannot be null");
this.requestCache = requestCache;
}

/**
* Indicates if the request is elgible to process a service ticket. This method exists
* for readability.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package org.springframework.security.cas.web;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;

import org.jasig.cas.client.authentication.DefaultGatewayResolverImpl;
import org.jasig.cas.client.authentication.GatewayResolver;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.authentication.CasAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
* Default RequestMatcher implementation for the {@link TriggerCasGatewayFilter}.
*
* This RequestMatcher returns <code>true</code> iff :
* <ul>
* <li>
* User is not already authenticated (see {@link #isAuthenticated})</li>
* <li>
* The request was not previously gatewayed</li>
* <li>
* The request matches additional criteria (see {@link #performGatewayAuthentication})</li>
* </ul>
*
* Implementors can override this class to customize the authentication check and the gateway criteria.
* <p>
* The request is marked as "gatewayed" using the configured {@link GatewayResolver} to avoid infinite loop.
*
* @author Michael Remond
*
*/
public class CasCookieGatewayRequestMatcher implements RequestMatcher {

// ~ Instance fields
// ================================================================================================

private ServiceProperties serviceProperties;

private String cookieName;

private GatewayResolver gatewayStorage = new DefaultGatewayResolverImpl();

// ~ Constructors
// ===================================================================================================

public CasCookieGatewayRequestMatcher(ServiceProperties serviceProperties, final String cookieName) {
Assert.notNull(serviceProperties, "serviceProperties cannot be null");
this.serviceProperties = serviceProperties;
this.cookieName = cookieName;
}

public final boolean matches(HttpServletRequest request) {

// Test if we are already authenticated
if (isAuthenticated(request)) {
return false;
}

// Test if the request was already gatewayed to avoid infinite loop
final boolean wasGatewayed = this.gatewayStorage.hasGatewayedAlready(request, serviceProperties.getService());

if (wasGatewayed) {
return false;
}

// If request matches gateway criteria, we mark the request as gatewayed and return true to trigger a CAS
// gateway authentication
if (performGatewayAuthentication(request)) {
gatewayStorage.storeGatewayInformation(request, serviceProperties.getService());
return true;
} else {
return false;
}
}

/**
* Test if the user is authenticated in Spring Security. Default implementation test if the user is CAS
* authenticated.
*
* @param request
* @return true if the user is authenticated
*/
protected boolean isAuthenticated(HttpServletRequest request) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return authentication instanceof CasAuthenticationToken;
}

/**
* Method that determines if the current request triggers a CAS gateway authentication.
* This implementation returns <code>true</code> only if a {@link Cookie} with the configured
* name is present at the request
*
* @param request
* @return true if the request must trigger a CAS gateway authentication
*/
protected boolean performGatewayAuthentication(HttpServletRequest request) {
if (!StringUtils.hasText(this.cookieName)) {
return true;
}

Cookie[] cookies = request.getCookies();
if (cookies == null || cookies.length == 0) {
return false;
}

for (Cookie cookie: cookies) {
// Check the cookie name. If it matches the configured cookie name, return true
if (this.cookieName.equalsIgnoreCase(cookie.getName())) {
return true;
}
}
return false;
}

public void setGatewayStorage(GatewayResolver gatewayStorage) {
Assert.notNull(gatewayStorage, "gatewayStorage cannot be null");
this.gatewayStorage = gatewayStorage;
}

public String getCookieName() {
return cookieName;
}

public void setCookieName(String cookieName) {
this.cookieName = cookieName;
}
}
Loading