Apereo CAS - Delegated Authentication to SAML2 Identity Providers

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

Apereo CAS has had support to delegate authentication to external SAML2 identity providers for quite some time. This functionality, if memory serves me correctly, started around CAS 3.x as an extension based on the pac4j project, which then later found its way into the CAS codebase as a first-class feature. Since then, the functionality more or less has evolved to allow the adopter less configuration overhead and fancier ways to automate workflows.

Of course, delegation is just a fancy word that ultimately means, whether automatically or at the click of a button, the browser is expected to redirect the user to the appropriate SAML2 endpoint and on the return trip back, CAS is tasked to parse the response and extract attributes, etc in order to establish an authentication session, issue tickets, etc. In other words, in delegated scenarios, the main identity provider is an external system, and CAS simply begins to act as a client or proxy in between.

In the most common use case, CAS is made entirely invisible to the end-user such that the redirect simply happens automatically and as far as the audience is concerned, there are only the external identity provider and the target application that is, of course, prepped to speak the CAS protocol.

Let’s begin. Our starting position is based on:

Configuration

The initial setup is, in fact simple; as the documentation describes, you simply need to add the required dependency in your overlay:

implementation "org.apereo.cas:cas-server-support-pac4j-webflow"
implementation "org.apereo.cas:cas-server-support-pac4j-saml"

…and then in your cas.properties, instruct CAS to hand off authentication to the SAML2 identity provider:

cas.authn.pac4j.saml[0].keystore-password=pac4j-demo-passwd
cas.authn.pac4j.saml[0].private-key-password=pac4j-demo-passwd
cas.authn.pac4j.saml[0].keystore-path=$/path/to/samlKeystore.jks
cas.authn.pac4j.saml[0].service-provider-entity-id=cas:apereo:pac4j:saml
cas.authn.pac4j.saml[0].client-name=SAML2Client
cas.authn.pac4j.saml[0].destination-binding=urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect

cas.authn.pac4j.saml[0].metadata.service-provider.file-system.location=/etc/cas/config/sp-metadata.xml
cas.authn.pac4j.saml[0].metadata.identity-provider-metadata-path=https://idp.example.org/metadata.php

The above settings instruct CAS to:

  • Generate the service-provider metadata at /etc/cas/config/sp-metadata.xml using entity id urn:mace:saml:pac4j.org automatically. This metadata is created on CAS startup once the login page is rendered. This metadata is expected to be shared somehow with the SAML2 identity provider.
  • The URL to the identity provider metadata is also taught to CAS; note that in this case, we are using Okta as the SAML2 external identity provider. …and just to make things more interesting, I am going to create a stub attribute definition for employeeNumber with an always-hardcoded value of 4095712:
cas.authn.attribute-repository.stub.attributes.employeeNumber=4095712

This is going to be rather interesting because I have also configured my Okta SAML2 IdP to release an attribute named employeeNumber. Let’s see what happens when two systems compete for the same attribute.

Da Test

If you build and then bring up CAS, the main login screen might look something like this:

Getting to the SAML2Client will redirect you to the Okta SAML2 IdP:

After authentication, CAS might greet with you with a Hey! You logged in successfully message. Note that this message shows up because we didn’t originally specify a target application, with a service parameter, perhaps when we first accessed CAS.

If you expand the link to see attributes currently resolved, you will see everything the identity provider has released to CAS as a service provider. Interestingly, CAS has also merged the values for employeeNumber, effectively turning it into a multi-valued attribute honor both sources of attributes.

Remap Attributes

To make things more exciting, let’s instruct CAS to fetch the attribute employeeNumber from the identity provider and then virtually rename it to empl_id:

cas.authn.pac4j.saml[0].mapped-attributes[0].name=employeeNumber
cas.authn.pac4j.saml[0].mapped-attributes[0].mappedTo=empl_id

With those settings, if you go through the same sequence again you might see something like this:

