From 53dbb488820a38000421954734f2cbe5a3f2109b Mon Sep 17 00:00:00 2001 From: "Marvin S. Addison" Date: Thu, 11 Feb 2016 14:53:14 -0500 Subject: [PATCH 01/10] Issue #152 Jetty container-based authn. --- .../jasig/cas/client/util/ReflectUtils.java | 32 +++ .../cas/client/util/ReflectUtilsTests.java | 24 ++ cas-client-integration-jetty/pom.xml | 106 ++++++++ .../cas/client/jetty/CasAuthentication.java | 45 ++++ .../cas/client/jetty/CasAuthenticator.java | 249 ++++++++++++++++++ .../cas/client/jetty/CasUserIdentity.java | 66 +++++ .../client/jetty/CasAuthenticatorTest.java | 210 +++++++++++++++ .../src/test/resources/jetty/webapp.xml | 14 + .../src/test/webapp/index.jsp | 9 + .../src/test/webapp/secure.jsp | 9 + pom.xml | 1 + 11 files changed, 765 insertions(+) create mode 100644 cas-client-integration-jetty/pom.xml create mode 100644 cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthentication.java create mode 100644 cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthenticator.java create mode 100644 cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasUserIdentity.java create mode 100644 cas-client-integration-jetty/src/test/java/org/jasig/cas/client/jetty/CasAuthenticatorTest.java create mode 100644 cas-client-integration-jetty/src/test/resources/jetty/webapp.xml create mode 100644 cas-client-integration-jetty/src/test/webapp/index.jsp create mode 100644 cas-client-integration-jetty/src/test/webapp/secure.jsp diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/util/ReflectUtils.java b/cas-client-core/src/main/java/org/jasig/cas/client/util/ReflectUtils.java index 1ec2116..8759593 100644 --- a/cas-client-core/src/main/java/org/jasig/cas/client/util/ReflectUtils.java +++ b/cas-client-core/src/main/java/org/jasig/cas/client/util/ReflectUtils.java @@ -22,6 +22,7 @@ import java.beans.BeanInfo; import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.PropertyDescriptor; +import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; /** @@ -148,4 +149,35 @@ public final class ReflectUtils { throw new RuntimeException("Error setting property " + propertyName, e); } } + + /** + * Gets the value of the given declared field on the target object or any of its superclasses. + * + * @param fieldName Name of field to get. + * @param target Target object that possesses field. + * + * @return Field value. + */ + public static Object getField(final String fieldName, final Object target) { + Class clazz = target.getClass(); + Field field = null; + do { + try { + field = clazz.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + clazz = clazz.getSuperclass(); + } + } while (field == null && clazz != null); + if (field == null) { + throw new IllegalArgumentException(fieldName + " does not exist on " + target); + } + try { + if (!field.isAccessible()) { + field.setAccessible(true); + } + return field.get(target); + } catch (Exception e) { + throw new IllegalArgumentException("Error getting field " + fieldName, e); + } + } } diff --git a/cas-client-core/src/test/java/org/jasig/cas/client/util/ReflectUtilsTests.java b/cas-client-core/src/test/java/org/jasig/cas/client/util/ReflectUtilsTests.java index d39e3bb..57d741c 100644 --- a/cas-client-core/src/test/java/org/jasig/cas/client/util/ReflectUtilsTests.java +++ b/cas-client-core/src/test/java/org/jasig/cas/client/util/ReflectUtilsTests.java @@ -54,6 +54,18 @@ public class ReflectUtilsTests extends TestCase { assertTrue(bean.isFlag()); } + public void testGetField() { + final TestBean bean = new TestBean(); + bean.setName("bob"); + assertEquals(bean.getName(), ReflectUtils.getField("name", bean)); + } + + public void testGetFieldSuperclass() { + final TestSubBean bean = new TestSubBean(); + bean.setName("bob"); + assertEquals(bean.getName(), ReflectUtils.getField("name", bean)); + } + static class TestBean { private int count; private boolean flag; @@ -102,4 +114,16 @@ public class ReflectUtilsTests extends TestCase { } } + + static class TestSubBean extends TestBean { + private String state; + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + } } diff --git a/cas-client-integration-jetty/pom.xml b/cas-client-integration-jetty/pom.xml new file mode 100644 index 0000000..96a28ac --- /dev/null +++ b/cas-client-integration-jetty/pom.xml @@ -0,0 +1,106 @@ + + + + cas-client + org.jasig.cas.client + 3.4.2-SNAPSHOT + + 4.0.0 + + org.jasig.cas.client + cas-client-integration-jetty + jar + Jasig CAS Client for Java - Jetty Container Integration + + + 9.2.14.v20151106 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.jasig.cas.client + cas-client-core + ${project.version} + + + org.eclipse.jetty + jetty-security + ${jetty.version} + + + + + org.jasig.cas.client + cas-client-core + ${project.version} + test-jar + test + + + org.eclipse.jetty + jetty-webapp + ${jetty.version} + test + + + org.eclipse.jetty + jetty-plus + ${jetty.version} + test + + + org.eclipse.jetty + jetty-annotations + ${jetty.version} + test + + + org.eclipse.jetty + apache-jsp + ${jetty.version} + test + + + javax.servlet + javax.servlet-api + 3.1.0 + provided + + + + + diff --git a/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthentication.java b/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthentication.java new file mode 100644 index 0000000..0b8fb2a --- /dev/null +++ b/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthentication.java @@ -0,0 +1,45 @@ +package org.jasig.cas.client.jetty; + +import org.eclipse.jetty.security.UserAuthentication; +import org.jasig.cas.client.validation.Assertion; + +/** + * CAS-specific user authentication. + * + * @author Marvin S. Addison + */ +public class CasAuthentication extends UserAuthentication { + + /** CAS authenticator that produced this authentication. */ + private final CasAuthenticator authenticator; + + /** CAS ticket that was successfully validated to permit authentication. */ + private final String ticket; + + + /** + * Creates a new instance. + * + * @param authenticator The authenticator that produced this authentication. + * @param ticket The CAS ticket that was successfully validated to permit authentication. + * @param assertion The CAS assertion produced from successful ticket validation. + */ + public CasAuthentication(final CasAuthenticator authenticator, final String ticket, final Assertion assertion) { + super(authenticator.getAuthMethod(), new CasUserIdentity(assertion, authenticator.getRoleAttribute())); + assert ticket != null : "Ticket cannot be null"; + assert authenticator != null : "CasAuthenticator cannot be null"; + this.authenticator = authenticator; + this.ticket = ticket; + } + + /** @return The CAS ticket that was successfully validated to permit authentication. */ + public String getTicket() { + return ticket; + } + + @Override + public void logout() { + super.logout(); + this.authenticator.clearCachedAuthentication(ticket); + } +} diff --git a/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthenticator.java b/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthenticator.java new file mode 100644 index 0000000..01c2c37 --- /dev/null +++ b/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthenticator.java @@ -0,0 +1,249 @@ +/* + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you 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 the following location: + * + * 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.jasig.cas.client.jetty; + +import org.eclipse.jetty.security.Authenticator; +import org.eclipse.jetty.security.ServerAuthException; +import org.eclipse.jetty.server.Authentication; +import org.eclipse.jetty.util.component.AbstractLifeCycle; +import org.jasig.cas.client.Protocol; +import org.jasig.cas.client.util.CommonUtils; +import org.jasig.cas.client.util.ReflectUtils; +import org.jasig.cas.client.validation.AbstractCasProtocolUrlBasedTicketValidator; +import org.jasig.cas.client.validation.AbstractUrlBasedTicketValidator; +import org.jasig.cas.client.validation.Assertion; +import org.jasig.cas.client.validation.TicketValidator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.IOException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * Jetty authenticator component for container-managed CAS authentication. + *

NOTE: This component does not support CAS gateway mode.

+ * + * @author Marvin S. Addison + * @since 3.4.2 + */ +public class CasAuthenticator extends AbstractLifeCycle implements Authenticator { + + /** Name of authentication method provided by this authenticator. */ + public static final String AUTH_METHOD = "CAS"; + + /** Session attribute used to cache CAS authentication data. */ + private static final String CACHED_AUTHN_ATTRIBUTE = "org.jasig.cas.client.jetty.Authentication"; + + /** Logger instance. */ + private final Logger logger = LoggerFactory.getLogger(CasAuthenticator.class); + + /** Map of tickets to sessions. */ + private final ConcurrentMap sessionMap = new ConcurrentHashMap(); + + /** CAS ticket validator component. */ + private TicketValidator ticketValidator; + + /** Space-delimited ist of server names. */ + private String serverNames; + + /** CAS principal attribute containing role data. */ + private String roleAttribute; + + /** URL to /login URI on CAS server. */ + private String casServerLoginUrl; + + /** Protocol used by ticket validator. */ + private Protocol protocol; + + /** CAS renew parameter. */ + private boolean renew; + + + /** + * Sets the CAS ticket validator component. + * + * @param ticketValidator Ticket validator, MUST NOT be null. + */ + public void setTicketValidator(final TicketValidator ticketValidator) { + assert ticketValidator != null : "TicketValidator cannot be null"; + if (ticketValidator instanceof AbstractUrlBasedTicketValidator) { + if (ticketValidator instanceof AbstractCasProtocolUrlBasedTicketValidator) { + protocol = Protocol.CAS2; + } else { + protocol = Protocol.SAML11; + } + casServerLoginUrl = ReflectUtils.getField("casServerUrlPrefix", ticketValidator) + "/login"; + renew = (Boolean) ReflectUtils.getField("renew", ticketValidator); + } else { + throw new IllegalArgumentException("Unsupported ticket validator " + ticketValidator); + } + this.ticketValidator = ticketValidator; + } + + /** + * Sets the names of the server host running Jetty. + * + * @param nameList Space-delimited list of one or more server names, e.g. "www1.example.com www2.example.com". + * MUST NOT be blank. + */ + public void setServerNames(final String nameList) { + CommonUtils.isNotBlank(nameList); + this.serverNames = nameList; + } + + /** @return The name of the CAS principal attribute that contains role data. */ + public String getRoleAttribute() { + return roleAttribute; + } + + /** + * Sets the name of the CAS principal attribute that contains role data. + * + * @param roleAttribute Role attribute name. MUST NOT be blank. + */ + public void setRoleAttribute(final String roleAttribute) { + CommonUtils.isNotBlank(roleAttribute); + this.roleAttribute = roleAttribute; + } + + @Override + public void setConfiguration(final AuthConfiguration configuration) { + // Nothing to do + // All configuration must be via CAS-specific setter methods + } + + @Override + public String getAuthMethod() { + return AUTH_METHOD; + } + + @Override + public void prepareRequest(final ServletRequest request) { + // Nothing to do + } + + @Override + public Authentication validateRequest( + final ServletRequest servletRequest, final ServletResponse servletResponse, final boolean mandatory) + throws ServerAuthException { + + final HttpServletRequest request = (HttpServletRequest) servletRequest; + final HttpServletResponse response = (HttpServletResponse) servletResponse; + + CasAuthentication authentication = fetchCachedAuthentication(request); + if (!mandatory) { + if (authentication != null) { + return authentication; + } + return Authentication.UNAUTHENTICATED; + } + + String ticket; + for (final Protocol protocol : Protocol.values()) { + ticket = request.getParameter(protocol.getArtifactParameterName()); + if (ticket != null) { + try { + logger.debug("Attempting to validate {}", ticket); + final Assertion assertion = ticketValidator.validate(ticket, serviceUrl(request, response)); + logger.debug("Successfully authenticated {}", assertion.getPrincipal()); + authentication = new CasAuthentication(this, ticket, assertion); + cacheAuthentication(request, authentication); + } catch (Exception e) { + throw new ServerAuthException("CAS ticket validation failed", e); + } + } + } + + if (authentication != null) { + return authentication; + } + redirectToCas(request, response); + return Authentication.SEND_CONTINUE; + } + + @Override + public boolean secureResponse( + final ServletRequest request, + final ServletResponse response, + final boolean mandatory, + final Authentication.User user) throws ServerAuthException { + return true; + } + + @Override + protected void doStart() throws Exception { + if (ticketValidator == null) { + throw new RuntimeException("TicketValidator cannot be null"); + } + if (serverNames == null) { + throw new RuntimeException("ServerNames cannot be null"); + } + } + + protected void clearCachedAuthentication(final String ticket) { + sessionMap.remove(ticket); + } + + private void cacheAuthentication(final HttpServletRequest request, final CasAuthentication authentication) { + final HttpSession session = request.getSession(false); + if (session != null) { + session.setAttribute(CACHED_AUTHN_ATTRIBUTE, authentication); + sessionMap.put(authentication.getTicket(), session); + } + } + + private CasAuthentication fetchCachedAuthentication(final HttpServletRequest request) { + final HttpSession session = request.getSession(false); + if (session != null) { + return (CasAuthentication) session.getAttribute(CACHED_AUTHN_ATTRIBUTE); + } + return null; + } + + private String serviceUrl(final HttpServletRequest request, final HttpServletResponse response) { + return CommonUtils.constructServiceUrl( + request, + response, + null, + serverNames, + protocol.getServiceParameterName(), + protocol.getArtifactParameterName(), + true); + } + + private void redirectToCas( + final HttpServletRequest request, final HttpServletResponse response) throws ServerAuthException { + try { + final String redirectUrl = CommonUtils.constructRedirectUrl( + casServerLoginUrl, protocol.getServiceParameterName(), serviceUrl(request, response), renew, false); + logger.debug("Redirecting to {}", redirectUrl); + response.sendRedirect(redirectUrl); + } catch (IOException e) { + logger.debug("Redirect to CAS failed with error: {}", e); + throw new ServerAuthException("Redirect to CAS failed", e); + } + } + +} diff --git a/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasUserIdentity.java b/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasUserIdentity.java new file mode 100644 index 0000000..200e626 --- /dev/null +++ b/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasUserIdentity.java @@ -0,0 +1,66 @@ +package org.jasig.cas.client.jetty; + +import org.eclipse.jetty.server.UserIdentity; +import org.jasig.cas.client.authentication.AttributePrincipal; +import org.jasig.cas.client.validation.Assertion; + +import javax.security.auth.Subject; +import java.security.Principal; +import java.util.Collection; + +/** + * CAS user identity backed by assertion data. + * + * @author Marvin S. Addison + */ +public class CasUserIdentity implements UserIdentity { + + /** CAS principal. */ + private AttributePrincipal principal; + + /** Assertion attribute containing role data. */ + private String roleAttribute; + + + /** + * Creates a new instance from a CAS assertion containing principal information. + * + * @param assertion CAS assertion resulting from successful ticket validation. + * @param roleAttribute Principal attribute containing role data. + */ + public CasUserIdentity(final Assertion assertion, final String roleAttribute) { + assert assertion != null : "Assertion cannot be null"; + this.principal = assertion.getPrincipal(); + this.roleAttribute = roleAttribute; + } + + @Override + public Subject getSubject() { + final Subject subject = new Subject(); + subject.getPrincipals().add(principal); + return subject; + } + + @Override + public Principal getUserPrincipal() { + return principal; + } + + @Override + public boolean isUserInRole(final String role, final Scope scope) { + if (roleAttribute != null) { + final Object value = principal.getAttributes().get(roleAttribute); + if (value instanceof Collection) { + return ((Collection) value).contains(role); + } else if (value instanceof String) { + return value.equals(role); + } + } + return false; + } + + @Override + public String toString() { + return principal.getName(); + } +} diff --git a/cas-client-integration-jetty/src/test/java/org/jasig/cas/client/jetty/CasAuthenticatorTest.java b/cas-client-integration-jetty/src/test/java/org/jasig/cas/client/jetty/CasAuthenticatorTest.java new file mode 100644 index 0000000..462f353 --- /dev/null +++ b/cas-client-integration-jetty/src/test/java/org/jasig/cas/client/jetty/CasAuthenticatorTest.java @@ -0,0 +1,210 @@ +package org.jasig.cas.client.jetty; + +import org.apache.tomcat.InstanceManager; +import org.apache.tomcat.SimpleInstanceManager; +import org.eclipse.jetty.annotations.ServletContainerInitializersStarter; +import org.eclipse.jetty.apache.jsp.JettyJasperInitializer; +import org.eclipse.jetty.jsp.JettyJspServlet; +import org.eclipse.jetty.plus.annotation.ContainerInitializer; +import org.eclipse.jetty.security.ConstraintMapping; +import org.eclipse.jetty.security.ConstraintSecurityHandler; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.util.security.Constraint; +import org.eclipse.jetty.webapp.WebAppContext; +import org.jasig.cas.client.PublicTestHttpServer; +import org.jasig.cas.client.validation.Cas20ServiceTicketValidator; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.nio.CharBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.*; + +/** + * Unit test for {@link CasAuthenticator}. + * + * @author Marvin S. Addison + */ +public class CasAuthenticatorTest { + + private static final Server server = new Server(8080); + private static final CasAuthenticator authenticator = new CasAuthenticator(); + + @BeforeClass + public static void beforeClass() throws Exception { + final WebAppContext context = new WebAppContext(); + context.setContextPath("/webapp"); + String workingDir = new File(".").getAbsolutePath(); + workingDir = workingDir.substring(0, workingDir.length() - 2); + final String webappDir; + if (workingDir.endsWith("/cas-client-integration-jetty")) { + webappDir = workingDir + "/src/test/webapp"; + } else { + webappDir = workingDir + "/cas-client-integration-jetty/src/test/webapp"; + } + context.setWar(webappDir); + + + // JSP config from https://github.com/jetty-project/embedded-jetty-jsp/ + System.setProperty("org.apache.jasper.compiler.disablejsr199", "false"); + context.setAttribute("javax.servlet.context.tempdir", getScratchDir()); + context.setAttribute("org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern", + ".*/[^/]*servlet-api-[^/]*\\.jar$|.*/javax.servlet.jsp.jstl-.*\\.jar$|.*/.*taglibs.*\\.jar$"); + context.setAttribute("org.eclipse.jetty.containerInitializers", jspInitializers()); + context.setAttribute(InstanceManager.class.getName(), new SimpleInstanceManager()); + context.addBean(new ServletContainerInitializersStarter(context), true); + context.addServlet(jspServletHolder(), "*.jsp"); + + // Wire up CAS authentication + authenticator.setServerNames("localhost:8080"); + authenticator.setTicketValidator(new Cas20ServiceTicketValidator("http://localhost:8081/cas")); + + // Configure security handling for webapp context + final ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler(); + final Constraint constraint = new Constraint("CasRealm", Constraint.ANY_AUTH); + constraint.setAuthenticate(true); + final ConstraintMapping secureMapping = new ConstraintMapping(); + secureMapping.setPathSpec("/secure.jsp"); + secureMapping.setConstraint(constraint); + securityHandler.addConstraintMapping(secureMapping); + securityHandler.setAuthenticator(authenticator); + context.setSecurityHandler(securityHandler); + + // Add webapp context and start the server + server.setHandler(context); + server.start(); + } + + @Test + public void testValidateRequestPublicPageNoTicket() throws Exception { + final HttpURLConnection uc = openConnection("http://localhost:8080/webapp/"); + try { + assertEquals(200, uc.getResponseCode()); + assertTrue(readOutput(uc).contains("Welcome everyone")); + } finally { + uc.disconnect(); + } + } + + @Test + public void testValidateRequestPublicPageWithTicket() throws Exception { + final HttpURLConnection uc = openConnection("http://localhost:8080/webapp/?ticket=ST-12345"); + try { + assertEquals(200, uc.getResponseCode()); + assertTrue(readOutput(uc).contains("Welcome everyone")); + } finally { + uc.disconnect(); + } + } + + @Test + public void testValidateRequestSecurePageNoTicket() throws Exception { + final HttpURLConnection uc = openConnection("http://localhost:8080/webapp/secure.jsp"); + try { + assertEquals(302, uc.getResponseCode()); + assertEquals( + "http://localhost:8081/cas/login?service=http%3A%2F%2Flocalhost%3A8080%2Fwebapp%2Fsecure.jsp", + uc.getHeaderField("Location")); + } finally { + uc.disconnect(); + } + } + + @Test + public void testValidateRequestSecurePageWithTicket() throws Exception { + final String successResponse = "" + + "" + + "bob" + + "" + + ""; + final PublicTestHttpServer server = PublicTestHttpServer.instance(8081); + server.content = successResponse.getBytes(StandardCharsets.UTF_8); + final HttpURLConnection uc = openConnection("http://localhost:8080/webapp/secure.jsp?ticket=ST-12345"); + try { + assertEquals(200, uc.getResponseCode()); + assertTrue(readOutput(uc).contains("Hello bob")); + } finally { + uc.disconnect(); + } + } + + @AfterClass + public static void afterClass() throws Exception { + server.stop(); + } + + private String readOutput(final URLConnection connection) throws IOException { + final InputStreamReader reader = new InputStreamReader(connection.getInputStream()); + final StringBuilder builder = new StringBuilder(); + final CharBuffer buffer = CharBuffer.allocate(1024); + try { + while (reader.read(buffer) > 0) { + builder.append(buffer.flip()); + buffer.clear(); + } + } finally { + reader.close(); + } + return builder.toString(); + } + + private static File getScratchDir() throws IOException + { + final File tempDir = new File(System.getProperty("java.io.tmpdir")); + final File scratchDir = new File(tempDir.toString(), "embedded-jetty-jsp"); + + if (!scratchDir.exists()) + { + if (!scratchDir.mkdirs()) + { + throw new IOException("Unable to create scratch directory: " + scratchDir); + } + } + return scratchDir; + } + + /** + * Ensure the jsp engine is initialized correctly + */ + private static List jspInitializers() + { + return Collections.singletonList(new ContainerInitializer(new JettyJasperInitializer(), null)); + } + + /** + * Create JSP Servlet (must be named "jsp") + */ + private static ServletHolder jspServletHolder() + { + final ServletHolder holderJsp = new ServletHolder("jsp", JettyJspServlet.class); + holderJsp.setInitOrder(0); + holderJsp.setInitParameter("logVerbosityLevel", "DEBUG"); + holderJsp.setInitParameter("fork", "false"); + holderJsp.setInitParameter("xpoweredBy", "false"); + holderJsp.setInitParameter("compilerTargetVM", "1.7"); + holderJsp.setInitParameter("compilerSourceVM", "1.7"); + holderJsp.setInitParameter("keepgenerated", "true"); + return holderJsp; + } + + private static HttpURLConnection openConnection(final String url) throws IOException { + final HttpURLConnection uc; + try { + uc = (HttpURLConnection) new URL(url).openConnection(); + } catch (IOException e) { + throw new RuntimeException("Invalid URL: " + url, e); + } + uc.setInstanceFollowRedirects(false); + uc.connect(); + return uc; + } +} \ No newline at end of file diff --git a/cas-client-integration-jetty/src/test/resources/jetty/webapp.xml b/cas-client-integration-jetty/src/test/resources/jetty/webapp.xml new file mode 100644 index 0000000..f3c4fc4 --- /dev/null +++ b/cas-client-integration-jetty/src/test/resources/jetty/webapp.xml @@ -0,0 +1,14 @@ + + + + + localhost:8080 + + + http://localhost:8081/cas + + + + + + \ No newline at end of file diff --git a/cas-client-integration-jetty/src/test/webapp/index.jsp b/cas-client-integration-jetty/src/test/webapp/index.jsp new file mode 100644 index 0000000..6d505bb --- /dev/null +++ b/cas-client-integration-jetty/src/test/webapp/index.jsp @@ -0,0 +1,9 @@ + + + + + Welcome Page + +

Welcome everyone

+ + \ No newline at end of file diff --git a/cas-client-integration-jetty/src/test/webapp/secure.jsp b/cas-client-integration-jetty/src/test/webapp/secure.jsp new file mode 100644 index 0000000..9add84e --- /dev/null +++ b/cas-client-integration-jetty/src/test/webapp/secure.jsp @@ -0,0 +1,9 @@ + + + + + Secure Page + +

Hello <%=request.getUserPrincipal()%>

+ + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 75886fa..99bfe01 100644 --- a/pom.xml +++ b/pom.xml @@ -252,6 +252,7 @@ cas-client-integration-tomcat-common cas-client-integration-tomcat-v6 cas-client-integration-tomcat-v7 + cas-client-integration-jetty From f04dedd9bb03575b6565d3c69b95d51b4da813d8 Mon Sep 17 00:00:00 2001 From: "Marvin S. Addison" Date: Thu, 11 Feb 2016 15:08:00 -0500 Subject: [PATCH 02/10] Issue #152 Use WeakReference to avoid resource leak. We don't want to hold a reference to HttpSession objects in the ticket-to-session mapping that would prevent orphaned or expired sessions from being purged. WeakReference ensures that won't happen. --- .../org/jasig/cas/client/jetty/CasAuthenticator.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthenticator.java b/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthenticator.java index 01c2c37..14b359b 100644 --- a/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthenticator.java +++ b/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthenticator.java @@ -38,6 +38,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; +import java.lang.ref.WeakReference; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -60,7 +61,8 @@ public class CasAuthenticator extends AbstractLifeCycle implements Authenticator private final Logger logger = LoggerFactory.getLogger(CasAuthenticator.class); /** Map of tickets to sessions. */ - private final ConcurrentMap sessionMap = new ConcurrentHashMap(); + private final ConcurrentMap> sessionMap = + new ConcurrentHashMap>(); /** CAS ticket validator component. */ private TicketValidator ticketValidator; @@ -203,14 +205,17 @@ public class CasAuthenticator extends AbstractLifeCycle implements Authenticator } protected void clearCachedAuthentication(final String ticket) { - sessionMap.remove(ticket); + final WeakReference sessionRef = sessionMap.remove(ticket); + if (sessionRef != null && sessionRef.get() != null) { + sessionRef.get().removeAttribute(CACHED_AUTHN_ATTRIBUTE); + } } private void cacheAuthentication(final HttpServletRequest request, final CasAuthentication authentication) { final HttpSession session = request.getSession(false); if (session != null) { session.setAttribute(CACHED_AUTHN_ATTRIBUTE, authentication); - sessionMap.put(authentication.getTicket(), session); + sessionMap.put(authentication.getTicket(), new WeakReference(session)); } } From 971d4b4854e58d56f257e1b6318654f951dca561 Mon Sep 17 00:00:00 2001 From: "Marvin S. Addison" Date: Thu, 11 Feb 2016 15:12:39 -0500 Subject: [PATCH 03/10] Issue #152 Remove unused jetty-maven-plugin. --- cas-client-integration-jetty/pom.xml | 35 +--------------------------- 1 file changed, 1 insertion(+), 34 deletions(-) diff --git a/cas-client-integration-jetty/pom.xml b/cas-client-integration-jetty/pom.xml index 96a28ac..480acbe 100644 --- a/cas-client-integration-jetty/pom.xml +++ b/cas-client-integration-jetty/pom.xml @@ -13,43 +13,10 @@ Jasig CAS Client for Java - Jetty Container Integration + 9.2.14.v20151106 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - org.jasig.cas.client From f09ee1c0e741eb58b7805af8748b3472d82c04db Mon Sep 17 00:00:00 2001 From: "Marvin S. Addison" Date: Fri, 12 Feb 2016 15:34:19 -0500 Subject: [PATCH 04/10] Issue #152 Fix multiple ticket validation attempt. --- .../cas/client/jetty/CasAuthenticator.java | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthenticator.java b/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthenticator.java index 14b359b..0cb10a0 100644 --- a/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthenticator.java +++ b/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthenticator.java @@ -162,22 +162,18 @@ public class CasAuthenticator extends AbstractLifeCycle implements Authenticator return Authentication.UNAUTHENTICATED; } - String ticket; - for (final Protocol protocol : Protocol.values()) { - ticket = request.getParameter(protocol.getArtifactParameterName()); - if (ticket != null) { - try { - logger.debug("Attempting to validate {}", ticket); - final Assertion assertion = ticketValidator.validate(ticket, serviceUrl(request, response)); - logger.debug("Successfully authenticated {}", assertion.getPrincipal()); - authentication = new CasAuthentication(this, ticket, assertion); - cacheAuthentication(request, authentication); - } catch (Exception e) { - throw new ServerAuthException("CAS ticket validation failed", e); - } + final String ticket = extractTicket(request); + if (ticket != null) { + try { + logger.debug("Attempting to validate {}", ticket); + final Assertion assertion = ticketValidator.validate(ticket, serviceUrl(request, response)); + logger.debug("Successfully authenticated {}", assertion.getPrincipal()); + authentication = new CasAuthentication(this, ticket, assertion); + cacheAuthentication(request, authentication); + } catch (Exception e) { + throw new ServerAuthException("CAS ticket validation failed", e); } } - if (authentication != null) { return authentication; } @@ -251,4 +247,14 @@ public class CasAuthenticator extends AbstractLifeCycle implements Authenticator } } + private String extractTicket(final HttpServletRequest request) { + String ticket; + for (final Protocol protocol : Protocol.values()) { + ticket = request.getParameter(protocol.getArtifactParameterName()); + if (ticket != null) { + return ticket; + } + } + return null; + } } From 5a68c92268b038ac21f7084bc6fae160831240a4 Mon Sep 17 00:00:00 2001 From: "Marvin S. Addison" Date: Fri, 12 Feb 2016 16:20:56 -0500 Subject: [PATCH 05/10] Issue #152 Prevent dupe ticket validations. The ticket parameter can linger in the URL after authentication. Don't attempt ticket validation if we are already authenticated. Fixes ticket validation failures on refresh and similar cases. --- .../cas/client/jetty/CasAuthenticator.java | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthenticator.java b/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthenticator.java index 0cb10a0..513ff55 100644 --- a/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthenticator.java +++ b/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthenticator.java @@ -155,15 +155,12 @@ public class CasAuthenticator extends AbstractLifeCycle implements Authenticator final HttpServletResponse response = (HttpServletResponse) servletResponse; CasAuthentication authentication = fetchCachedAuthentication(request); - if (!mandatory) { - if (authentication != null) { - return authentication; - } - return Authentication.UNAUTHENTICATED; + if (authentication != null) { + return authentication; } final String ticket = extractTicket(request); - if (ticket != null) { + if (ticket != null && mandatory) { try { logger.debug("Attempting to validate {}", ticket); final Assertion assertion = ticketValidator.validate(ticket, serviceUrl(request, response)); @@ -176,9 +173,11 @@ public class CasAuthenticator extends AbstractLifeCycle implements Authenticator } if (authentication != null) { return authentication; + } else if (mandatory) { + redirectToCas(request, response); + return Authentication.SEND_CONTINUE; } - redirectToCas(request, response); - return Authentication.SEND_CONTINUE; + return Authentication.UNAUTHENTICATED; } @Override @@ -208,7 +207,7 @@ public class CasAuthenticator extends AbstractLifeCycle implements Authenticator } private void cacheAuthentication(final HttpServletRequest request, final CasAuthentication authentication) { - final HttpSession session = request.getSession(false); + final HttpSession session = request.getSession(true); if (session != null) { session.setAttribute(CACHED_AUTHN_ATTRIBUTE, authentication); sessionMap.put(authentication.getTicket(), new WeakReference(session)); From 184868b296f9e73801bc716fad3e00bf8ea47db4 Mon Sep 17 00:00:00 2001 From: "Marvin S. Addison" Date: Fri, 12 Feb 2016 16:40:05 -0500 Subject: [PATCH 06/10] Issue #152 Add working Jetty context config files. --- .../src/test/resources/jetty/context-cas2.xml | 20 ++++++++++++++++++ .../test/resources/jetty/context-saml11.xml | 21 +++++++++++++++++++ .../src/test/resources/jetty/webapp.xml | 14 ------------- 3 files changed, 41 insertions(+), 14 deletions(-) create mode 100644 cas-client-integration-jetty/src/test/resources/jetty/context-cas2.xml create mode 100644 cas-client-integration-jetty/src/test/resources/jetty/context-saml11.xml delete mode 100644 cas-client-integration-jetty/src/test/resources/jetty/webapp.xml diff --git a/cas-client-integration-jetty/src/test/resources/jetty/context-cas2.xml b/cas-client-integration-jetty/src/test/resources/jetty/context-cas2.xml new file mode 100644 index 0000000..a786496 --- /dev/null +++ b/cas-client-integration-jetty/src/test/resources/jetty/context-cas2.xml @@ -0,0 +1,20 @@ + + + + + / + /webapps/yourapp + + + + app.example.com + + + https://cas.example.com/cas + + + + + + + diff --git a/cas-client-integration-jetty/src/test/resources/jetty/context-saml11.xml b/cas-client-integration-jetty/src/test/resources/jetty/context-saml11.xml new file mode 100644 index 0000000..5e1d5ad --- /dev/null +++ b/cas-client-integration-jetty/src/test/resources/jetty/context-saml11.xml @@ -0,0 +1,21 @@ + + + + + / + /webapps/yourapp + + + + app.example.com + memberOf + + + https://cas.example.com/cas + + + + + + + diff --git a/cas-client-integration-jetty/src/test/resources/jetty/webapp.xml b/cas-client-integration-jetty/src/test/resources/jetty/webapp.xml deleted file mode 100644 index f3c4fc4..0000000 --- a/cas-client-integration-jetty/src/test/resources/jetty/webapp.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - localhost:8080 - - - http://localhost:8081/cas - - - - - - \ No newline at end of file From 40291a447874704d6392df25d7c3f6414e8533d3 Mon Sep 17 00:00:00 2001 From: "Marvin S. Addison" Date: Fri, 19 Feb 2016 10:43:15 -0500 Subject: [PATCH 07/10] Issue #152 Log authentication success at INFO. --- .../main/java/org/jasig/cas/client/jetty/CasAuthenticator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthenticator.java b/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthenticator.java index 513ff55..a9a3cfa 100644 --- a/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthenticator.java +++ b/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthenticator.java @@ -164,7 +164,7 @@ public class CasAuthenticator extends AbstractLifeCycle implements Authenticator try { logger.debug("Attempting to validate {}", ticket); final Assertion assertion = ticketValidator.validate(ticket, serviceUrl(request, response)); - logger.debug("Successfully authenticated {}", assertion.getPrincipal()); + logger.info("Successfully authenticated {}", assertion.getPrincipal()); authentication = new CasAuthentication(this, ticket, assertion); cacheAuthentication(request, authentication); } catch (Exception e) { From 812198b6a56d245a77e27ecd7aeb00751d0cd93b Mon Sep 17 00:00:00 2001 From: "Marvin S. Addison" Date: Mon, 7 Mar 2016 07:42:31 -0500 Subject: [PATCH 08/10] Issue #152 Use CommonUtils for null checks. --- .../java/org/jasig/cas/client/jetty/CasAuthentication.java | 5 +++-- .../java/org/jasig/cas/client/jetty/CasAuthenticator.java | 2 +- .../java/org/jasig/cas/client/jetty/CasUserIdentity.java | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthentication.java b/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthentication.java index 0b8fb2a..831ec75 100644 --- a/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthentication.java +++ b/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthentication.java @@ -1,6 +1,7 @@ package org.jasig.cas.client.jetty; import org.eclipse.jetty.security.UserAuthentication; +import org.jasig.cas.client.util.CommonUtils; import org.jasig.cas.client.validation.Assertion; /** @@ -26,8 +27,8 @@ public class CasAuthentication extends UserAuthentication { */ public CasAuthentication(final CasAuthenticator authenticator, final String ticket, final Assertion assertion) { super(authenticator.getAuthMethod(), new CasUserIdentity(assertion, authenticator.getRoleAttribute())); - assert ticket != null : "Ticket cannot be null"; - assert authenticator != null : "CasAuthenticator cannot be null"; + CommonUtils.assertNotNull(ticket, "Ticket cannot be null"); + CommonUtils.assertNotNull(authenticator, "CasAuthenticator cannot be null"); this.authenticator = authenticator; this.ticket = ticket; } diff --git a/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthenticator.java b/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthenticator.java index a9a3cfa..355e1d4 100644 --- a/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthenticator.java +++ b/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthenticator.java @@ -89,7 +89,7 @@ public class CasAuthenticator extends AbstractLifeCycle implements Authenticator * @param ticketValidator Ticket validator, MUST NOT be null. */ public void setTicketValidator(final TicketValidator ticketValidator) { - assert ticketValidator != null : "TicketValidator cannot be null"; + CommonUtils.assertNotNull(ticketValidator, "TicketValidator cannot be null"); if (ticketValidator instanceof AbstractUrlBasedTicketValidator) { if (ticketValidator instanceof AbstractCasProtocolUrlBasedTicketValidator) { protocol = Protocol.CAS2; diff --git a/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasUserIdentity.java b/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasUserIdentity.java index 200e626..aee4212 100644 --- a/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasUserIdentity.java +++ b/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasUserIdentity.java @@ -2,6 +2,7 @@ package org.jasig.cas.client.jetty; import org.eclipse.jetty.server.UserIdentity; import org.jasig.cas.client.authentication.AttributePrincipal; +import org.jasig.cas.client.util.CommonUtils; import org.jasig.cas.client.validation.Assertion; import javax.security.auth.Subject; @@ -29,7 +30,7 @@ public class CasUserIdentity implements UserIdentity { * @param roleAttribute Principal attribute containing role data. */ public CasUserIdentity(final Assertion assertion, final String roleAttribute) { - assert assertion != null : "Assertion cannot be null"; + CommonUtils.assertNotNull(assertion, "Assertion cannot be null"); this.principal = assertion.getPrincipal(); this.roleAttribute = roleAttribute; } From 3f0a1c688376f3eca3a2dfc45a2199f29700d84d Mon Sep 17 00:00:00 2001 From: "Marvin S. Addison" Date: Mon, 7 Mar 2016 07:45:50 -0500 Subject: [PATCH 09/10] Issue #152 Fix javadoc typo. --- .../main/java/org/jasig/cas/client/jetty/CasAuthenticator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthenticator.java b/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthenticator.java index 355e1d4..45167ca 100644 --- a/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthenticator.java +++ b/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthenticator.java @@ -67,7 +67,7 @@ public class CasAuthenticator extends AbstractLifeCycle implements Authenticator /** CAS ticket validator component. */ private TicketValidator ticketValidator; - /** Space-delimited ist of server names. */ + /** Space-delimited list of server names. */ private String serverNames; /** CAS principal attribute containing role data. */ From 06b566e2d6aed997b635fafff7a9c40c12266907 Mon Sep 17 00:00:00 2001 From: "Marvin S. Addison" Date: Mon, 7 Mar 2016 07:51:25 -0500 Subject: [PATCH 10/10] Issue #152 Simplify ticket lookup. --- .../jasig/cas/client/jetty/CasAuthenticator.java | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthenticator.java b/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthenticator.java index 45167ca..21d4c51 100644 --- a/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthenticator.java +++ b/cas-client-integration-jetty/src/main/java/org/jasig/cas/client/jetty/CasAuthenticator.java @@ -159,7 +159,7 @@ public class CasAuthenticator extends AbstractLifeCycle implements Authenticator return authentication; } - final String ticket = extractTicket(request); + final String ticket = request.getParameter(protocol.getArtifactParameterName()); if (ticket != null && mandatory) { try { logger.debug("Attempting to validate {}", ticket); @@ -245,15 +245,4 @@ public class CasAuthenticator extends AbstractLifeCycle implements Authenticator throw new ServerAuthException("Redirect to CAS failed", e); } } - - private String extractTicket(final HttpServletRequest request) { - String ticket; - for (final Protocol protocol : Protocol.values()) { - ticket = request.getParameter(protocol.getArtifactParameterName()); - if (ticket != null) { - return ticket; - } - } - return null; - } }