Skip to content

SEC-977: Add support for CAS gateway feature #40

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

Conversation

miremond
Copy link

@miremond miremond commented Aug 8, 2013

The opportunity and the implementation details of this new feature were discussed in Jira SEC-977.

The new filter TriggerCasGatewayAuthenticationFilter has been added to
call the CasAuthenticationEntryPoint when we want try a silent CAS
authentication (typically on a public page). The trigger criteria is
done with a requestMatcher instance.
The method unsuccessfulAuthentication has been overridden in
CasAuthenticationFilter in order to redirect to the saved url if there
was no SSO session (no service ticket sent from CAS).
To avoid infinite loop, we use the DefaultGatewayResolverImpl from Jasig
Cas Client.

I have signed and agree to the terms of the SpringSource Individual Contributor License Agreement.

@miremond
Copy link
Author

Hello Rob,
Did you glance at the pull request? Don't hesitate to comment on my work.
Thank you very much

@rwinch
Copy link
Member

rwinch commented Aug 21, 2013

@miremond - Sorry I haven't had a chance to look at this in detail yet. I will look it over sometime this week though.

@rwinch
Copy link
Member

rwinch commented Aug 23, 2013

@miremond I apologize, but I won't be able to get to this today. It will be the first thing I work on for Monday.

* @author Michael Remond
*
*/
public class InitiateCasGatewayAuthenticationException extends
Copy link
Member

Choose a reason for hiding this comment

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

To be consistent with the TriggerCasGatewayAuthenticationFilter and the other AuthenticationException objects (which don't necessarily contain Authentication in them), this might be better named TriggerCasGatewayException

@rwinch
Copy link
Member

rwinch commented Aug 24, 2013

I was able to get to it this week after all (just needed a bit of weekend work) :) In general I think this pull request looks pretty good....only some minor renaming and extracting some of the logic into a RequestMatcher. For details, see the comments inline. Please feel free to respond to the comments if you disagree or have other ideas.

@miremond
Copy link
Author

Hello Rob,

Thank you so much for spending time on this pull request ; you should not
work the week end :-). I will make the enhancements you told me.
Regards.

2013/8/24 Rob Winch [email protected]

I was able to get to it this week after all (just needed a bit of weekend
work) :) In general I think this pull request looks pretty good....only
some minor renaming and extracting some of the logic into a RequestMatcher.
For details, see the comments inline. Please feel free to respond to the
comments if you disagree or have other ideas.


Reply to this email directly or view it on GitHubhttps://github.com//pull/40#issuecomment-23216544
.

The new filter TriggerCasGatewayAuthenticationFilter has been added to
call the CasAuthenticationEntryPoint when we want try a silent CAS
authentication (typically on a public page). The trigger criteria is
done with a requestMatcher instance.
The method unsuccessfulAuthentication has been overridden in
CasAuthenticationFilter in order to redirect to the saved url if there
was no SSO session (no service ticket sent from CAS).
To avoid infinite loop, we use the DefaultGatewayResolverImpl from Jasig
Cas Client.
@miremond
Copy link
Author

Some comments on my last commit:

  • I find DefaultCasGatewayRequestMatcher is a damned good name :-)
  • The authentication test is still based on CasAuthenticationToken but the method isAuthenticated can be overriden
  • Similarly the method performGatewayAuthentication can also be overriden to have fine control over the gateway feature

@miremond
Copy link
Author

Hello Rob, no news from you since my last submission. Can you check it?
Thank you very much.

@miremond
Copy link
Author

Hello Rob, still no news. don't forget me :-)

@rwinch
Copy link
Member

rwinch commented Sep 25, 2013

@miremond Thanks for your patience. I probably should have been more explicit about this, but this feature won't be included until the next release. The reason is that we have already put out a RC (which was out prior to the PR modifications) which means we shouldn't be including any new large features (especially that integrate with existing functionality). In the meantime, I am busy finishing up with 3.2. I assure you that this will be the first thing I integrate with the next version of Spring Security.

@miremond
Copy link
Author