Remember
Note that we are only in the process of fetching and resolving attributes in fancy ways. The decision of which application(s) should receive which resolved attributes may come later is likely to be decided on a per-application basis as part of the registered service definition body and its attribute release policy with CAS.

Identity Provider Authorization

Let’s pretend that we are using the JSON Service Registry to manage our application registration records. On a per-app basis and for a sample test application, let’s make sure our app is authorized to use our SAML2 identity provider in a delegated authentication scenario.

{
  "@class" : "org.apereo.cas.services.CasRegisteredService",
  "serviceId" : "https://www.example.org",
  "name" : "Example",
  "id" : 1,
  "evaluationOrder" : 1,
  "accessStrategy" : {
    "@class" : "org.apereo.cas.services.DefaultRegisteredServiceAccessStrategy",
    "delegatedAuthenticationPolicy" : {
      "@class" : "org.apereo.cas.services.DefaultRegisteredServiceDelegatedAuthenticationPolicy",
      "allowedProviders" : [ "java.util.ArrayList", [ "SAML2Client" ] ]
    }
  }
}
Remember
For backward compatibility reasons, leaving the allowedProviders as empty does not prevent a service definition for using an external identity provider...yet. While this behavior may change in future CAS versions (and you can expect warnings in the CAS logs if you leave this field as empty), you can still stop a service from using delegated authentication by assigning it an invalid/non-existing identity provider (i.e., client name).

Service Access Strategy

We know our identity provider is releasing a handful of attributes to CAS. Let’s play around with CAS access strategies and design a rule for our example application to only grant entry access to the application if CAS has access to a memberOf attribute with a value of Administrator. We know, of course, that the identity provider is not releasing this attribute yet, so we promptly should be greeted with a Sorry, you are not allowed to proceed type of error message.

Our application policy would look similar to this:

{
  "@class" : "org.apereo.cas.services.CasRegisteredService",
  "serviceId" : "https://www.example.org",
  "name" : "Example",
  "id" : 1,
  "evaluationOrder" : 1,
  "accessStrategy" : {
    "@class" : "org.apereo.cas.services.DefaultRegisteredServiceAccessStrategy",
    "requiredAttributes" : {
      "@class" : "java.util.HashMap",
      "memberOf" : [ "java.util.HashSet", [ "Administrator" ] ]
    },
    "delegatedAuthenticationPolicy" : {
      "@class" : "org.apereo.cas.services.DefaultRegisteredServiceDelegatedAuthenticationPolicy",
      "allowedProviders" : [ "java.util.ArrayList", [ "SAML2Client" ] ]
    }
  }
}

Now, if you try the same sequence again (don’t forget to start with the application), you’ll be greeted at the end of the flow with:

Metadata Management

Currently, the SAML2 metadata for CAS as a service provider is managed on disk:

cas.authn.pac4j.saml[0].metadata.service-provider.file-system.location=/etc/cas/config/sp-metadata.xml

Of course, there are a few other options that allow one to manage this metadata externally, one of which is using Amazon S3 buckets. This requires that you include the following module in your build:

implementation "org.apereo.cas:cas-server-support-aws"

…and then turn on the feature:

CasFeatureModule.DelegatedAuthentication.saml-s3.enabled=true

If you need to, you can also configure specific settings via the cas.authn.pac4j.saml[0].metadata.service-provider.amazon-s3 namespace. For example, setting the AWS region would be done via:

cas.authn.pac4j.saml[0].metadata.service-provider.amazon-s3.region=us-east-1

Or, if you wanted to explicitly set up AWS credentials:

cas.authn.pac4j.saml[0].metadata.service-provider.amazon-s3.credential-access-key=...
cas.authn.pac4j.saml[0].metadata.service-provider.amazon-s3.credential-secret-key=...

This allows CAS to create a dedicated Amazon S3 bucket and generate, track or find metadata objects by the service provider entity id. The bucket will be created automatically if it does not exist.

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.

Finale

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.

Misagh Moayyed