From 01fb4bdb6d8987c9d0521e30031e6396d9aca9ae Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Sat, 16 Apr 2011 19:03:18 -0500 Subject: [PATCH] SEC-1718: Update documentation and sample application to demonstrate how to use a PGT to authenticate to stateless services using a PT --- docs/manual/src/docbook/cas-auth-provider.xml | 111 +++++++++++++++--- samples/cas/cas.gradle | 7 +- .../samples/cas/CasSampleProxySpec.groovy | 7 ++ .../security/samples/cas/CasSampleSpec.groovy | 7 ++ .../cas/pages/ProxyTicketSamplePage.groovy | 33 ++++++ .../cas/web/ProxyTicketSampleServlet.java | 79 +++++++++++++ .../WEB-INF/applicationContext-security.xml | 13 +- samples/cas/src/main/webapp/WEB-INF/web.xml | 9 ++ samples/cas/src/main/webapp/index.jsp | 1 + .../src/main/webapp/secure/extreme/index.jsp | 2 + samples/cas/src/main/webapp/secure/index.jsp | 8 +- 11 files changed, 256 insertions(+), 21 deletions(-) create mode 100644 samples/cas/src/integration-test/groovy/org/springframework/security/samples/cas/pages/ProxyTicketSamplePage.groovy create mode 100644 samples/cas/src/main/java/org/springframework/security/samples/cas/web/ProxyTicketSampleServlet.java diff --git a/docs/manual/src/docbook/cas-auth-provider.xml b/docs/manual/src/docbook/cas-auth-provider.xml index 04f01e7be8..3665811066 100644 --- a/docs/manual/src/docbook/cas-auth-provider.xml +++ b/docs/manual/src/docbook/cas-auth-provider.xml @@ -125,8 +125,8 @@ 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. + 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. @@ -134,11 +134,11 @@ 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. + included in the XML response. - + The Cas20TicketValidator will parse the XML received from the CAS server. It will return to the CasAuthenticationProvider a - TicketResponse, which includes the username (mandatory). + requested). - + CasAuthenticationProvider will next request a AuthenticationUserDetailsService to load the @@ -219,9 +219,9 @@ Service Ticket Authentication - This section describes how to setup Spring Security to authenticate Service Tickets. You will need - to add a ServiceProperties bean to your application context. This represents - your CAS service: + 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: @@ -384,6 +384,89 @@ when an HttpSession expires, the mapping used for single logout is removed. +
+ + Authenticating to a Stateless Service with CAS + + 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. +
+ + Configuring CAS to Obtain Proxy Granting Tickets + + 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 + +]]> + 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. + + ... + + + + + + + + + +]]> + 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. + + ... + + + +]]> +
+
+ + Calling a Stateless Service Using a Proxy Ticket + + 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: + +
+
Proxy Ticket Authentication diff --git a/samples/cas/cas.gradle b/samples/cas/cas.gradle index 230f2221b5..1715a7bb06 100644 --- a/samples/cas/cas.gradle +++ b/samples/cas/cas.gradle @@ -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" diff --git a/samples/cas/src/integration-test/groovy/org/springframework/security/samples/cas/CasSampleProxySpec.groovy b/samples/cas/src/integration-test/groovy/org/springframework/security/samples/cas/CasSampleProxySpec.groovy index ec92ec3ac4..a7ffe6c1a3 100644 --- a/samples/cas/src/integration-test/groovy/org/springframework/security/samples/cas/CasSampleProxySpec.groovy +++ b/samples/cas/src/integration-test/groovy/org/springframework/security/samples/cas/CasSampleProxySpec.groovy @@ -54,6 +54,13 @@ class CasSampleProxySpec extends BaseSpec { content.contains('

Secure Page

') } + 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('

Secure Page using a Proxy Ticket

