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:
7.1.x
21
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:
/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.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.
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.
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:
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" ] ]
}
}
}
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).
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:
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.
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.
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.
Monday-Friday
9am-6pm, Central European Time
7am-1pm, U.S. Eastern Time
Monday-Friday
9am-6pm, Central European Time