Thank you for your response Rob. No problem for the targeted version ; I just wanted to have your feedback on the last version of my work if it sounds good for you. Thank you again for your interest.

@rwinch
Copy link
Member

rwinch commented Sep 25, 2013

Understood...right now I am trying to focus as much on the next RC so we can have it out for feedback. I do apologize for this sitting so long, but at the moment getting out the next RC is the highest priority. With limited resources that means this will sit until we get the RC out. Again thank you very much for your contribution and I will look at it as soon as the next RC is out (likely around a week).

@obozek
Copy link

obozek commented Oct 1, 2013

I tried your implementation, it works fine except one thing. There is no support for gatewaying requests to applications where user is already authenticated. So when user logs out from cas these applications stay authenticated with this user and doesn't recognise that user have already logged out. This could lead to potential security risk.

@miremond
Copy link
Author

miremond commented Oct 1, 2013

Hello Infrag, i'm not sure I understand the security issue you're talking about.

The gateway mechanism is just something to silently authenticate to one application if the user is already CAS authenticated.
When the user logout from CAS, it is its responsibility to suspend every session on the applications the user has accessed. Even in the situation where the CAS logout does not imply an local application logout, the gateway mechanism will try an authentication but will silently fail if the user is not CAS authenticated anymore.

public final boolean matches(HttpServletRequest request) {

// Test if we are already authenticated
if (isAuthenticated(request)) {
Copy link

Choose a reason for hiding this comment

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

Should be omited for Single Logout to work

@obozek
Copy link

obozek commented Oct 3, 2013

Hi, sorry for late reply.

I have implemented Single Logout mechanism in my aplications. So if I log out from CAS it logs out also from all the other applications (I'm not sure how it exactly works in background). But the same thing doesn't work with gateway mode. See scenario below:

  1. USER_1 is not logged in to any application
  2. USER_1 accesses APP_1 and logs in to the APP_1 using CAS.
  3. USER_1 navigates to public section of APP_2 - CAS in gateway mode is used to log user in in APP_2.
  4. USER_1 navigates to APP_1 and logs out.
  5. USER_1 navigates to APP_2 and is still logged in! Possible security risk.

This occurs only in combination with gateway mode. If i don't use GW mode, and APP_2 doesn't have public section (whole APP_2 requires authentication), Single Logout works and I'm logged out from APP_2 in step 5 of scenario above.

My current solution for this behavior is to omit lines 54,55,56 from DefaultCasGatewayRequestMatcher:

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

This way loged user is checked on CAS server and Single Logout works. It's not very scalable. Better understanding of Single Logout in CAS could help to find better solution.
This solution also collides when default target url is configured (application can't escape default target URL).

@miremond
Copy link
Author

Hi and also sorry for my delay this time ;-)

I still think there is no problem with my gateway implementation ; you can check that my implementation is similar to the standard Jasig CAS client.

It seems you say when one authenticates in an app with the gateway mode (APP2 in your example), the CAS server won't handle this app in the single logout process. But as far as I know, as soon the CAS server generates a service ticket for one service (gateway mode or not), the service is saved and will be notified during logout to invalidate the local session (the mechanism is: the CAS server sends a logout request to each application that requested an authentication (server-to-server call)).
Moreover, a user has a unique Spring Security Context: basically anonymous or authenticated. It is different from the public / private sections of your website. I am identically authenticated on public or private sections.

@rwinch
Copy link
Member

rwinch commented Mar 13, 2014

@miremond Sorry this has taken so long. I'm looking into merging this now.

I'm curious if it was intentional that every anonymous request hits the CAS server? This seems like it could place a large load on the CAS Server if there are many anonymous users. I realize that the request is a very quick one if no TGT is found, but it seems like a very large number of requests will be hitting the CAS Server.

Perhaps the default should be trying once per session?

Any thoughts would be appreciated.

@miremond
Copy link
Author

@rwinch Sorry for my delay, I was on Holidays :-)
Your question is very interesting; I discussed this issue with @leleuj. We think there is a performance problem even in the original CAS client; the behavior is exactly the same. We should probably change the gatewayResolver to try a gateway authentication every xx minutes.

I will discuss the question on the CAS mailing list and come back with answers.
Any suggestions from you are welcome.

@julioarguello
Copy link

Any news regarding this request? Really interesting topic.

@miremond
Copy link
Author

Hello,
Good to see that someone is also interested in this feature. It's my fault, recently I focused on other subjects but I still have to make a pull request on the CAS Client before we can finish this discussion with Rob.

@julioarguello
Copy link

Thanks for your response. But, before the change to CAS client arrives, is any change to deploy your branch in a production service?

@miremond
Copy link
Author

miremond commented Dec 1, 2014

I think you can use the code from this PR but be careful to how often you try a gateway request. By overriding the method peformGatewayRequest in DefaultCasGatewayRequestMatcher, you can fine tune when to issue a gateway request (e.g once per session, every XX minutes...)

@julioarguello
Copy link

Thanks indeed. In my use case once per session is enough.

@jgribonvald
Copy link

+1 interest

Otherwise i followed this approch to redirect after authentication on specific page : http://jlorenzen.blogspot.fr/2013/07/remember-target-url-with-spring.html

@julioarguello
Copy link

I'm afraid this solution does not fit perfectly to my circumstances.

In my opinion there are three pitfalls:

  • The first one is that redirecting promiscuously on every single request is a performance degradation.
  • On other hand this may affect to browsing history behaviour.
  • And at last but not least, from SEO point of view may there be a real problem with robots indexing a 302 status code while indexing home page, for instance.

I have implemented a easy solution based on javascript in order to trigger a gateway aware request if and only if CAS query (CASTGC) is present. Let me know if you need any further information.

@jgribonvald
Copy link

@Julicrack I'm interested, because i used my specified approach to authenticate on a REST api for a web single page done with angularJS. And I found the CAS approach wasn't really easy/obvious (for this case) when we compare to other auth.

@julioarguello
Copy link

My approach consists on including an iframe within client page pointing to CAS domain.

This iframe would include a javascript in order to dinamically obtain CAS ticket granting cookie and if 'valid' reload page on top window using gateway mode.

The aim is to avoid as much as possible gateway requests.

This is the iframe content:

<html>
    <head>
        <script src="/js/gateway.js"></script>
    </head>
    <body onload="CAS.triggerGatewayAuth()">
        <form id="reloadParent" name="reloadParent" method="GET" target="_top">
            <input type="hidden" name="gateway" value="true" />
        </form>
    </body id="body">
</html>

And this is the included javascript:

var CAS = {
    // Regexp that matches an URL
    urlRegexp : /^(?:([A-Za-z]+):)?(\/{0,3})([0-9.\-A-Za-z]+)(?::(\d+))?(?:\/([^?#]*))?(?:\?([^#]*))?(?:#(.*))?$/,

    // Regexp that matches the Query String Part (QSP)
    qspRegexp : /([^?=&]+)(=([^&]*))?/g,

    // Triggers a CAS gateway authentication if Ticket Granting Cookie (TGC) is present and has not already being
    // processed (TGC == CTR)
    triggerGatewayAuth : function() {
        /*
         * Parse iframe location to obtain a gateway aware redirect location
         * 
         * i.e.: <iframe src="http://my.cas.com/gateway.html#http://mysite.com?foo=bar"></iframe>
         */
        var triggerCasGatewayUrl = decodeURI(CAS.urlRegexp.exec(location.href)[7]); // i.e.: http://mysite.com?foo=bar
        var triggerCasGatewayUrlParts = CAS.urlRegexp.exec(triggerCasGatewayUrl);
        var triggerCasGatewayHst = triggerCasGatewayUrlParts[3]; // i.e.: mysite.com
        var triggerCasGatewayQsp = triggerCasGatewayUrlParts[6]; // i.e.: ?foo=bar

        var casCtrCookieName = 'CASCTR.' + triggerCasGatewayHst;

        var casTgc = W3S.getCookie('CASTGC'); // CAS ticket granting cookie
        var casCtr = W3S.getCookie(casCtrCookieName); // Last tested ticket granting cookie

        // If ticket granting cookie exist, has not been tested and
        if ((typeof casTgc != 'undefined') && (typeof triggerCasGatewayUrl != 'undefined')) {

            if (casCtr != casTgc) {

                var triggerCasGatewayQsp = CAS.urlRegexp.exec(triggerCasGatewayUrl)[6]; // i.e.: ?foo=bar

                var triggerCasGatewayQspMap = {}; // https://remysharp.com/2008/06/24/query-string-to-object-via-regex
                triggerCasGatewayQsp.replace(CAS.qspRegexp, function($0, $1, $2, $3) {
                    triggerCasGatewayQspMap[$1] = $3;
                });

                // Avoid infinite loop
                W3S.setCookie(casCtrCookieName, casTgc);

                // Trigger a gateway aware authentication request
                CAS.decorateForm(document.forms[0], triggerCasGatewayUrl, triggerCasGatewayQspMap).submit();
            }
        }
    },

    // Decorated the form that triggers the authentication request
    decorateForm : function(formElement, action, parameterMap) {

        formElement.action = action;

        for ( var key in parameterMap) {
            var inputElement = document.createElement("input");

            inputElement.setAttribute('type', 'hidden');
            inputElement.setAttribute('name', key);
            inputElement.setAttribute('value', parameterMap[key]);

            formElement.appendChild(inputElement);
        }

        return formElement;
    }
};

/*
 * http://www.w3schools.com/js/js_cookies.asp
 */
var W3S = {
    setCookie : function(cname, cvalue, exdays) {
        var d = new Date();
        d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
        var expires = "expires=" + d.toUTCString();
        document.cookie = cname + "=" + cvalue + "; " + expires;
    },

    getCookie : function(cname) {
        var name = cname + "=";
        var ca = document.cookie.split(';');
        for (var i = 0; i < ca.length; i++) {
            var c = ca[i];
            while (c.charAt(0) == ' ')
                c = c.substring(1);
            if (c.indexOf(name) == 0)
                return c.substring(name.length, c.length);
        }
        // return "";
    }
}

Iframe src should be something like this:
https://cas.domain/html/gateway.html#current_page_url_with_query_string

Gateway mode is enabled if and only if 'gateway' parameter is included and true.

Do note external javascript frameworks are intentionally avoided.

Any feedback would be really apprecitated.

@miremond
Copy link
Author

miremond commented Mar 4, 2015

hello
@Julicrack From what I can see here, you implemented the gateway feature almost entirely in javascript; I think this does not fit to the spring security philosophy. We should ask @rwinch.
I agree with you that SEO could be an issue with the gateway mechanism. I think one can easily fix that with a simple cookie indicating if we should attempt gateway. Then, if you consider my PR, you can simply override the method to test the existence of the cookie:

protected boolean performGatewayAuthentication(HttpServletRequest request) {
      return true;
 }

But again, there is something missing to fulfill this PR; I will soon submit an update.

@julioarguello
Copy link

Umm, I'm afraid that Google does (or will) take into account JavaScript and/or cookie during indexing.

That is the reason why script above triggers a gateway authentication if and only if a cookie from other domain is present. Google will never keep track of this cookie (CASTGC) and gateway mode is avoided during crawling procceses.

On other hand, the unique mission of this script is just to realize when is the more accurate moment to trigger a gateway request. In such a case the negotiation is done according with this branch.

Further reading:
http://www.tomanthony.co.uk/blog/googlebot-accepting-cookies/

@rwinch
Copy link
Member

rwinch commented Mar 6, 2015

Thanks for carrying this forward guys. As @miremond guessed, I'd prefer to allow this feature to work without JavaScript. I don't think there is necessarily a problem with using JavaScript in individual applications for customizations, I just don't thinks something like CAS Gateway should require JavaScript. With that in mind, I prefer the cookie approach.

@julioarguello
Copy link

A cookie trigerring based solution is easier... and 90% of times easier means better.

Supossing Google won't follow cookies this would be a valid approach from SEO point of view.

However asumming every single anonymous request will carry out a 302 is too much IMHO.

Aproach above not only reduce the number of hits to CAS server but also simplifies browsing and backward and forward button from cliente webapp point of view.

Thanks indeed for your support.

@rwinch rwinch force-pushed the master branch 2 times, most recently from aa28ae7 to aed288d Compare July 8, 2015 16:48
@dave0100
Copy link

Hi all, I just wanted to share a server side implementation based on the feedback, patches and discussion here - in case others don't want to manually build spring security or modify cas. We of course want to use this patch once it landed, but this approch works for us so far:

  • map a filter before the spring security filter chain in your web.xml to your landing page, the one you want both anonymous access on but autologin in users which already have a CAS ticket:
    public void doFilter(javax.servlet.ServletRequest req, javax.servlet.ServletResponse res, javax.servlet.FilterChain chain) throws java.io.IOException, javax.servlet.ServletException {

        if(req instanceof HttpServletRequest) {
            HttpServletRequest request = ((HttpServletRequest)req);

            LOG.trace("CASAuthFilter called");

            if(toggles.is("cas.gateway.enabled")) {

                LOG.debug("CASAuthFilter recognized request for main page, checking login conditions");

                if(WebUtils.getSessionAttribute(request, "ssoCheckDone") == null) {

                    LOG.debug("SSO check not done yet");

                    //don't use this feature for requests made via redirect.html (yet, may enable at will)
                    if(WebUtils.getSessionAttribute(request, "startImmediately") == null) {

                        WebUtils.setSessionAttribute(request, "ssoCheckDone", true);

                        HttpServletResponse response = ((HttpServletResponse) res);

                        String url = casURL + "/login?gateway=true&service=" + service + "/j_spring_cas_security_check";

                        LOG.debug("Redirecting to gateway URL: "+url);

                        response.sendRedirect(url);
                        response.flushBuffer();
                        response.getOutputStream().close();

                        return;
                    }
                    else {
                        LOG.debug("SSO check skipped because startImmediatly is set");
                    }
                }
                else {
                    LOG.debug("SSO check has already been done");
                }
            }
        }

        chain.doFilter(req, res);
    }

The HTTP session attribute ssoCheckDone takes care of just using the CAS gateway feature once for each session.

  • Map an error handler in your web.xml an let him handle the Exception that is thrown if /j_spring_cas_security_check (the gateway return URL) is called without a valid ticket. This redirects back to the landingpage:
    @RequestMapping("error")
    public String handelError(final HttpServletRequest request, HttpServletResponse response, final Model model) {

        //ONSE-9988 Fail gracefully here, as it can be normal behaviour after using the gateway feature
        if(toggles != null && toggles.is("cas.gateway.enabled") && handleGracefulSSO(request, response)) {
            return null;
        }
         return "error/error";
    }
    private boolean handleGracefulSSO(final HttpServletRequest request, HttpServletResponse response) {
        String fwdURL = (String) request.getAttribute(RequestDispatcher.FORWARD_REQUEST_URI);

        LOG.warn(fwdURL);

        if(fwdURL != null && fwdURL.endsWith("/j_spring_cas_security_check")) {
            LOG.warn("CAS security check failed, redirecting to /app gracefully");

            try {
                response.sendRedirect("/app");
                response.flushBuffer();
                response.getOutputStream().close();

                return true;
            }
            catch (Throwable t) {
                LOG.error("Response failed", t);
            }
        }

        return false;
    }

@pivotal-issuemaster
Copy link

@miremond Please sign the Contributor License Agreement!

Click here to manually synchronize the status of this Pull Request.

See the FAQ for frequently asked questions.

@pivotal-issuemaster
Copy link

@miremond Please sign the Contributor License Agreement!

Click here to manually synchronize the status of this Pull Request.

See the FAQ for frequently asked questions.

@rwinch
Copy link
Member

rwinch commented Oct 18, 2016

Closing this given there hasn't been the updates (i.e. use cookies to prevent redirecting for every request) have not been made

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants