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 automated 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:
6.6.x
11
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"
…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].service-provider-entity-id=cas:apereo:pac4j:saml
cas.authn.pac4j.saml[0].service-provider-metadata-path=/etc/cas/config/sp-metadata.xml
cas.authn.pac4j.saml[0].keystore-path=$/path/to/samlKeystore.jks
cas.authn.pac4j.saml[0].identity-provider-metadata-path=https://idp.example.org/metadata.php
cas.authn.pac4j.saml[0].destination-binding=urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect
cas.authn.pac4j.saml[0].client-name=SAML2Client
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.…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 with two systems competing 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].mappedAttributes[0].name=employeeNumber
cas.authn.pac4j.saml[0].mappedAttributes[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.RegexRegisteredService",
"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.RegexRegisteredService",
"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’d be greeted at the end of the flow with:
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