JA-SIG produces an enterprise-wide single sign on system known as CAS. Unlike other initiatives, JA-SIG's Central Authentication Service is open source, widely used, simple to understand, platform independent, and supports proxy capabilities. Spring Security fully supports CAS, and provides an easy migration path from single-application deployments of Spring Security through to multiple-application deployments secured by an enterprise-wide CAS server.
You can learn more about CAS at http://www.ja-sig.org/cas
. You will
also need to visit this site to download the CAS Server files.
Whilst the CAS web site contains documents that detail the architecture of CAS, we present the general overview again here within the context of Spring Security. Spring Security 3.x supports CAS 3. At the time of writing, the CAS server was at version 3.4.
Somewhere in your enterprise you will need to setup a CAS server. The CAS server is simply a standard WAR file, so there isn't anything difficult about setting up your server. Inside the WAR file you will customise the login and other single sign on pages displayed to users.
When deploying a CAS 3.4 server, you will also need to specify an
AuthenticationHandler
in the
deployerConfigContext.xml
included with CAS. The
AuthenticationHandler
has a simple method that returns a boolean as to
whether a given set of Credentials is valid. Your AuthenticationHandler
implementation will need to link into some type of backend authentication repository, such as
an LDAP server or database. CAS itself includes numerous
AuthenticationHandler
s out of the box to assist with this. When you
download and deploy the server war file, it is set up to successfully authenticate users who
enter a password matching their username, which is useful for testing.
Apart from the CAS server itself, the other key players are of course the secure web applications deployed throughout your enterprise. These web applications are known as "services". There are three types of services. Those that authenticate service tickets, those that can obtain proxy tickets, and those that authenticate proxy tickets. Authenticating a proxy ticket differs because the list of proxies must be validated and often times a proxy ticket can be reused.
The basic interaction between a web browser, CAS server and a Spring Security-secured service is as follows:
The web user is browsing the service's public pages. CAS or Spring Security is not involved.
The user eventually requests a page that is either secure or
one of the beans it uses is secure. Spring Security's
ExceptionTranslationFilter
will detect the
AccessDeniedException
or AuthenticationException
.
Because the user's Authentication
object (or lack
thereof) caused an AuthenticationException
, the
ExceptionTranslationFilter
will call the configured
AuthenticationEntryPoint
. If using CAS, this will be
the CasAuthenticationEntryPoint
class.
The CasAuthenticationEntryPoint
will redirect the user's browser
to the CAS server. It will also indicate a service
parameter, which
is the callback URL for the Spring Security service (your application). For example, the
URL to which the browser is redirected might be
https://my.company.com/cas/login?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Fj_spring_cas_security_check
.
After the user's browser redirects to CAS, they will be
prompted for their username and password. If the user presents a
session cookie which indicates they've previously logged on, they
will not be prompted to login again (there is an exception to this
procedure, which we'll cover later). CAS will use the
PasswordHandler
(or
AuthenticationHandler
if using CAS 3.0)
discussed above to decide whether the username and password is
valid.
Upon successful login, CAS will redirect the user's browser
back to the original service. It will also include a
ticket
parameter, which is an opaque string
representing the "service ticket". Continuing our earlier example,
the URL the browser is redirected to might be
https://server3.company.com/webapp/j_spring_cas_security_check?ticket=ST-0-ER94xMJmn6pha35CQRoZ
.
Back in the service web application, the CasAuthenticationFilter
is
always listening for requests to /j_spring_cas_security_check
(this
is configurable, but we'll use the defaults in this introduction). The processing filter
will construct a UsernamePasswordAuthenticationToken
representing the
service ticket. The principal will be equal to
CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER
, whilst the credentials
will be the service ticket opaque value. This authentication request will then be handed
to the configured AuthenticationManager
.
The AuthenticationManager
implementation
will be the ProviderManager
, which is in turn
configured with the CasAuthenticationProvider
.
The CasAuthenticationProvider
only responds to
UsernamePasswordAuthenticationToken
s containing
the CAS-specific principal (such as
CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER
)
and CasAuthenticationToken
s (discussed
later).
CasAuthenticationProvider
will validate the service ticket using a
TicketValidator
implementation. This will typically be a
Cas20ServiceTicketValidator
which is one of the classes
included in the CAS client library. In the event the application needs to validate proxy tickets, the
Cas20ProxyTicketValidator
is used. The
TicketValidator
makes an HTTPS request to the CAS server in order to
validate the service ticket. It may also include a proxy callback URL, which is included in this example:
https://my.company.com/cas/proxyValidate?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Fj_spring_cas_security_check&ticket=ST-0-ER94xMJmn6pha35CQRoZ&pgtUrl=https://server3.company.com/webapp/j_spring_cas_security_proxyreceptor
.
Back on the CAS server, the validation request will be received. If the presented service ticket matches the service URL the ticket was issued to, CAS will provide an affirmative response in XML indicating the username. If any proxy was involved in the authentication (discussed below), the list of proxies is also included in the XML response.
[OPTIONAL] If the request to the CAS validation service included the proxy callback
URL (in the pgtUrl
parameter), CAS will include a
pgtIou
string in the XML response. This pgtIou
represents a proxy-granting ticket IOU. The CAS server will then create its own HTTPS
connection back to the pgtUrl
. This is to mutually authenticate the
CAS server and the claimed service URL. The HTTPS connection will be used to send a
proxy granting ticket to the original web application. For example,
https://server3.company.com/webapp/j_spring_cas_security_proxyreceptor?pgtIou=PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt&pgtId=PGT-1-si9YkkHLrtACBo64rmsi3v2nf7cpCResXg5MpESZFArbaZiOKH
.
The Cas20TicketValidator
will parse the XML received from the
CAS server. It will return to the CasAuthenticationProvider
a
TicketResponse
, which includes the username (mandatory), proxy list
(if any were involved), and proxy-granting ticket IOU (if the proxy callback was
requested).
Next CasAuthenticationProvider
will call
a configured CasProxyDecider
. The
CasProxyDecider
indicates whether the proxy
list in the TicketResponse
is acceptable to the
service. Several implementations are provided with Spring
Security: RejectProxyTickets
,
AcceptAnyCasProxy
and
NamedCasProxyDecider
. These names are largely
self-explanatory, except NamedCasProxyDecider
which allows a List
of trusted proxies to be
provided.
CasAuthenticationProvider
will next
request a AuthenticationUserDetailsService
to load the
GrantedAuthority
objects that apply to the user
contained in the Assertion
.
If there were no problems,
CasAuthenticationProvider
constructs a
CasAuthenticationToken
including the details
contained in the TicketResponse
and the
GrantedAuthority
s.
Control then returns to
CasAuthenticationFilter
, which places the created
CasAuthenticationToken
in the security context.
The user's browser is redirected to the original page that
caused the AuthenticationException
(or a
custom destination depending on
the configuration).
It's good that you're still here! Let's now look at how this is configured
The web application side of CAS is made easy due to Spring Security. It is assumed you already know the basics of using Spring Security, so these are not covered again below. We'll assume a namespace based configuration is being used and add in the CAS beans as required. Each section builds upon the previous section. A full CAS sample application can be found in the Spring Security Samples.
This section describes how to setup Spring Security to authenticate Service Tickets. Often times
this is all a web application requires. You will need to add a ServiceProperties
bean to your application context. This represents your CAS service:
<bean id="serviceProperties" class="org.springframework.security.cas.ServiceProperties"> <property name="service" value="https://localhost:8443/cas-sample/j_spring_cas_security_check"/> <property name="sendRenew" value="false"/> </bean>
The service
must equal a URL that will be monitored by the
CasAuthenticationFilter
. The sendRenew
defaults to
false, but should be set to true if your application is particularly sensitive. What
this parameter does is tell the CAS login service that a single sign on login is
unacceptable. Instead, the user will need to re-enter their username and password in
order to gain access to the service.
The following beans should be configured to commence the CAS authentication process (assuming you're using a namespace configuration):
<security:http entry-point-ref="casEntryPoint"> ... <security:custom-filter position="CAS_FILTER" ref="casFilter" /> </security:http> <bean id="casFilter" class="org.springframework.security.cas.web.CasAuthenticationFilter"> <property name="authenticationManager" ref="authenticationManager"/> </bean> <bean id="casEntryPoint" class="org.springframework.security.cas.web.CasAuthenticationEntryPoint"> <property name="loginUrl" value="https://localhost:9443/cas/login"/> <property name="serviceProperties" ref="serviceProperties"/> </bean>
For CAS to operate, the ExceptionTranslationFilter
must have
its authenticationEntryPoint
property set to the
CasAuthenticationEntryPoint
bean. This can easily be done using
entry-point-ref
as is
done in the example above. The CasAuthenticationEntryPoint
must refer to the
ServiceProperties
bean (discussed above), which provides the URL
to the enterprise's CAS login server. This is where the user's browser will be
redirected.
The CasAuthenticationFilter
has very similar properties to the
UsernamePasswordAuthenticationFilter
(used for form-based
logins). You can use these properties to customize things like behavior for authentication
success and failure.
Next you need to add a CasAuthenticationProvider
and its
collaborators:
<security:authentication-manager alias="authenticationManager"> <security:authentication-provider ref="casAuthenticationProvider" /> </security:authentication-manager> <bean id="casAuthenticationProvider" class="org.springframework.security.cas.authentication.CasAuthenticationProvider"> <property name="authenticationUserDetailsService"> <bean class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper"> <constructor-arg ref="userService" /> </bean> </property> <property name="serviceProperties" ref="serviceProperties" /> <property name="ticketValidator"> <bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator"> <constructor-arg index="0" value="https://localhost:9443/cas" /> </bean> </property> <property name="key" value="an_id_for_this_auth_provider_only"/> </bean> <security:user-service id="userService"> <security:user name="joe" password="joe" authorities="ROLE_USER" /> ... </security:user-service>
The CasAuthenticationProvider
uses a
UserDetailsService
instance to load the authorities for a
user, once they have been authenticated by CAS. We've shown a simple in-memory setup
here. Note that the CasAuthenticationProvider
does not actually use
the password for authentication, but it does use the authorities.
The beans are all reasonably self-explanatory if you refer back to the How CAS Works section.
This completes the most basic configuration for CAS. If you haven't made any mistakes, your web application should happily work within the framework of CAS single sign on. No other parts of Spring Security need to be concerned about the fact CAS handled authentication. In the following sections we will discuss some (optional) more advanced configurations.
The CAS protocol supports Single Logout and can be easily added to your Spring Security configuration. Below are updates to the Spring Security configuration that handle Single Logout
<security:http entry-point-ref="casEntryPoint"> ... <security:logout logout-success-url="/cas-logout.jsp"/> <security:custom-filter ref="requestSingleLogoutFilter" before="LOGOUT_FILTER"/> <security:custom-filter ref="singleLogoutFilter" before="CAS_FILTER"/> </security:http> <!-- This filter handles a Single Logout Request from the CAS Server --> <bean id="singleLogoutFilter" class="org.jasig.cas.client.session.SingleSignOutFilter"/> <!-- This filter redirects to the CAS Server to signal Single Logout should be performed --> <bean id="requestSingleLogoutFilter" class="org.springframework.security.web.authentication.logout.LogoutFilter"> <constructor-arg value="https://localhost:9443/cas/logout"/> <constructor-arg> <bean class= "org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler"/> </constructor-arg> <property name="filterProcessesUrl" value="/j_spring_cas_security_logout"/> </bean>
The logout
element logs the user out of the local application, but
does not terminate the session with the CAS server or any other applications that have been logged
into. The requestSingleLogoutFilter
filter will allow the url of
/spring_security_cas_logout
to be requested to redirect the application to the
configured CAS Server logout url. Then the CAS Server will send a Single Logout request to all the
services that were signed into. The singleLogoutFilter
handles the Single Logout
request by looking up the HttpSession
in a static Map
and then invalidating it.
It might be confusing why both the logout
element and the
singleLogoutFilter
are needed. It is considered best practice to logout locally
first since the SingleSignOutFilter
just stores the
HttpSession
in a static Map
in order to
call invalidate on it. With the configuration above, the flow of logout would be:
/j_spring_security_logout
which would log the user
out of the local application and send the user to the logout success page./cas-logout.jsp
, should instruct the user
to click a link pointing to /j_spring_cas_security_logout
in order to logout
out of all applications.https://localhost:9443/cas/logout
).SingleSignOutFilter
processes the logout request by invaliditing the
original session.
The next step is to add the following to your web.xml
<filter> <filter-name>characterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>characterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <listener> <listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class> </listener>
When using the SingleSignOutFilter you might encounter some encoding issues. Therefore it is
recommended to add the CharacterEncodingFilter
to ensure that the character
encoding is correct when using the SingleSignOutFilter
. Again, refer to JASIG's
documentation for details. The SingleSignOutHttpSessionListener
ensures that
when an HttpSession
expires, the mapping used for single logout is
removed.
This section describes how to authenticate to a service using CAS. In other words, this section discusses how to setup a client that uses a service that authenticates with CAS. The next section describes how to setup a stateless service to Authenticate using CAS.
In order to authenticate to a stateless service, the application needs to obtain a proxy granting ticket (PGT). This section describes how to configure Spring Security to obtain a PGT building upon then Service Ticket Authentication configuration.
The first step is to include a ProxyGrantingTicketStorage
in your Spring Security
configuration. This is used to store PGT's that are obtained by the
CasAuthenticationFilter
so that they can be used to obtain proxy tickets. An example
configuration is shown below
<!-- NOTE: In a real application you should not use an in memory implementation. You will also want to ensure to clean up expired tickets by calling ProxyGrantingTicketStorage.cleanup() --> <bean id="pgtStorage" class="org.jasig.cas.client.proxy.ProxyGrantingTicketStorageImpl"/>
The next step is to update the CasAuthenticationProvider
to be able to obtain proxy
tickets. To do this replace the Cas20ServiceTicketValidator
with a
Cas20ProxyTicketValidator
. The proxyCallbackUrl
should be set to
a URL that the application will receive PGT's at. Last, the configuration should also reference the
ProxyGrantingTicketStorage
so it can use a PGT to obtain proxy tickets.
You can find an example of the configuration changes that should be made below.
<bean id="casAuthenticationProvider" class="org.springframework.security.cas.authentication.CasAuthenticationProvider"> ... <property name="ticketValidator"> <bean class="org.jasig.cas.client.validation.Cas20ProxyTicketValidator"> <constructor-arg value="https://localhost:9443/cas"/> <property name="proxyCallbackUrl" value="https://localhost:8443/cas-sample/j_spring_cas_security_proxyreceptor"/> <property name="proxyGrantingTicketStorage" ref="pgtStorage"/> </bean> </property> </bean>
The last step is to update the CasAuthenticationFilter
to accept PGT and to store them
in the ProxyGrantingTicketStorage
. It is important the the proxyReceptorUrl
matches the proxyCallbackUrl
of the Cas20ProxyTicketValidator
. An example
configuration is shown below.
<bean id="casFilter" class="org.springframework.security.cas.web.CasAuthenticationFilter"> ... <property name="proxyGrantingTicketStorage" ref="pgtStorage"/> <property name="proxyReceptorUrl" value="/j_spring_cas_security_proxyreceptor"/> </bean>
Now that Spring Security obtains PGTs, you can use them to create proxy tickets which can be used to authenticate
to a stateless service. The CAS sample application contains a working example in
the ProxyTicketSampleServlet
. Example code can be found below:
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // NOTE: The CasAuthenticationToken can also be obtained using // SecurityContextHolder.getContext().getAuthentication() final CasAuthenticationToken token = (CasAuthenticationToken) request.getUserPrincipal(); // proxyTicket could be reused to make calls to the CAS service even if the // target url differs final String proxyTicket = token.getAssertion().getPrincipal().getProxyTicketFor(targetUrl); // Make a remote call using the proxy ticket final String serviceUrl = targetUrl+"?ticket="+URLEncoder.encode(proxyTicket, "UTF-8"); String proxyResponse = CommonUtils.getResponseFromServer(serviceUrl, "UTF-8"); ... }
The CasAuthenticationProvider
distinguishes
between stateful and stateless clients. A stateful client is
considered any that submits to the filterProcessUrl
of the
CasAuthenticationFilter
. A stateless client is any that
presents an authentication request to CasAuthenticationFilter
on a URL other than the filterProcessUrl
.
Because remoting protocols have no way of presenting themselves
within the context of an HttpSession
, it isn't
possible to rely on the default practice of storing the security context in the
session between requests. Furthermore, because the CAS server invalidates a
ticket after it has been validated by the TicketValidator
,
presenting the same proxy ticket on subsequent requests will not
work.
One obvious option is to not use CAS at all for remoting
protocol clients. However, this would eliminate many of the desirable
features of CAS. As a middle-ground, the
CasAuthenticationProvider
uses a
StatelessTicketCache
. This is used solely for stateless clients
which use a principal equal to
CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER
. What
happens is the CasAuthenticationProvider
will store
the resulting CasAuthenticationToken
in the
StatelessTicketCache
, keyed on the proxy ticket.
Accordingly, remoting protocol clients can present the same proxy
ticket and the CasAuthenticationProvider
will not
need to contact the CAS server for validation (aside from the first
request). Once authenticated, the proxy ticket could be used for URLs other than the
original target service.
This section builds upon the previous sections to accomodate proxy ticket authentication. The first step is to specify to authenticate all artifacts as shown below.
<bean id="serviceProperties" class="org.springframework.security.cas.ServiceProperties"> ... <property name="authenticateAllArtifacts" value="true"/> </bean>
The next step is to specify serviceProperties
and the
authenticationDetailsSource
for the CasAuthenticationFilter
.
The serviceProperties
property instructs the
CasAuthenticationFilter
to attempt to authenticate all artifacts instead of only
ones present on the filterProcessUrl
. The
ServiceAuthenticationDetailsSource
creates a
ServiceAuthenticationDetails
that ensures the current URL, based
upon the HttpServletRequest
, is used as the service URL when validating the ticket.
The method for generating the service URL can be customized by injecting a custom
AuthenticationDetailsSource
that returns a custom
ServiceAuthenticationDetails
.
<bean id="casFilter" class="org.springframework.security.cas.web.CasAuthenticationFilter"> ... <property name="serviceProperties" ref="serviceProperties"/> <property name="authenticationDetailsSource"> <bean class= "org.springframework.security.cas.web.authentication.ServiceAuthenticationDetailsSource"> <constructor-arg ref="serviceProperties"/> </bean> </property> </bean>
You will also need to update the CasAuthenticationProvider
to handle proxy tickets.
To do this replace the Cas20ServiceTicketValidator
with a
Cas20ProxyTicketValidator
. You will need to configure the
statelessTicketCache
and which proxies you want to accept. You can find an example of the updates
required to accept all proxies below.
<bean id="casAuthenticationProvider" class="org.springframework.security.cas.authentication.CasAuthenticationProvider"> ... <property name="ticketValidator"> <bean class="org.jasig.cas.client.validation.Cas20ProxyTicketValidator"> <constructor-arg value="https://localhost:9443/cas"/> <property name="acceptAnyProxy" value="true"/> </bean> </property> <property name="statelessTicketCache"> <bean class="org.springframework.security.cas.authentication.EhCacheBasedTicketCache"> <property name="cache"> <bean class="net.sf.ehcache.Cache" init-method="initialise" destroy-method="dispose"> <constructor-arg value="casTickets"/> <constructor-arg value="50"/> <constructor-arg value="true"/> <constructor-arg value="false"/> <constructor-arg value="3600"/> <constructor-arg value="900"/> </bean> </property> </bean> </property> </bean>