Forced Authentication with Apereo CAS

Posted by Misagh Moayyed on February 26, 2018 · 8 mins read ·
Content Unavailable
Your browser is blocking content on this website. Please check your browser settings and try again.

This post summarizes a recent conversation I had with a few colleagues on strategies one may use to support forced authentication with CAS and the journey we went on to discover and diagnose a few integration issues with an application protected by mod_auth_cas.

Let’s begin.

The Problem

Some CAS deployments present with the rather common requirement to challenge the user to re-enter credentials in some applications. To accommodate this, it’s likely that deployments may opt into creative yet non-standard solutions such as applications themselves prompting for credentials to replay them in a back-end call. This is problematic for a number of reasons:

  • Applications present a differently styled login form.
  • Applications directly get involved in handling user credentials.

Last but most important:

The only thing necessary for the triumph of evil is for good men to replay credentials.

So, is there a way to force re-authentication with CAS?

Forced Authentication

The CAS protocol has a specific parameter dedicated to forced authentication, named as renew=true.

If supplied as part of an authentication request:

Single sign-on will be bypassed. In this case, CAS will require the client to present credentials regardless of the existence of a single sign-on session with CAS.

If supplied as part of a validation request:

Ticket validation will only succeed if the service ticket was issued from the presentation of the user’s primary credentials. It will fail if the ticket was issued from a single sign-on session.

Version Caveat
If you have deployed CAS 5.2.x, you need to at least be on 5.2.3 for the renew parameter to function correctly.

It goes without saying that renew=true works best if you wish to let the application make that decision when needed. Alternatively, you may also control this behavior centrally by marking the relevant application/service in the CAS service registry (JSON file, etc) such that it would not participate in SSO and would always be asked for credentials regardless of what the application says.

Real Life Example

Let’s say we have an application at https://secure-dev.example.org/groups/ that has SSO enabled. We need one subfolder of that app at https://secure-dev.example.org/groups/reauth/ to require re-authentication to access.

So, (assuming a JSON service registry), we have one service registration record requiring SSO to cover any app on the host. Note that SSO participation is by default on and it’s perfectly good for us to practice laziness here.

{
  "@class": "org.apereo.cas.services.RegexRegisteredService"
  "serviceId": "^https?://[\w]+-dev.example.org/.*"
  "name": "secure-dev.example.org"
  "id": "1200"
  "description": "Everything"
  "evaluationOrder": "1200"
}

We set up another service registration with a lower (i.e. processed first; think of it like the Olympics rankings) evaluation order and ssoEnabled: false to cover the subfolder:

{
  "@class": "org.apereo.cas.services.RegexRegisteredService"
  "serviceId": "^https?://[\w]+-dev.example.org/groups/reauth/.*"
  "name": "secure-dev.example.org Groups Reauth"
  "id": "1200"
  "description": "Groups Reauth"
  "evaluationOrder": "1100"
  "accessStrategy": {
    "@class": "org.apereo.cas.services.DefaultRegisteredServiceAccessStrategy"
    "ssoEnabled": "false"
  }
}

Our application, protected by mod-auth-cas has the following configuration:

<Location /groups/reauth>
    AuthType CAS
    AuthName "CAS"
    CASScope /groups/reauth
    CASAuthNHeader SOME_LoginID
    CASScrubRequestHeaders On
    CASCookie MOD_AUTH_CAS_REAUTH
    Options None
    require valid-user
    Order allow, deny
    Allow from all
</Location>

<Location /groups>
    AuthType CAS
    AuthName "CAS"
    CASScope /
    CASAuthNHeader SOME_LoginID
    CASScrubRequestHeaders On
    CASCookie MOD_AUTH_CAS
    Options None
    require valid-user
    Order allow, deny
    Allow from all
</Location>

At first glance, this should do exactly as you would expect if only it weren’t for a small caveat.

The Caveat

If the request begins with https://secure-dev.example.org/groups/reauth/ first and then goes to https://secure-dev.example.org/groups/, the user would be prompted to log in - as expected because those locations use different cookie names. However, once logged in, the user would get caught in a redirect loop between mod_auth_cas and CAS. It turns out that there were two issues. One was that more precise location is placed first in the config:

<Location /groups/reauth>
   CASCookie MOD_AUTH_CAS_REAUTH
   CASScope /groups/reauth/
   ...
<Location>

<Location /groups>
   CASCookie MOD_AUTH_CAS
   CASScope /
   …
<Location>

This made mod_auth_cas use the last applicable CASScope directive which meant that the MOD_AUTH_CAS_REAUTH cookie was being set at Path=/ instead of Path=/groups/reauth/.

This might have been fine except for the way mod_auth_cas parses the values in the Cookie header. It tokenizes the header on ; then iterates through each cookie string by matching the number of characters equal to the length of the expected cookie name defined by the CASCookie directive. If the cookie string starts with the CASCookie name, it skips the next character, assuming that it’s = and takes all remaining characters. So what was happening was that, given the cookie header Cookie: BLARG=WuzzleWuzzle;MOD_AUTH_CAS_REAUTH=foofoofoofoo;, mod_auth_cas was matching the cookie string MOD_AUTH_CAS_REAUTH=foofoofoofoo with the CASCookie name of MOD_AUTH_CAS. Finding that, it skipped the _ and returned REAUTH=foofoofoofoo as the CAS cookie value. That isn’t a valid format for a CAS cookie so it redirected back to CAS. CAS, finding a valid TGC, then performed SSO and redirected back to the service. The service ticket was never validated because mod_auth_cas checks for the presence of a cookie before it checks for the ticket parameter. Finding the invalid cookie again, it redirected back to CAS and so on until the browser threw up its hands in defeat.

So, just something to look out for:

  • When using mod_auth_cas, configuration elements that share a scope (e.g. /groups, /groups/reauth) MUST be listed in order of least to most precise.
  • The CASCookie directives that share a scope MUST NOT be substrings of each other.

Summary

I hope this review was of some help to you and I am sure that both this post as well as the functionality it attempts to explain can be improved in any number of ways. Please feel free to engage and contribute as best as you can.

…and of course, a very special thanks to all colleagues who took part in this post, exchanged dialogue, verified behavior and generously took the time to share their analysis and findings. Thank you.

Misagh Moayyed