Apereo CAS - Authentication Flow Interrupts

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

The fastest route to a 10X engineer is to give them 0.1X the distractions. - Eric Meyer

While that is generally sensible advice, when it comes to CAS there are times where you wish to interrupt the CAS authentication flow and the present the end-user with notifications and announcements. A common use case deals with presenting a message board during the authentication flow to select users and then optionally require the audience to complete a certain task before CAS is able to honor the authentication request and establish a session. Examples of such messages tasks may include: “The kitchen’s menu today features Khash. Click here to get directions.” or “The office of compliance and regulations has announced a new policy on using forks. Click to accept, or forever be doomed with spoons”.

CAS has the ability to pause and interrupt the authentication flow to reach out to external services and resources, querying for status and settings that would then dictate how CAS should manage and control the SSO session. Interrupt services are able to present notification messages to the user, provide options for redirects to external services, etc.

In this post, we are going to take a brief look at what it takes to interrupt the authentication flow. This tutorial specifically focuses on:

  • CAS 7.0.x
  • Java 21

Interrupt Source

First and foremost, there needs to be an engine of some sort that is able to produce notifications and interruptions. CAS supports a range of such engines that are backed by JSON & Groovy resources, REST endpoints or one you decide to create and inject into the runtime.

For the purposes of this tutorial, I will be using a Groovy script. This strategy reaches out to a Groovy resource whose job is to dynamically calculate whether the authentication flow should be interrupted given the provided username and certain number of other parameters.

cas.interrupt.groovy.location=file:/path/to/your/interrupt.groovy

You can choose any path and filename you prefer.

Interrupt Rules

Once you have defined the above setting and assuming your overlay is prepped with relevant configuration module, CAS will attempt to understand the interruption rules that are defined in the Groovy file. My rules are defined as such:

import org.apereo.cas.interrupt.*

def run(final Object... args) {
    def (principal,attributes,service,registeredService,requestContext,logger) = args

    if (dontYouLoveItWhenYouAreInterrupted(principal, attributes)) {
      def block = false
      def ssoEnabled = true
      return new InterruptResponse("Message", [link1:"google.com", link2:"yahoo.com"], block, ssoEnabled)
    }
    return new InterruptResponse(false)
}

The above ruleset says: Evaluate the current principal and its attributes using dontYouLoveItWhenYouAreInterrupted(...) (something you, yes you, have to complete) and then present the Message to the user with a number of links. Do not block the user and allow the SSO session to be established.

Note that this is the default strategy that allows the interrupt query and script to execute after the primary authentication event and before the single sign-on event. This means an authenticated user has been identified by CAS and by extension is made available to the interrupt, and interrupt has the ability to decide whether a single sign-on session can be established for the user.

The Looks

Once that is all in place, casuser will see something similar to the following screen, after having authenticated successfully:

Custom Interrupt Sources

In scenarios where the power of Groovy is not good enough for you, you can always create your own interrupt source in Java. If you wish to design your own interrupt strategy to make inquiries, register the following bean:

@Bean
public InterruptInquiryExecutionPlanConfigurer myInterruptConfigurer() {
    return plan -> {
        plan.registerInterruptInquirer(new MyInterruptInquirer());
    };
}

…and your implementation may potentially look like this:

class MyInterruptInquirer extends BaseInterruptInquirer {
    @Override
    protected InterruptResponse inquireInternal(Authentication authentication,
                                                RegisteredService registeredService,
                                                Service service, 
                                                Credential credential,
                                                RequestContext requestContext) {
      // Stuff happens...
    }
}

Interruptions Per Application

There may be scenarios where you want to conditionally activate the interrupt flow for a specific or group of applications. One rather obvious though possibly cumbersome way of doing this would be to account for the condition and the application in your InterruptInquirer implementation or script the logic you need in Groovy. One easier way of doing this would be to activate the interrupt flow for an application only when the authenticated user carries a certain attributes that matches a value:

{
  "@class" : "org.apereo.cas.services.CasRegisteredService",
  "serviceId" : "^https://my.application.org.*",
  "name" : "MyApplication",
  "id" : 1,
  "webflowInterruptPolicy" : {
    "@class" : "org.apereo.cas.services.DefaultRegisteredServiceWebflowInterruptPolicy",
    "attributeName": "memberOf",
    "attributeValue": "^st[a-z]ff$"
  }
}

The above policy will activate the interrupt flow when logging into https://my.application.org or any of its child pages when the user has the attribute memberOf with at least a value matching the defined pattern.

A slightly more advanced way of doing this would be to move the trigger and activation logic inside the application record itself and use a modest Groovy script to build the condition:

{
  "@class" : "org.apereo.cas.services.CasRegisteredService",
  "serviceId" : "^https://my.application.org.*",
  "name" : "MyApplication",
  "id" : 1,
  "webflowInterruptPolicy" : {
    "@class" : "org.apereo.cas.services.DefaultRegisteredServiceWebflowInterruptPolicy",
    "enabled": true,
    "groovyScript":
      '''
      groovy {
        logger.info("Current attributes received are [{}]", attributes)
        return username == 'testuser' && attributes.containsKey('family_name')
      }
      '''
  }
}

The above application policy will activate the interrupt flow when logging into https://my.application.org or any of its child pages when the authenticated username is testuser and the user has the attribute family_name with any value.

Need Help?

If you have questions about the contents and the topic of this blog post, or if you need additional guidance and support, feel free to send us a note and ask about consulting and support services.

So…

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.

Happy Coding,

Misagh Moayyed