SEC-1718: Update documentation and sample application to demonstrate how to use a PGT to authenticate to stateless services using a PT

This commit is contained in:
Rob Winch 2011-04-16 19:03:18 -05:00
parent abfa558c3c
commit 01fb4bdb6d
11 changed files with 256 additions and 21 deletions

View File

@ -125,8 +125,8 @@
included in the CAS client library. In the event the application needs to validate proxy tickets, the
<classname>Cas20ProxyTicketValidator</classname> is used. The
<interfacename>TicketValidator</interfacename> 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:
<literal>https://my.company.com/cas/proxyValidate?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Fj_spring_cas_security_check&amp;ticket=ST-0-ER94xMJmn6pha35CQRoZ&amp;pgtUrl=https://server3.company.com/webapp/casProxy/receptor</literal>.-->
validate the service ticket. It may also include a proxy callback URL, which is included in this example:
<literal>https://my.company.com/cas/proxyValidate?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Fj_spring_cas_security_check&amp;ticket=ST-0-ER94xMJmn6pha35CQRoZ&amp;pgtUrl=https://server3.company.com/webapp/j_spring_cas_security_proxyreceptor</literal>.
</para>
</listitem>
@ -134,11 +134,11 @@
<para>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
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.--></para>
included in the XML response.</para>
</listitem>
<!--
<listitem>
<para>[OPTIONAL] If the request to the CAS validation service included the proxy callback
URL (in the <literal>pgtUrl</literal> parameter), CAS will include a
@ -147,17 +147,17 @@
connection back to the <literal>pgtUrl</literal>. 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,
<literal>https://server3.company.com/webapp/casProxy/receptor?pgtIou=PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt&amp;pgtId=PGT-1-si9YkkHLrtACBo64rmsi3v2nf7cpCResXg5MpESZFArbaZiOKH</literal>.</para>
<literal>https://server3.company.com/webapp/j_spring_cas_security_proxyreceptor?pgtIou=PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt&amp;pgtId=PGT-1-si9YkkHLrtACBo64rmsi3v2nf7cpCResXg5MpESZFArbaZiOKH</literal>.</para>
</listitem>
-->
<listitem>
<para>The <classname>Cas20TicketValidator</classname> will parse the XML received from the
CAS server. It will return to the <classname>CasAuthenticationProvider</classname> a
<literal>TicketResponse</literal>, which includes the username (mandatory). <!--, proxy list
<literal>TicketResponse</literal>, which includes the username (mandatory), proxy list
(if any were involved), and proxy-granting ticket IOU (if the proxy callback was
requested). --></para>
requested).</para>
</listitem>
<!--
<listitem>
<para>Next <literal>CasAuthenticationProvider</literal> will call
a configured <literal>CasProxyDecider</literal>. The
@ -171,7 +171,7 @@
which allows a <literal>List</literal> of trusted proxies to be
provided.</para>
</listitem>
-->
<listitem>
<para><classname>CasAuthenticationProvider</classname> will next
request a <interfacename>AuthenticationUserDetailsService</interfacename> to load the
@ -219,9 +219,9 @@
<info>
<title>Service Ticket Authentication</title>
</info>
<para>This section describes how to setup Spring Security to authenticate Service Tickets. You will need
to add a <classname>ServiceProperties</classname> bean to your application context. This represents
your CAS service:</para>
<para>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 <classname>ServiceProperties</classname>
bean to your application context. This represents your CAS service:</para>
<para> <programlisting language="xml"><![CDATA[
<bean id="serviceProperties"
class="org.springframework.security.cas.ServiceProperties">
@ -384,6 +384,89 @@
when an <interfacename>HttpSession</interfacename> expires, the mapping used for single logout is
removed.</para>
</section>
<section xml:id="cas-pt-client">
<info>
<title>Authenticating to a Stateless Service with CAS</title>
</info>
<para>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.</para>
<section xml:id="cas-pt-client-config">
<info>
<title>Configuring CAS to Obtain Proxy Granting Tickets</title>
</info>
<para>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
<link xlink:href="cas-st">Service Ticket Authentication</link> configuration.</para>
<para>The first step is to include a <classname>ProxyGrantingTicketStorage</classname> in your Spring Security
configuration. This is used to store PGT's that are obtained by the
<classname>CasAuthenticationFilter</classname> so that they can be used to obtain proxy tickets. An example
configuration is shown below <programlisting language="xml"><![CDATA[
<!--
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"/>
]]></programlisting></para>
<para>The next step is to update the <classname>CasAuthenticationProvider</classname> to be able to obtain proxy
tickets. To do this replace the <classname>Cas20ServiceTicketValidator</classname> with a
<classname>Cas20ProxyTicketValidator</classname>. The <literal>proxyCallbackUrl</literal> should be set to
a URL that the application will receive PGT's at. Last, the configuration should also reference the
<classname>ProxyGrantingTicketStorage</classname> so it can use a PGT to obtain proxy tickets.
You can find an example of the configuration changes that should be made below.
<programlisting language="xml"><![CDATA[
<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>
]]></programlisting></para>
<para>The last step is to update the <classname>CasAuthenticationFilter</classname> to accept PGT and to store them
in the <classname>ProxyGrantingTicketStorage</classname>. It is important the the <literal>proxyReceptorUrl</literal>
matches the <literal>proxyCallbackUrl</literal> of the <classname>Cas20ProxyTicketValidator</classname>. An example
configuration is shown below.
<programlisting language="xml"><![CDATA[
<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>
]]></programlisting></para>
</section>
<section xml:id="cas-pt-client-sample">
<info>
<title>Calling a Stateless Service Using a Proxy Ticket</title>
</info>
<para>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 <link xlink:href="#cas-sample">CAS sample application</link> contains a working example in
the <classname>ProxyTicketSampleServlet</classname>. Example code can be found below:
<programlisting language="xml"><![CDATA[
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 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");
...
}
]]></programlisting></para>
</section>
</section>
<section xml:id="cas-pt">
<info>
<title>Proxy Ticket Authentication</title>

View File

@ -33,8 +33,13 @@ dependencies {
casServer "org.jasig.cas:cas-server-webapp:3.4.3.1@war"
runtime project(':spring-security-web'),
providedCompile 'javax.servlet:servlet-api:2.5@jar'
compile project(':spring-security-core'),
project(':spring-security-cas'),
"org.jasig.cas.client:cas-client-core:3.1.12"
runtime project(':spring-security-web'),
project(':spring-security-config'),
"org.slf4j:jcl-over-slf4j:$slf4jVersion",
"ch.qos.logback:logback-classic:$logbackVersion"

View File

@ -54,6 +54,13 @@ class CasSampleProxySpec extends BaseSpec {
content.contains('<h1>Secure Page</h1>')
}
def 'access proxy ticket sample succeeds with ROLE_USER'() {
when: 'a proxy ticket is used to create another proxy ticket'
def content = getSecured(getBaseUrl()+ProxyTicketSamplePage.url).responseBodyAsString
then: 'The proxy ticket sample page is returned'
content.contains('<h1>Secure Page using a Proxy Ticket</h1>')
}
def 'access extremely secure page with ROLE_USER is denied'() {
when: 'User with ROLE_USER accesses the extremely secure page'
GetMethod method = getSecured(getBaseUrl()+ExtremelySecurePage.url)

View File

@ -69,6 +69,13 @@ class CasSampleSpec extends BaseSpec {
at SecurePage
}
def 'access proxy ticket sample with ROLE_USER is allowed'() {
when: 'user with ROLE_USER requests the proxy ticket sample page'
to ProxyTicketSamplePage
then: 'the proxy ticket sample page is displayed'
at ProxyTicketSamplePage
}
def 'access extremely secure page with ROLE_USER is denied'() {
when: 'User with ROLE_USER accesses extremely secure page'
to ExtremelySecurePage

View File

@ -0,0 +1,33 @@
/*
* Copyright 2011 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.samples.cas.pages;
import geb.*
import org.springframework.security.samples.cas.modules.*
/**
* Represents the proxy ticket sample page within the CAS Sample application.
*
* @author Rob Winch
*/
class ProxyTicketSamplePage extends Page {
static url = "secure/ptSample"
static at = { assert $('h1').text() == 'Secure Page using a Proxy Ticket'; true}
static content = {
navModule { module NavModule }
}
}

View File

@ -0,0 +1,79 @@
/*
* Copyright 2011 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.samples.cas.web;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URLEncoder;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.jasig.cas.client.util.CommonUtils;
import org.springframework.security.cas.authentication.CasAuthenticationToken;
/**
* <p>
* {@link ProxyTicketSampleServlet} demonstrates how to obtain a proxy ticket
* and then use it to make a remote call. To learn how proxy tickets work, see
* the <a href="https://wiki.jasig.org/display/CAS/Proxy+CAS+Walkthrough">Proxy
* CAS Walkthrough</a>
* </p>
*
* @author Rob Winch
*/
public final class ProxyTicketSampleServlet extends HttpServlet {
/**
* This is the URL that will be called and authenticate a proxy ticket.
*/
private String targetUrl;
@Override
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 to the CAS service even if the target url differs
final String proxyTicket = token.getAssertion().getPrincipal().getProxyTicketFor(targetUrl);
// Make a remote call to ourself. This is a bit silly, but it works well to demonstrate how to use proxy tickets.
final String serviceUrl = targetUrl+"?ticket="+URLEncoder.encode(proxyTicket, "UTF-8");
String proxyResponse = CommonUtils.getResponseFromServer(serviceUrl, "UTF-8");
// modify the response and write it out to inform the user that it was obtained using a proxy ticket.
proxyResponse = proxyResponse.replaceFirst("Secure Page", "Secure Page using a Proxy Ticket");
proxyResponse = proxyResponse.replaceFirst("<p>",
"<p>This page is rendered by "+getClass().getSimpleName()+" by making a remote call to the Secure Page using a proxy ticket ("+proxyTicket+") and inserts this message. ");
final PrintWriter writer = response.getWriter();
writer.write(proxyResponse);
}
/**
* Initialize the target URL. It allows for the host to change based upon
* the "cas.service.host" system property. If the property is not set, the
* default is "localhost:8443".
*/
@Override
public void init() throws ServletException {
super.init();
String casServiceHost = System.getProperty("cas.service.host", "localhost:8443");
targetUrl = "https://"+casServiceHost+"/cas-sample/secure/";
}
private static final long serialVersionUID = -7720161771819727775L;
}

View File

@ -57,7 +57,9 @@
<b:bean id="casFilter"
class="org.springframework.security.cas.web.CasAuthenticationFilter"
p:authenticationManager-ref="authManager"
p:serviceProperties-ref="serviceProperties">
p:serviceProperties-ref="serviceProperties"
p:proxyGrantingTicketStorage-ref="pgtStorage"
p:proxyReceptorUrl="/j_spring_cas_security_proxyreceptor">
<b:property name="authenticationDetailsSource">
<b:bean class="org.springframework.security.cas.web.authentication.ServiceAuthenticationDetailsSource"/>
</b:property>
@ -66,6 +68,11 @@
p:defaultFailureUrl="/casfailed.jsp"/>
</b:property>
</b:bean>
<!--
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()
-->
<b:bean id="pgtStorage" class="org.jasig.cas.client.proxy.ProxyGrantingTicketStorageImpl"/>
<b:bean id="casAuthProvider" class="org.springframework.security.cas.authentication.CasAuthenticationProvider"
p:serviceProperties-ref="serviceProperties"
p:key="casAuthProviderKey">
@ -78,7 +85,9 @@
<b:property name="ticketValidator">
<b:bean
class="org.jasig.cas.client.validation.Cas20ProxyTicketValidator"
p:acceptAnyProxy="true">
p:acceptAnyProxy="true"
p:proxyCallbackUrl="https://${cas.service.host}/cas-sample/j_spring_cas_security_proxyreceptor"
p:proxyGrantingTicketStorage-ref="pgtStorage">
<b:constructor-arg value="https://${cas.server.host}/cas" />
</b:bean>
</b:property>

View File

@ -69,6 +69,15 @@
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>ptSampleServlet</servlet-name>
<servlet-class>org.springframework.security.samples.cas.web.ProxyTicketSampleServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ptSampleServlet</servlet-name>
<url-pattern>/secure/ptSample</url-pattern>
</servlet-mapping>
<error-page>
<error-code>403</error-code>
<location>/403.jsp</location>

View File

@ -6,6 +6,7 @@
<p>Your principal object is....: <%= request.getUserPrincipal() %></p>
<p><a href="secure/index.jsp">Secure page</a></p>
<p><a href="secure/ptSample">Proxy Ticket Sample page</a></p>
<p><a href="secure/extreme/index.jsp">Extremely secure page</a></p>
</body>
</html>

View File

@ -4,6 +4,8 @@
This is a protected page. You can only see me if you are a supervisor.
<p><a href="../../">Home</a>
<p><a href="../../secure/index.jsp">Secure page</a></p>
<p><a href="../../secure/ptSample">Proxy Ticket Sample page</a></p>
<p><a href="../../j_spring_security_logout">Logout</a>
</body>
</html>

View File

@ -1,15 +1,15 @@
<html>
<body>
<h1>Secure Page</h1>
This is a protected page. You can get to me if you've been remembered,
or if you've authenticated this session.<br><br>
<p>This is a protected page. You can get to me if you've been remembered,
or if you've authenticated this session.</p>
<%if (request.isUserInRole("ROLE_SUPERVISOR")) { %>
You are a supervisor! You can therefore see the <a href="extreme/index.jsp">extremely secure page</a>.<br><br>
<p>You are a supervisor! You can therefore see the <a href="extreme/index.jsp">extremely secure page</a>.</p>
<% } %>
<p><a href="../">Home</a>
<p><a href="ptSample">Proxy Ticket Sample page</a></p>
<p><a href="../j_spring_security_logout">Logout</a>
</body>
</html>