') + } + 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) diff --git a/samples/cas/src/integration-test/groovy/org/springframework/security/samples/cas/CasSampleSpec.groovy b/samples/cas/src/integration-test/groovy/org/springframework/security/samples/cas/CasSampleSpec.groovy index 78ddc7621f..e71df91190 100644 --- a/samples/cas/src/integration-test/groovy/org/springframework/security/samples/cas/CasSampleSpec.groovy +++ b/samples/cas/src/integration-test/groovy/org/springframework/security/samples/cas/CasSampleSpec.groovy @@ -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 diff --git a/samples/cas/src/integration-test/groovy/org/springframework/security/samples/cas/pages/ProxyTicketSamplePage.groovy b/samples/cas/src/integration-test/groovy/org/springframework/security/samples/cas/pages/ProxyTicketSamplePage.groovy new file mode 100644 index 0000000000..f9cd98657c --- /dev/null +++ b/samples/cas/src/integration-test/groovy/org/springframework/security/samples/cas/pages/ProxyTicketSamplePage.groovy @@ -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 } + } +} \ No newline at end of file diff --git a/samples/cas/src/main/java/org/springframework/security/samples/cas/web/ProxyTicketSampleServlet.java b/samples/cas/src/main/java/org/springframework/security/samples/cas/web/ProxyTicketSampleServlet.java new file mode 100644 index 0000000000..e3e0c3bc0c --- /dev/null +++ b/samples/cas/src/main/java/org/springframework/security/samples/cas/web/ProxyTicketSampleServlet.java @@ -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; + +/** + *

+ * {@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 Proxy + * CAS Walkthrough + *

+ * + * @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("

", + "

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; +} diff --git a/samples/cas/src/main/webapp/WEB-INF/applicationContext-security.xml b/samples/cas/src/main/webapp/WEB-INF/applicationContext-security.xml index 78d58960cf..3fc6a6495d 100644 --- a/samples/cas/src/main/webapp/WEB-INF/applicationContext-security.xml +++ b/samples/cas/src/main/webapp/WEB-INF/applicationContext-security.xml @@ -57,7 +57,9 @@ + p:serviceProperties-ref="serviceProperties" + p:proxyGrantingTicketStorage-ref="pgtStorage" + p:proxyReceptorUrl="/j_spring_cas_security_proxyreceptor"> @@ -66,6 +68,11 @@ p:defaultFailureUrl="/casfailed.jsp"/> + + @@ -78,7 +85,9 @@ + p:acceptAnyProxy="true" + p:proxyCallbackUrl="https://${cas.service.host}/cas-sample/j_spring_cas_security_proxyreceptor" + p:proxyGrantingTicketStorage-ref="pgtStorage"> diff --git a/samples/cas/src/main/webapp/WEB-INF/web.xml b/samples/cas/src/main/webapp/WEB-INF/web.xml index 3675fa76df..0c45c39474 100644 --- a/samples/cas/src/main/webapp/WEB-INF/web.xml +++ b/samples/cas/src/main/webapp/WEB-INF/web.xml @@ -69,6 +69,15 @@ org.springframework.web.context.ContextLoaderListener + + ptSampleServlet + org.springframework.security.samples.cas.web.ProxyTicketSampleServlet + + + + ptSampleServlet + /secure/ptSample + 403 /403.jsp diff --git a/samples/cas/src/main/webapp/index.jsp b/samples/cas/src/main/webapp/index.jsp index c815d484a3..546d5996d7 100644 --- a/samples/cas/src/main/webapp/index.jsp +++ b/samples/cas/src/main/webapp/index.jsp @@ -6,6 +6,7 @@

Your principal object is....: <%= request.getUserPrincipal() %>

Secure page

+

Proxy Ticket Sample page

Extremely secure page

\ No newline at end of file diff --git a/samples/cas/src/main/webapp/secure/extreme/index.jsp b/samples/cas/src/main/webapp/secure/extreme/index.jsp index c5ff40bfef..b67a000319 100644 --- a/samples/cas/src/main/webapp/secure/extreme/index.jsp +++ b/samples/cas/src/main/webapp/secure/extreme/index.jsp @@ -4,6 +4,8 @@ This is a protected page. You can only see me if you are a supervisor.

Home +

Secure page

+

Proxy Ticket Sample page

Logout \ No newline at end of file diff --git a/samples/cas/src/main/webapp/secure/index.jsp b/samples/cas/src/main/webapp/secure/index.jsp index 97d9b216d4..3ba0fec58f 100644 --- a/samples/cas/src/main/webapp/secure/index.jsp +++ b/samples/cas/src/main/webapp/secure/index.jsp @@ -1,15 +1,15 @@

Secure Page

-This is a protected page. You can get to me if you've been remembered, -or if you've authenticated this session.

+

This is a protected page. You can get to me if you've been remembered, +or if you've authenticated this session.

<%if (request.isUserInRole("ROLE_SUPERVISOR")) { %> - You are a supervisor! You can therefore see the extremely secure page.

+

You are a supervisor! You can therefore see the extremely secure page.

<% } %> -

Home +

Proxy Ticket Sample page

Logout \ No newline at end of file