spring-projects / spring-security

Spring Security
http://spring.io/projects/spring-security
Apache License 2.0
8.79k stars 5.9k forks source link

SEC-977: CAS gateway feature support #1229

Closed spring-projects-issues closed 3 years ago

spring-projects-issues commented 16 years ago

["Martin Buechler":https://jira.spring.io/secure/ViewProfile.jspa?name=mdmd](Migrated from ["SEC-977":https://jira.spring.io/browse/SEC-977?redirect=false]) said:

Acegi CAS client integration should also allow CAS gateway calls for configurable entry points like home/welcome/landing pages: Reference: http://www.ja-sig.org/products/cas/client/gateway/index.html, and http://thread.gmane.org/gmane.comp.java.jasig.cas.user/32 from 2006!

spring-projects-issues commented 16 years ago

["Rob Winch":https://jira.spring.io/secure/ViewProfile.jspa?name=rwinch] said:

I am also interested in this issue and would be willing to contribute. Can anyone point me to the process for providing a contribution?

Thanks, Rob

spring-projects-issues commented 16 years ago

["Martin Buechler":https://jira.spring.io/secure/ViewProfile.jspa?name=mdmd] said:

great: it works not so great: no single configuration option, no tests

gateway.patch:

securityContext.xml.diff

comments? please review

spring-projects-issues commented 16 years ago

["Luke Taylor":https://jira.spring.io/secure/ViewProfile.jspa?name=luke] said:

Any comments, Scott?

spring-projects-issues commented 12 years ago

Jeremy said:

Looking in the forums and other sites, it seems like there is a big need for this feature. Has there been any progress made for adding gateway support?

spring-projects-issues commented 12 years ago

Jeremy said:

Sorry, I forgot to add that I found this possible solution. Thoughts?

http://prodia.co.uk/blog/doahh/entry/spring_cas_with_unprotected_pages

spring-projects-issues commented 11 years ago

Tim Lenz said:

I also took a stab at a custom filter for gateway authentication, and tried to simplify a lot of the things in the solution posted above. My solution depends on a second, custom implementation of CasAuthenticationEntryPoint in which the "gateway" parameter is always set to true. I did this by overriding the createRedirectUrl() method. The filter itself does three important things

(1) If the user is already signed in or gateway authentication was already attempted, continue with the filter chain, otherwise (2) Save the request in a requestCache so that either the AuthenticationSuccessHandler or AuthenticationFailureHandler will know where to go after receiving the response back from CAS (3) redirect to CAS for gateway authentication

Your AuthenticationFailureHandler then just needs to check whether gateway authentication was attempted, and if so, redirect the user to the URL in the saved request. I think the only disadvantage to this approach is that it requires sessions for every page in your application. However, you might be able to work around this with a cookie implementation.

public class CasUnprotectedPageFilter extends GenericFilterBean
{
    private static final Logger logger = Logger.getLogger(CasUnprotectedPageFilter.class);

    private RequestCache requestCache = new HttpSessionRequestCache();

    //Assumed to be a custom implementation where useGateway is always set to TRUE
    private CasAuthenticationEntryPoint casAuthenticationEntryPoint;

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException 
    {
        if (User.isLoggedIn())
        {   //If we're already logged in, there's no work to do
            chain.doFilter(req,res);
            return;
        }

        HttpServletRequest request = (HttpServletRequest) req;  
        boolean attemptedGatewayAuth = WebUtils.getSessionAttribute(request,"ATTEMPTED_GATEWAY_AUTH") != null;        
        if(!attemptedGatewayAuth)
        {           
            request.getSession().setAttribute("ATTEMPTED_GATEWAY_AUTH",true);

            HttpServletResponse response = (HttpServletResponse) res;

            //Save the request so either the AuthenticationSuccessHandler or AuthenticationFailureHandler will know where to go after receiving the response back from CAS
            requestCache.saveRequest(request, response);

            //Try to authenticate with CAS gateway. Not entirely sure why we're passing in an exception to this method
            if (logger.isDebugEnabled())
                logger.debug("Commencing CAS gateway authentication");

            casAuthenticationEntryPoint.commence(request,response,new AuthenticationCredentialsNotFoundException("Authentication credentials not found"));

            //Skip the rest of the filter chain
            return;
        }

        //Already tried gateway authentication and not logged in, continue with filter chain        
        chain.doFilter(req,res);                            
    }    

    public void setGatewayEnabledCasAuthenticationEntryPoint(
            CasAuthenticationEntryPoint casAuthenticationEntryPoint) {
        this.casAuthenticationEntryPoint = casAuthenticationEntryPoint;
    }
}
public class GatewayAwareAuthenticationFailureHandler implements AuthenticationFailureHandler 
{
    private RequestCache requestCache = new HttpSessionRequestCache();
    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    //Used as a fallback for when gateway authentication was not attempted
    private AuthenticationFailureHandler authenticationFailureHandler = new SimpleUrlAuthenticationFailureHandler();

    public void onAuthenticationFailure(HttpServletRequest request,
            HttpServletResponse response, AuthenticationException exception)
            throws IOException, ServletException 
    {   
        boolean attemptedGatewayAuth = WebUtils.getSessionAttribute(request,"ATTEMPTED_GATEWAY_AUTH") != null;
        if (attemptedGatewayAuth)
        {
            //Invalid ticket, or no ticket received from CAS gateway--should have a SavedRequest in the session for the unprotected page
            SavedRequest savedRequest = requestCache.getRequest(request, response);
            if (savedRequest != null)
            {
                String targetUrl = savedRequest.getRedirectUrl();          
                redirectStrategy.sendRedirect(request, response, targetUrl);
                return;
            }
        }

        authenticationFailureHandler.onAuthenticationFailure(request, response, exception);     
    }

    public void setRequestCache(RequestCache requestCache) {
        this.requestCache = requestCache;
    }

    public void setFallbackAuthenticationFailureHandler(
            AuthenticationFailureHandler authenticationFailureHandler) {
        this.authenticationFailureHandler = authenticationFailureHandler;
    }
}
spring-projects-issues commented 11 years ago

Eduard Gurskiy said:

Tim, thanks for this solution! Could you clarify how is it possible to configure such "gateway" behavior for some specific URLs in my app using "intercept-url" patterns?

spring-projects-issues commented 11 years ago

Tim Lenz said:

Good question! After my original post, I realized there were certain unprotected URLs that I didn't want to gateway at all (mostly AJAX services). Here's a new version of the filter that supports a list of "doNotGateway" expressions. It could probably be easily modified to support the reverse, i.e. a specific list of URLs to gateway.

public class CasUnprotectedPageFilter extends GenericFilterBean
{
    private static final Logger logger = Logger.getLogger(CasUnprotectedPageFilter.class);

    private RequestCache requestCache = new HttpSessionRequestCache();

    /**
     * A list of regular expressions the filter should NOT gateway to CAS
     */
    private List<RegexRequestMatcher> doNotGateway = new ArrayList<RegexRequestMatcher>();

    //Assumed to be a custom implementation where useGateway is always set to TRUE
    private CasAuthenticationEntryPoint casAuthenticationEntryPoint;

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException 
    {
        //If we're already logged in, there's no work to do
        if (CasUser.isLoggedIn())
        {   
            chain.doFilter(req,res);
            return;
        }

        HttpServletRequest request = (HttpServletRequest) req;
        boolean attemptedGatewayAuth = WebUtils.getSessionAttribute(request,"ATTEMPTED_GATEWAY_AUTH") != null;      
        if (attemptedGatewayAuth)
        {   //Already tried gateway authentication and not logged in, continue with filter chain 
            chain.doFilter(req,res);
            return;
        }

        //Compare request with doNotGateway patterns
        for (RegexRequestMatcher rrm:doNotGateway)
        {  
            if (rrm.matches(request))
            {
                //Exit filter if we find a match
                chain.doFilter(req,res);
                return;
            }
        }       

        request.getSession().setAttribute("ATTEMPTED_GATEWAY_AUTH",true);

        HttpServletResponse response = (HttpServletResponse) res;

        //Save the request so either the AuthenticationSuccessHandler or AuthenticationFailureHandler will know where to go after receiving the response back from CAS
        requestCache.saveRequest(request, response);

        //Try to authenticate with CAS gateway. Not entirely sure why we're passing in an exception to this method
        if (logger.isDebugEnabled())
            logger.debug("Commencing CAS gateway authentication");

        casAuthenticationEntryPoint.commence(request,response,new AuthenticationCredentialsNotFoundException("Authentication credentials not found"));

        return;
    }    

    public void setGatewayEnabledCasAuthenticationEntryPoint(
            CasAuthenticationEntryPoint casAuthenticationEntryPoint) {
        this.casAuthenticationEntryPoint = casAuthenticationEntryPoint;
    }

    public void setRequestCache(RequestCache requestCache) {
        this.requestCache = requestCache;
    }

    public void setDoNotGateway(List<String> patterns) 
    {
        if (patterns == null)
            return;

        for (String s:patterns)
        {
            doNotGateway.add(new RegexRequestMatcher(s,null));
        }   
    }
}

Then you can just wire in the URLs like this

 <beans:property name="doNotGateway">
            <beans:list>
                <beans:value>\A/.*ajaxCall1.*\Z</beans:value>
                        <beans:value>\A/.*ajaxCall2.*\Z</beans:value>
</beans:list>
        </beans:property>
spring-projects-issues commented 11 years ago

Eduard Gurskiy said:

Tim, thanks! This solution will work. Honestly, it would be much better to have gateway feature support inside Spring Security. Are there any plans to officially fix this issue? By the way, at the moment we use ajax-based solution for such scenario, which goes asynchronously to CAS and if it returns any ticket, then we confidently redirect user to secured page to perform auto-login, otherwise - we leave user on unsecured page. To minimize login form content amount we use custom specific CAS theme, which returns raw data instead of login page. I can provide details to anyone interested.

spring-projects-issues commented 11 years ago

Michaël REMOND said:

Hello,

I'm also interested in having the CAS gateway mechanism implemented by default in Spring Security. I would be happy to submit a pull request to help the team. Therefore I would like to discuss the main implementation details with a Spring Security member before submitting. Basically, my implementation is based on the patch submitted in 2008 :

Any suggestions are welcome

spring-projects-issues commented 11 years ago

Rob Winch said:

This sounds fairly reasonable. One question I have is what is the proposed mechanism for determining when a gateway request should be made? For example:

You can see that each request could trigger an authentication attempt. However, this is obviously not desirable. So my question is, what is the suggested mechanism to get this right? A few approaches could be:

The question with these solutions is why do we need the gateway request now? To me the gateway request is no longer necessary since we already know that we should be able to successfully authenticate. It would be quite rare to have a race condition and even if so the only downside is that the user is unnecessarily prompted to authenticate. So my question is how do we support the scenario where a user optionally logs in / does not always attempt to do a gateway authentication and still need gateway support?

spring-projects-issues commented 11 years ago

Jérôme Leleu said:

Hi Rob,

Michaël is working in my team.

By default, the CAS authentication is only started (entry point) when you are not authenticated and try to access a protected area. This would make sense if you only login in one web application. But, as CAS is a SSO, you are certainly already authenticated in another web applications before going to a new one. So this lead to this strange behaviour : after being authenticated and recognized in one web application, you are then anonymous in every none protected area of a new application.

Thus, the use case to have gateway configuration for some urls and no gateway configuration globally is to try to retrieve a previous CAS authentication. On public urls, if you want to retrieve a previous CAS authentication , you need to try to authenticate using gateway=true. On protected urls, as the user is required to be authenticated, it's not an attempted, the authentication must be successful to access (gateway=false).

About the way to detect that a CAS authentication has previously happened, you're right, we could use a cookie shared accross the domain. But this cookie does not reflect the CAS SSO session : this is a flag to say "an authentication has occured" which lasts the web session, where as the SSO session has its own timeout policy (idle time, max time to live). For example, you can login into a CAS server with a hard timeout of 30 minutes, access the first application and browse it 45 minutes. Then, if you try to access a new application, the domain-shared cookie still exists but your SSO session has ended, gateway=true is therefore necessary.

Thanks, Jérôme

spring-projects-issues commented 11 years ago

Rob Winch said:

Jérôme,

Thus, the use case to have gateway configuration for some urls and no gateway configuration globally is to try to retrieve a previous CAS authentication. On public urls, if you want to retrieve a previous CAS authentication , you need to try to authenticate using gateway=true. On protected urls, as the user is required to be authenticated, it's not an attempted, the authentication must be successful to access (gateway=false).

Yes this makes sense and was as I understood it. However, as I outlined in my example, it seems that the public pages would need to hit the CAS server for every request. This does not seem very scalable to me.

For example, you can login into a CAS server with a hard timeout of 30 minutes, access the first application and browse it 45 minutes. Then, if you try to access a new application, the domain-shared cookie still exists but your SSO session has ended, gateway=true is therefore necessary.

My thought was that only the CAS server would modify the cookie and it would set shared domain cookie to timeout at the same time as the CAS session. If the session gets touched, then you would simply update the the shared domain cookie. Yes CAS services could tamper with this cookie, but the worst case scenario is that it triggers a CAS service to request the user authenticate unnecessarily.

I'm not trying to resist this feature (in fact at one point I was a voter on this issue). I really just want to understand before we add additional complexity.

spring-projects-issues commented 11 years ago

Jérôme Leleu said:

You're right on both points.

We can't round-trip by the CAS server every time we access a public page. That doesn't scale at all ! That said, an easy solution would be to save into the web session that we already tried a CAS authentication with gateway=true to avoid any new attempt. But we can also reuse the Jasig CAS client mechanism to remember all urls tried with gateway=true (as Michaël proposed).

We could try to make the domain cookie follow exactly the lifetime of the SSO session but it's a customization on the CAS server side, so I tend to think that it's better if we limit the custom changes and the minimal one is to add this domain cookie when the login happens. It's sufficient to make things work.

We have a pretty big CAS server installation with more than 200 web applications, most of them using Spring Security and this custom mechanism. Thus, I'm pretty confident that it will work properly in terms of performance and use cases. We have decided to spend time to try to contribute back the main and reusable customaizations we have made in Spring Security. The main concern for us is to turn our custom implementations into more viable ones, with satisfactory design. That's what we try to propose here (with Michaël).

spring-projects-issues commented 11 years ago

Rob Winch said:

My main concern is trying to find a reason to add the additional (although minimal) complexity in order to support gateway. It seems that in order to support gateway we would need to add additional logic that could be used just as easily and reliably done without gateway support. Please correct me if I am wrong.

That said, an easy solution would be to save into the web session that we already tried a CAS authentication with gateway=true to avoid any new attempt

The concern I have with remembering that we have already tried to authenticate is it seems more likely to get out of sync. For example:

I'm not sure I followed this part. I didn't catch anything about the Jasig client being able to remember the URLs that were tried with gateway=true. In fact, I didn't know it supported this out of the box. Can one of you expand upon this?

The main concern for us is to turn our custom implementations into more viable ones, with satisfactory design. That's what we try to propose here (with Michaël).

This is much appreciated. Not only are you bringing real world use cases back to the community, but you are offering to do the coding. Again this and your patience is very much appreciated.

spring-projects-issues commented 11 years ago

Priit Liivak said:

bq. My main concern is trying to find a reason to add the additional (although minimal) complexity in order to support gateway. It seems that in order to support gateway we would need to add additional logic that could be used just as easily and reliably done without gateway support. Please correct me if I am wrong.

I have been involved in developing of an application that consisted of several modules that were deployed as separate applications. All of these module-applications looked the same so that user does not even notice what module he is using. If the only indication of authentication would have been users name in top right corner of the page then the gateway would not have been necessary. In our case it was very disturbing when user authenticated in one app, navigated to another applications public page using menu and all of a sudden the menu would change (authenticated items are hidden).

For us the cookie worked both ways

This of course requires that all connected applications use this logic.

spring-projects-issues commented 11 years ago

Rob Winch said:

priitThank you for your response. So my question is, "Do you feel like gateway is needed?" If so, why? It seems that unless a user or another application is tampering with the cookie (I assume this is kept up to date by the CAS Server), then there is no need for the gateway cookie. If someone does tamper with the cookie, the worst case scenario is the user is not prompted for authentication or is unnecessarily prompted for authentication.

spring-projects-issues commented 11 years ago

Michaël REMOND said:

Hi Rob,

You pointed out real implementation problems and it is true that gateway is a complex subject ; thank you for making us design better solutions.

However the gateway feature is part of the CAS protocol and we think spring-security should provide at least a basic implementation of the gateway feature (for example, the "renew" parameter is handled by spring-security). I think this is the primary question we must first discuss and agree on.

Then I think we should implement a solution the same way it is implemented in the Jasig Cas Client.

Here is how the Jasig client works :

This way, we always have an up-to-date authentication information about the user. I think the key to have an optimal spring-security configuration (scalable as you said) is to use carefully the spring security filter which will trigger the redirect to CAS server with gateway=true (for example by giving a limited list of public pages or by using several elements with different patterns). In short, it is the application responsibility to say which pages are allowed to trigger CAS gateway roundtrips (the people involved must understand the impacts of this feature).

Of course, the defining of this filter in the spring security configuration will be optional so the impact on a standard Spring Security CAS config will be minimal along with the added complexity in the source code. By default, the casAuthenticationEntryPoint will still send gateway=false requests to the CAS login url.

I propose to submit an implementation example based on everything previously discussed so we can share about a concrete implementation.

spring-projects-issues commented 11 years ago

Rob Winch said:

I think maybe the issue is that I am just shrugging off the benefit of the gateway feature. For very little more effort we don't need to worry about the edge case of unnecessarily requesting the user to login. Although a pretty big edge case, this does provide value and for very little additional complexity.

mremondPlease do submit an example as a pull request. One thing I think we should do is to ensure that what triggers the gateway request is implemented as a RequestMatcher. We can and should certainly have a default implementation for the user, but by using the RequestMatcher users can plugin their own strategies.

spring-projects-issues commented 11 years ago

Michaël REMOND said:

The feature was submitted in pull-request https://github.com/SpringSource/spring-security/pull/40. Let's review :-)

rwinch commented 3 years ago

Closing since this was superseded by gh-40