diff --git a/.classpath b/.classpath index 13063b1cba..e96de12118 100644 --- a/.classpath +++ b/.classpath @@ -21,5 +21,8 @@ + + + diff --git a/adapters/cas/src/main/java/org/acegisecurity/adapters/cas/CasPasswordHandler.java b/adapters/cas/src/main/java/org/acegisecurity/adapters/cas/CasPasswordHandler.java new file mode 100644 index 0000000000..36bc94a92a --- /dev/null +++ b/adapters/cas/src/main/java/org/acegisecurity/adapters/cas/CasPasswordHandler.java @@ -0,0 +1,116 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.adapters.cas; + +import net.sf.acegisecurity.Authentication; +import net.sf.acegisecurity.AuthenticationException; +import net.sf.acegisecurity.AuthenticationManager; +import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.InitializingBean; + +import javax.servlet.ServletRequest; + + +/** + * Provides actual CAS authentication by delegation to an + * AuthenticationManager. + * + *

+ * Do not use this class directly. Instead configure CAS to use the {@link + * CasPasswordHandlerProxy}. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public final class CasPasswordHandler implements InitializingBean { + //~ Static fields/initializers ============================================= + + private static final Log logger = LogFactory.getLog(CasPasswordHandler.class); + + //~ Instance fields ======================================================== + + private AuthenticationManager authenticationManager; + + //~ Methods ================================================================ + + public void setAuthenticationManager( + AuthenticationManager authenticationManager) { + this.authenticationManager = authenticationManager; + } + + public AuthenticationManager getAuthenticationManager() { + return authenticationManager; + } + + public void afterPropertiesSet() throws Exception { + if (this.authenticationManager == null) { + throw new IllegalArgumentException( + "An AuthenticationManager is required"); + } + } + + /** + * Called by CasPasswordHandlerProxy for individual + * authentication requests. + * + *

+ * Delegates to the configured AuthenticationManager. + *

+ * + * @param servletRequest as provided by CAS + * @param username provided to CAS + * @param password provided to CAS + * + * @return whether authentication was successful or not + */ + public boolean authenticate(ServletRequest servletRequest, String username, + String password) { + if ((username == null) || "".equals(username)) { + return false; + } + + if (password == null) { + password = ""; + } + + Authentication request = new UsernamePasswordAuthenticationToken(username + .toString(), password.toString()); + Authentication response = null; + + try { + response = authenticationManager.authenticate(request); + } catch (AuthenticationException failed) { + if (logger.isDebugEnabled()) { + logger.debug("Authentication request for user: " + username + + " failed: " + failed.toString()); + } + + return false; + } + + if (logger.isDebugEnabled()) { + logger.debug("Authentication request for user: " + username + + " successful"); + } + + return true; + } +} diff --git a/adapters/cas/src/main/java/org/acegisecurity/adapters/cas/CasPasswordHandlerProxy.java b/adapters/cas/src/main/java/org/acegisecurity/adapters/cas/CasPasswordHandlerProxy.java new file mode 100644 index 0000000000..a5dbd27dfe --- /dev/null +++ b/adapters/cas/src/main/java/org/acegisecurity/adapters/cas/CasPasswordHandlerProxy.java @@ -0,0 +1,142 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.adapters.cas; + +import edu.yale.its.tp.cas.auth.PasswordHandler; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.context.ApplicationContext; + +import org.springframework.web.context.support.WebApplicationContextUtils; + +import java.util.Map; + +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; + + +/** + * Enables CAS to use the Acegi Security System for authentication. + * + *

+ * This class works along with {@link CasPasswordHandler} to enable users to + * easily migrate from stand-alone Acegi Security System deployments to + * enterprise-wide CAS deployments. + *

+ * + *

+ * It should be noted that the Acegi Security System will operate as a CAS + * client irrespective of the PasswordHandler used on the CAS + * server. In other words, this class need not be used on the CAS + * server if not desired. It exists solely for the convenience of users + * wishing have CAS delegate to an Acegi Security System-based + * AuthenticationManager. + *

+ * + *

+ * This class works requires a properly configured + * CasPasswordHandler. On the first authentication request, the + * class will use Spring's {@link + * WebApplicationContextUtils#getWebApplicationContext(ServletContext sc)} + * method to obtain an ApplicationContext instance, inside which + * must be a configured CasPasswordHandler instance. The + * CasPasswordHandlerProxy will then delegate authentication + * requests to that instance. + *

+ * + *

+ * To configure CAS to use this class, edit CAS' web.xml and + * define the edu.yale.its.tp.cas.authHandler context parameter + * with the value + * net.sf.acegisecurity.adapters.cas.CasPasswordHandlerProxy. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public class CasPasswordHandlerProxy implements PasswordHandler { + //~ Static fields/initializers ============================================= + + private static final Log logger = LogFactory.getLog(CasPasswordHandlerProxy.class); + + //~ Instance fields ======================================================== + + private ApplicationContext ctx; + private CasPasswordHandler handler; + + //~ Methods ================================================================ + + /** + * Called by CAS when authentication is required. + * + *

+ * Delegates to the CasPasswordHandler. + *

+ * + * @param request as provided by CAS + * @param username provided to CAS + * @param password provided to CAS + * + * @return whether authentication was successful or not + * + * @throws IllegalArgumentException if the application context does not + * contain a CasPasswordHandler or the + * ServletRequest was not of type + * HttpServletRequest + */ + public boolean authenticate(ServletRequest request, String username, + String password) { + if (ctx == null) { + if (!(request instanceof HttpServletRequest)) { + throw new IllegalArgumentException( + "Can only process HttpServletRequest"); + } + + HttpServletRequest httpRequest = (HttpServletRequest) request; + + ctx = this.getContext(httpRequest); + } + + if (handler == null) { + Map beans = ctx.getBeansOfType(CasPasswordHandler.class, true, true); + + if (beans.size() == 0) { + throw new IllegalArgumentException( + "Bean context must contain at least one bean of type CasPasswordHandler"); + } + + String beanName = (String) beans.keySet().iterator().next(); + handler = (CasPasswordHandler) beans.get(beanName); + } + + return handler.authenticate(request, username, password); + } + + /** + * Allows test cases to override where application context obtained from. + * + * @param httpRequest which can be used to find the + * ServletContext + * + * @return the Spring application context + */ + protected ApplicationContext getContext(HttpServletRequest httpRequest) { + return WebApplicationContextUtils.getRequiredWebApplicationContext(httpRequest.getSession() + .getServletContext()); + } +} diff --git a/adapters/cas/src/main/java/org/acegisecurity/adapters/cas/package.html b/adapters/cas/src/main/java/org/acegisecurity/adapters/cas/package.html new file mode 100644 index 0000000000..85dafa4bb6 --- /dev/null +++ b/adapters/cas/src/main/java/org/acegisecurity/adapters/cas/package.html @@ -0,0 +1,7 @@ + + +Adapter to Yale Central Authentication Service (CAS). +

+ + + diff --git a/adapters/cas/src/main/resources/org/acegisecurity/adapters/cas/applicationContext.xml b/adapters/cas/src/main/resources/org/acegisecurity/adapters/cas/applicationContext.xml new file mode 100644 index 0000000000..78cf5216dc --- /dev/null +++ b/adapters/cas/src/main/resources/org/acegisecurity/adapters/cas/applicationContext.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + marissa=koala,ROLES_IGNORED_BY_CAS + dianne=emu,ROLES_IGNORED_BY_CAS + scott=wombat,ROLES_IGNORED_BY_CAS + peter=opal,disabled,ROLES_IGNORED_BY_CAS + + + + + + + + + + + + + + + + + + + + diff --git a/adapters/cas/src/test/java/org/acegisecurity/adapters/cas/CasPasswordHandlerProxyTests.java b/adapters/cas/src/test/java/org/acegisecurity/adapters/cas/CasPasswordHandlerProxyTests.java new file mode 100644 index 0000000000..ccbbd0b5cc --- /dev/null +++ b/adapters/cas/src/test/java/org/acegisecurity/adapters/cas/CasPasswordHandlerProxyTests.java @@ -0,0 +1,112 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.adapters.cas; + +import junit.framework.TestCase; + +import net.sf.acegisecurity.MockHttpServletRequest; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import javax.servlet.http.HttpServletRequest; + + +/** + * Tests {@link CasPasswordHandlerProxy}. + * + * @author Ben Alex + * @version $Id$ + */ +public class CasPasswordHandlerProxyTests extends TestCase { + //~ Constructors =========================================================== + + public CasPasswordHandlerProxyTests() { + super(); + } + + public CasPasswordHandlerProxyTests(String arg0) { + super(arg0); + } + + //~ Methods ================================================================ + + public final void setUp() throws Exception { + super.setUp(); + } + + public static void main(String[] args) { + junit.textui.TestRunner.run(CasPasswordHandlerProxyTests.class); + } + + public void testDetectsIfHttpServletRequestNotPassed() { + CasPasswordHandlerProxy proxy = new MockCasPasswordHandlerProxy( + "net/sf/acegisecurity/adapters/cas/applicationContext-valid.xml"); + + try { + proxy.authenticate(null, "x", "y"); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("Can only process HttpServletRequest", + expected.getMessage()); + } + } + + public void testDetectsMissingDelegate() { + CasPasswordHandlerProxy proxy = new MockCasPasswordHandlerProxy( + "net/sf/acegisecurity/adapters/cas/applicationContext-invalid.xml"); + + try { + proxy.authenticate(new MockHttpServletRequest("/"), "x", "y"); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("Bean context must contain at least one bean of type CasPasswordHandler", + expected.getMessage()); + } + } + + public void testNormalOperation() { + CasPasswordHandlerProxy proxy = new MockCasPasswordHandlerProxy( + "net/sf/acegisecurity/adapters/cas/applicationContext-valid.xml"); + assertTrue(proxy.authenticate(new MockHttpServletRequest("/"), + "marissa", "koala")); + assertFalse(proxy.authenticate(new MockHttpServletRequest("/"), + "marissa", "WRONG_PASSWORD")); + assertFalse(proxy.authenticate(new MockHttpServletRequest("/"), + "INVALID_USER_NAME", "WRONG_PASSWORD")); + } + + //~ Inner Classes ========================================================== + + /** + * Mock object so that application context source can be specified. + */ + private class MockCasPasswordHandlerProxy extends CasPasswordHandlerProxy { + private ApplicationContext ctx; + + public MockCasPasswordHandlerProxy(String appContextLocation) { + ctx = new ClassPathXmlApplicationContext(appContextLocation); + } + + private MockCasPasswordHandlerProxy() { + super(); + } + + protected ApplicationContext getContext(HttpServletRequest httpRequest) { + return ctx; + } + } +} diff --git a/adapters/cas/src/test/java/org/acegisecurity/adapters/cas/CasPasswordHandlerTests.java b/adapters/cas/src/test/java/org/acegisecurity/adapters/cas/CasPasswordHandlerTests.java new file mode 100644 index 0000000000..f6c5876ddd --- /dev/null +++ b/adapters/cas/src/test/java/org/acegisecurity/adapters/cas/CasPasswordHandlerTests.java @@ -0,0 +1,107 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.adapters.cas; + +import junit.framework.TestCase; + +import net.sf.acegisecurity.MockAuthenticationManager; +import net.sf.acegisecurity.MockHttpServletRequest; + + +/** + * Tests {@link CasPasswordHandler}. + * + * @author Ben Alex + * @version $Id$ + */ +public class CasPasswordHandlerTests extends TestCase { + //~ Constructors =========================================================== + + public CasPasswordHandlerTests() { + super(); + } + + public CasPasswordHandlerTests(String arg0) { + super(arg0); + } + + //~ Methods ================================================================ + + public final void setUp() throws Exception { + super.setUp(); + } + + public static void main(String[] args) { + junit.textui.TestRunner.run(CasPasswordHandlerTests.class); + } + + public void testDeniesAccessWhenAuthenticationManagerThrowsException() + throws Exception { + CasPasswordHandler handler = new CasPasswordHandler(); + handler.setAuthenticationManager(new MockAuthenticationManager(false)); + handler.afterPropertiesSet(); + + assertFalse(handler.authenticate(new MockHttpServletRequest("/"), + "username", "password")); + } + + public void testDetectsEmptyAuthenticationManager() + throws Exception { + CasPasswordHandler handler = new CasPasswordHandler(); + + try { + handler.afterPropertiesSet(); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("An AuthenticationManager is required", + expected.getMessage()); + } + } + + public void testGettersSetters() { + CasPasswordHandler handler = new CasPasswordHandler(); + handler.setAuthenticationManager(new MockAuthenticationManager(false)); + assertTrue(handler.getAuthenticationManager() != null); + } + + public void testGracefullyHandlesEmptyUsernamesAndPassword() + throws Exception { + CasPasswordHandler handler = new CasPasswordHandler(); + handler.setAuthenticationManager(new MockAuthenticationManager(true)); + handler.afterPropertiesSet(); + + // If empty or null username we return false + assertFalse(handler.authenticate(new MockHttpServletRequest("/"), "", + "password")); + assertFalse(handler.authenticate(new MockHttpServletRequest("/"), null, + "password")); + + // We authenticate with null passwords (they might not have one) + assertTrue(handler.authenticate(new MockHttpServletRequest("/"), + "user", null)); + assertTrue(handler.authenticate(new MockHttpServletRequest("/"), + "user", "")); + } + + public void testNormalOperation() throws Exception { + CasPasswordHandler handler = new CasPasswordHandler(); + handler.setAuthenticationManager(new MockAuthenticationManager(true)); + handler.afterPropertiesSet(); + + assertTrue(handler.authenticate(new MockHttpServletRequest("/"), + "username", "password")); + } +} diff --git a/adapters/cas/src/test/java/org/acegisecurity/adapters/cas/applicationContext-invalid.xml b/adapters/cas/src/test/java/org/acegisecurity/adapters/cas/applicationContext-invalid.xml new file mode 100644 index 0000000000..872d1f5977 --- /dev/null +++ b/adapters/cas/src/test/java/org/acegisecurity/adapters/cas/applicationContext-invalid.xml @@ -0,0 +1,47 @@ + + + + + + + + + + marissa=koala,ROLE_TELLER,ROLE_SUPERVISOR + dianne=emu,ROLE_TELLER + scott=wombat,ROLE_TELLER + peter=opal,disabled,ROLE_TELLER + + + + + + + + + + + + + + + + + + diff --git a/adapters/cas/src/test/java/org/acegisecurity/adapters/cas/applicationContext-valid.xml b/adapters/cas/src/test/java/org/acegisecurity/adapters/cas/applicationContext-valid.xml new file mode 100644 index 0000000000..33132386f3 --- /dev/null +++ b/adapters/cas/src/test/java/org/acegisecurity/adapters/cas/applicationContext-valid.xml @@ -0,0 +1,49 @@ + + + + + + + + + + marissa=koala,ROLE_TELLER,ROLE_SUPERVISOR + dianne=emu,ROLE_TELLER + scott=wombat,ROLE_TELLER + peter=opal,disabled,ROLE_TELLER + + + + + + + + + + + + + + + + + + + + diff --git a/core/src/main/java/org/acegisecurity/providers/cas/CasAuthenticationProvider.java b/core/src/main/java/org/acegisecurity/providers/cas/CasAuthenticationProvider.java new file mode 100644 index 0000000000..9e78ef5303 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/cas/CasAuthenticationProvider.java @@ -0,0 +1,220 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.providers.cas; + +import net.sf.acegisecurity.Authentication; +import net.sf.acegisecurity.AuthenticationException; +import net.sf.acegisecurity.BadCredentialsException; +import net.sf.acegisecurity.GrantedAuthority; +import net.sf.acegisecurity.providers.AuthenticationProvider; +import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken; +import net.sf.acegisecurity.ui.cas.CasProcessingFilter; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.InitializingBean; + + +/** + * An {@link AuthenticationProvider} implementation that integrates with Yale + * Central Authentication Service (CAS). + * + *

+ * This AuthenticationProvider is capable of validating {@link + * UsernamePasswordAuthenticationToken} requests which contain a + * principal name equal to either {@link + * CasProcessingFilter#CAS_STATEFUL_IDENTIFIER} or {@link + * CasProcessingFilter#CAS_STATELESS_IDENTIFIER}. It can also validate a + * previously created {@link CasAuthenticationToken}. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public class CasAuthenticationProvider implements AuthenticationProvider, + InitializingBean { + //~ Static fields/initializers ============================================= + + private static final Log logger = LogFactory.getLog(CasAuthenticationProvider.class); + + //~ Instance fields ======================================================== + + private CasAuthoritiesPopulator casAuthoritiesPopulator; + private CasProxyDecider casProxyDecider; + private StatelessTicketCache statelessTicketCache; + private String key; + private TicketValidator ticketValidator; + + //~ Methods ================================================================ + + public void setCasAuthoritiesPopulator( + CasAuthoritiesPopulator casAuthoritiesPopulator) { + this.casAuthoritiesPopulator = casAuthoritiesPopulator; + } + + public CasAuthoritiesPopulator getCasAuthoritiesPopulator() { + return casAuthoritiesPopulator; + } + + public void setCasProxyDecider(CasProxyDecider casProxyDecider) { + this.casProxyDecider = casProxyDecider; + } + + public CasProxyDecider getCasProxyDecider() { + return casProxyDecider; + } + + public void setKey(String key) { + this.key = key; + } + + public String getKey() { + return key; + } + + public void setStatelessTicketCache( + StatelessTicketCache statelessTicketCache) { + this.statelessTicketCache = statelessTicketCache; + } + + public StatelessTicketCache getStatelessTicketCache() { + return statelessTicketCache; + } + + public void setTicketValidator(TicketValidator ticketValidator) { + this.ticketValidator = ticketValidator; + } + + public TicketValidator getTicketValidator() { + return ticketValidator; + } + + public void afterPropertiesSet() throws Exception { + if (this.casAuthoritiesPopulator == null) { + throw new IllegalArgumentException( + "A casAuthoritiesPopulator must be set"); + } + + if (this.ticketValidator == null) { + throw new IllegalArgumentException("A ticketValidator must be set"); + } + + if (this.casProxyDecider == null) { + throw new IllegalArgumentException("A casProxyDecider must be set"); + } + + if (this.statelessTicketCache == null) { + throw new IllegalArgumentException( + "A statelessTicketCache must be set"); + } + + if (key == null) { + throw new IllegalArgumentException( + "A Key is required so CasAuthenticationProvider can identify tokens it previously authenticated"); + } + } + + public Authentication authenticate(Authentication authentication) + throws AuthenticationException { + if (!supports(authentication.getClass())) { + return null; + } + + if (authentication instanceof UsernamePasswordAuthenticationToken + && (!CasProcessingFilter.CAS_STATEFUL_IDENTIFIER.equals( + authentication.getPrincipal().toString()) + && !CasProcessingFilter.CAS_STATELESS_IDENTIFIER.equals( + authentication.getPrincipal().toString()))) { + // UsernamePasswordAuthenticationToken not CAS related + return null; + } + + // If an existing CasAuthenticationToken, just check we created it + if (authentication instanceof CasAuthenticationToken) { + if (this.key.hashCode() == ((CasAuthenticationToken) authentication) + .getKeyHash()) { + return authentication; + } else { + throw new BadCredentialsException( + "The presented CasAuthenticationToken does not contain the expected key"); + } + } + + // Ensure credentials are presented + if (authentication.getCredentials() == null || "".equals(authentication.getCredentials())) { + throw new BadCredentialsException( + "Failed to provide a CAS service ticket to validate"); + } + + boolean stateless = false; + + if (authentication instanceof UsernamePasswordAuthenticationToken + && CasProcessingFilter.CAS_STATELESS_IDENTIFIER.equals( + authentication.getPrincipal())) { + stateless = true; + } + + CasAuthenticationToken result = null; + + if (stateless) { + // Try to obtain from cache + result = statelessTicketCache.getByTicketId(authentication.getCredentials() + .toString()); + } + + if (result == null) { + result = this.authenticateNow(authentication); + } + + if (stateless) { + // Add to cache + statelessTicketCache.putTicketInCache(result); + } + + return result; + } + + public boolean supports(Class authentication) { + if (UsernamePasswordAuthenticationToken.class.isAssignableFrom( + authentication)) { + return true; + } else if (CasAuthenticationToken.class.isAssignableFrom(authentication)) { + return true; + } else { + return false; + } + } + + private CasAuthenticationToken authenticateNow( + Authentication authentication) throws AuthenticationException { + // Validate + TicketResponse response = ticketValidator.confirmTicketValid(authentication.getCredentials() + .toString()); + + // Check proxy list is trusted + this.casProxyDecider.confirmProxyListTrusted(response.getProxyList()); + + // Build list of granted authorities + GrantedAuthority[] ga = this.casAuthoritiesPopulator.getAuthorities(response + .getUser()); + + // Construct CasAuthenticationToken + return new CasAuthenticationToken(this.key, response.getUser(), + authentication.getCredentials(), ga, response.getProxyList(), + response.getProxyGrantingTicketIou()); + } +} diff --git a/core/src/main/java/org/acegisecurity/providers/cas/CasAuthenticationToken.java b/core/src/main/java/org/acegisecurity/providers/cas/CasAuthenticationToken.java new file mode 100644 index 0000000000..d0e90fa2ce --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/cas/CasAuthenticationToken.java @@ -0,0 +1,172 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.providers.cas; + +import net.sf.acegisecurity.GrantedAuthority; +import net.sf.acegisecurity.providers.AbstractAuthenticationToken; + +import java.io.Serializable; +import java.util.List; + + +/** + * Represents a successful CAS Authentication. + * + * @author Ben Alex + * @version $Id$ + */ +public class CasAuthenticationToken extends AbstractAuthenticationToken implements Serializable { + //~ Instance fields ======================================================== + + private List proxyList; + private Object credentials; + private Object principal; + private String proxyGrantingTicketIou; + private GrantedAuthority[] authorities; + private int keyHash; + + //~ Constructors =========================================================== + + /** + * Constructor. + * + * @param key to identify if this object made by a given {@link + * CasAuthenticationProvider} + * @param principal the username from CAS (cannot be null) + * @param credentials the service/proxy ticket ID from CAS (cannot be + * null) + * @param authorities the authorities granted to the user (from {@link + * CasAuthoritiesPopulator}) (cannot be null) + * @param proxyList the list of proxies from CAS (cannot be + * null) + * @param proxyGrantingTicketIou the PGT-IOU ID from CAS (cannot be + * null) + * + * @throws IllegalArgumentException if a null was passed + */ + public CasAuthenticationToken(String key, Object principal, + Object credentials, GrantedAuthority[] authorities, List proxyList, + String proxyGrantingTicketIou) { + if ((key == null) || ("".equals(key)) || (principal == null) + || "".equals(principal) || (credentials == null) + || "".equals(credentials) || (authorities == null) + || (proxyList == null) || (proxyGrantingTicketIou == null) + || ("".equals(proxyGrantingTicketIou))) { + throw new IllegalArgumentException( + "Cannot pass null or empty values to constructor"); + } + + for (int i = 0; i < authorities.length; i++) { + if (authorities[i] == null) { + throw new IllegalArgumentException("Granted authority element " + + i + + " is null - GrantedAuthority[] cannot contain any null elements"); + } + } + + this.keyHash = key.hashCode(); + this.principal = principal; + this.credentials = credentials; + this.authorities = authorities; + this.proxyList = proxyList; + this.proxyGrantingTicketIou = proxyGrantingTicketIou; + } + + protected CasAuthenticationToken() { + throw new IllegalArgumentException("Cannot use default constructor"); + } + + //~ Methods ================================================================ + + /** + * Ignored (always true). + * + * @param isAuthenticated ignored + */ + public void setAuthenticated(boolean isAuthenticated) { + // ignored + } + + /** + * Always returns true. + * + * @return true + */ + public boolean isAuthenticated() { + return true; + } + + public GrantedAuthority[] getAuthorities() { + return this.authorities; + } + + public Object getCredentials() { + return this.credentials; + } + + public int getKeyHash() { + return this.keyHash; + } + + public Object getPrincipal() { + return this.principal; + } + + public String getProxyGrantingTicketIou() { + return proxyGrantingTicketIou; + } + + public List getProxyList() { + return proxyList; + } + + public boolean equals(Object obj) { + if (!super.equals(obj)) { + return false; + } + + if (obj instanceof CasAuthenticationToken) { + CasAuthenticationToken test = (CasAuthenticationToken) obj; + + // proxyGrantingTicketIou is never null due to constructor + if (!this.getProxyGrantingTicketIou().equals(test + .getProxyGrantingTicketIou())) { + return false; + } + + // proxyList is never null due to constructor + if (!this.getProxyList().equals(test.getProxyList())) { + return false; + } + + return true; + } + + System.out.println("THey're not equal"); + + return false; + } + + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append(super.toString()); + sb.append("; Credentials (Service/Proxy Ticket): " + this.credentials); + sb.append("; Proxy-Granting Ticket IOU: " + this.proxyGrantingTicketIou); + sb.append("; Proxy List: " + this.proxyList.toString()); + + return sb.toString(); + } +} diff --git a/core/src/main/java/org/acegisecurity/providers/cas/CasAuthoritiesPopulator.java b/core/src/main/java/org/acegisecurity/providers/cas/CasAuthoritiesPopulator.java new file mode 100644 index 0000000000..e5b247c488 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/cas/CasAuthoritiesPopulator.java @@ -0,0 +1,59 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.providers.cas; + +import net.sf.acegisecurity.AuthenticationException; +import net.sf.acegisecurity.GrantedAuthority; + + +/** + * Populates the GrantedAuthority[] objects for a CAS + * authenticated user. + * + *

+ * CAS does not provide the authorities (roles) granted to a user. It merely + * authenticates their identity. As the Acegi Security System for Spring needs + * to know the authorities granted to a user in order to construct a valid + * Authentication object, implementations of this interface will + * provide this information. + *

+ * + *

+ * Implementations should not perform any caching. They will only be called + * when a refresh is required. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public interface CasAuthoritiesPopulator { + //~ Methods ================================================================ + + /** + * Obtains the granted authorities for the specified user. + * + *

+ * May throw any AuthenticationException or return + * null if the authorities are unavailable. + *

+ * + * @param casUserId as obtained from the CAS validation service + * + * @return the granted authorities for the indicated user + */ + public GrantedAuthority[] getAuthorities(String casUserId) + throws AuthenticationException; +} diff --git a/core/src/main/java/org/acegisecurity/providers/cas/CasProxyDecider.java b/core/src/main/java/org/acegisecurity/providers/cas/CasProxyDecider.java new file mode 100644 index 0000000000..59783973d2 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/cas/CasProxyDecider.java @@ -0,0 +1,72 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.providers.cas; + +import java.util.List; + + +/** + * Decides whether a proxy list presented via CAS is trusted or not. + * + *

+ * CAS 1.0 allowed services to receive a service ticket and then validate it. + * CAS 2.0 allows services to receive a service ticket and then validate it + * with a proxy callback URL. The callback will enable the CAS server to + * authenticate the service. In doing so the service will receive a + * proxy-granting ticket and a proxy-granting ticket IOU. The IOU is just an + * internal record that a proxy-granting ticket is due to be received via the + * callback URL. + *

+ * + *

+ * With a proxy-granting ticket, a service can request the CAS server provides + * it with a proxy ticket. A proxy ticket is just a service ticket, but the + * CAS server internally tracks the list (chain) of services used to build the + * proxy ticket. The proxy ticket is then presented to the target service. + *

+ * + *

+ * If this application is a target service of a proxy ticket, the + * CasProxyDecider resolves whether or not the proxy list is + * trusted. Applications should only trust services they allow to impersonate + * an end user. + *

+ * + *

+ * If this application is a service that should never accept proxy-granting + * tickets, the implementation should reject tickets that present a proxy list + * with any members. If the list has no members, it indicates the CAS server + * directly authenticated the user (ie there are no services which proxied the + * user authentication). + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public interface CasProxyDecider { + //~ Methods ================================================================ + + /** + * Decides whether the proxy list is trusted. + * + *

+ * Must throw any ProxyUntrustedException if the proxy list is + * untrusted. + *

+ */ + public void confirmProxyListTrusted(List proxyList) + throws ProxyUntrustedException; +} diff --git a/core/src/main/java/org/acegisecurity/providers/cas/ProxyUntrustedException.java b/core/src/main/java/org/acegisecurity/providers/cas/ProxyUntrustedException.java new file mode 100644 index 0000000000..66733ba960 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/cas/ProxyUntrustedException.java @@ -0,0 +1,50 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.providers.cas; + +import net.sf.acegisecurity.AuthenticationException; + + +/** + * Thrown if a CAS proxy ticket is presented from an untrusted proxy. + * + * @author Ben Alex + * @version $Id$ + */ +public class ProxyUntrustedException extends AuthenticationException { + //~ Constructors =========================================================== + + /** + * Constructs a ProxyUntrustedException with the specified + * message. + * + * @param msg the detail message. + */ + public ProxyUntrustedException(String msg) { + super(msg); + } + + /** + * Constructs a ProxyUntrustedException with the specified + * message and root cause. + * + * @param msg the detail message. + * @param t root cause + */ + public ProxyUntrustedException(String msg, Throwable t) { + super(msg, t); + } +} diff --git a/core/src/main/java/org/acegisecurity/providers/cas/StatelessTicketCache.java b/core/src/main/java/org/acegisecurity/providers/cas/StatelessTicketCache.java new file mode 100644 index 0000000000..ad406e040e --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/cas/StatelessTicketCache.java @@ -0,0 +1,116 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.providers.cas; + +/** + * Caches CAS service tickets and CAS proxy tickets for stateless connections. + * + *

+ * When a service ticket or proxy ticket is validated against the CAS server, + * it is unable to be used again. Most types of callers are stateful and are + * associated with a given HttpSession. This allows the + * affirmative CAS validation outcome to be stored in the + * HttpSession, meaning the removal of the ticket from the CAS + * server is not an issue issue. + *

+ * + *

+ * Stateless callers, such as remoting protocols, cannot take advantage of + * HttpSession. If the stateless caller is located a significant + * network distance from the CAS server, acquiring a fresh service ticket or + * proxy ticket for each invocation would be expensive. + *

+ * + *

+ * To avoid this issue with stateless callers, it is expected stateless callers + * will obtain a single service ticket or proxy ticket, and then present this + * same ticket to the Acegi Security System secured application on each + * occasion. As no HttpSession is available for such callers, the + * affirmative CAS validation outcome cannot be stored in this location. + *

+ * + *

+ * The StatelessTicketCache enables the service tickets and proxy + * tickets belonging to stateless callers to be placed in a cache. This + * in-memory cache stores the CasAuthenticationToken, effectively + * providing the same capability as a HttpSession with the ticket + * identifier being the key rather than a session identifier. + *

+ * + *

+ * Implementations should provide a reasonable timeout on stored entries, such + * that the stateless caller are not required to unnecessarily acquire fresh + * CAS service tickets or proxy tickets. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public interface StatelessTicketCache { + //~ Methods ================================================================ + + /** + * Retrieves the CasAuthenticationToken associated with the + * specified ticket. + * + *

+ * If not found, returns a + * nullCasAuthenticationToken. + *

+ * + * @return the fully populated authentication token + */ + public CasAuthenticationToken getByTicketId(String serviceTicket); + + /** + * Adds the specified CasAuthenticationToken to the cache. + * + *

+ * The {@link CasAuthenticationToken#getCredentials()} method is used to + * retrieve the service ticket number. + *

+ * + * @param token to be added to the cache + */ + public void putTicketInCache(CasAuthenticationToken token); + + /** + * Removes the specified ticket from the cache, as per {@link + * #removeTicketFromCache(String)}. + * + *

+ * Implementations should use {@link + * CasAuthenticationToken#getCredentials()} to obtain the ticket and then + * delegate to to the {@link #removeTicketFromCache(String)} method. + *

+ * + * @param token to be removed + */ + public void removeTicketFromCache(CasAuthenticationToken token); + + /** + * Removes the specified ticket from the cache, meaning that future calls + * will require a new service ticket. + * + *

+ * This is in case applications wish to provide a session termination + * capability for their stateless clients. + *

+ * + * @param serviceTicket to be removed + */ + public void removeTicketFromCache(String serviceTicket); +} diff --git a/core/src/main/java/org/acegisecurity/providers/cas/TicketResponse.java b/core/src/main/java/org/acegisecurity/providers/cas/TicketResponse.java new file mode 100644 index 0000000000..d7600c744c --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/cas/TicketResponse.java @@ -0,0 +1,102 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.providers.cas; + +import java.util.List; +import java.util.Vector; + + +/** + * Represents a CAS service ticket in native CAS form. + * + * @author Ben Alex + * @version $Id$ + */ +public class TicketResponse { + //~ Instance fields ======================================================== + + private List proxyList; + private String proxyGrantingTicketIou; + private String user; + + //~ Constructors =========================================================== + + /** + * Constructor. + * + *

+ * If null is passed into the proxyList or + * proxyGrantingTicketIou, suitable defaults are established. + * However, null cannot be passed for the user + * argument. + *

+ * + * @param user the user as indicated by CAS (cannot be null or + * an empty String) + * @param proxyList as provided by CAS (may be null) + * @param proxyGrantingTicketIou as provided by CAS (may be + * null) + * + * @throws IllegalArgumentException DOCUMENT ME! + */ + public TicketResponse(String user, List proxyList, + String proxyGrantingTicketIou) { + if (proxyList == null) { + proxyList = new Vector(); + } + + if (proxyGrantingTicketIou == null) { + proxyGrantingTicketIou = ""; + } + + if ((user == null) || "".equals(user)) { + throw new IllegalArgumentException( + "Cannot pass null or empty String for User"); + } + + this.user = user; + this.proxyList = proxyList; + this.proxyGrantingTicketIou = proxyGrantingTicketIou; + } + + protected TicketResponse() { + throw new IllegalArgumentException("Cannot use default constructor"); + } + + //~ Methods ================================================================ + + public String getProxyGrantingTicketIou() { + return proxyGrantingTicketIou; + } + + public List getProxyList() { + return proxyList; + } + + public String getUser() { + return user; + } + + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append(super.toString()); + sb.append(": User: " + this.user); + sb.append("; Proxy-Granting Ticket IOU: " + this.proxyGrantingTicketIou); + sb.append("; Proxy List: " + this.proxyList.toString()); + + return sb.toString(); + } +} diff --git a/core/src/main/java/org/acegisecurity/providers/cas/TicketValidator.java b/core/src/main/java/org/acegisecurity/providers/cas/TicketValidator.java new file mode 100644 index 0000000000..c9be4d38cb --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/cas/TicketValidator.java @@ -0,0 +1,53 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.providers.cas; + +import net.sf.acegisecurity.AuthenticationException; + + +/** + * Validates a CAS service ticket. + * + *

+ * Implementations must accept CAS proxy tickets, in addition to CAS service + * tickets. If proxy tickets should be rejected, this is resolved by a {@link + * CasProxyDecider} implementation (not by the TicketValidator). + *

+ * + *

+ * Implementations may request a proxy granting ticket if wish, although this + * behaviour is not mandatory. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public interface TicketValidator { + //~ Methods ================================================================ + + /** + * Returns information about the ticket, if it is valid for this service. + * + *

+ * Must throw an AuthenticationException if the ticket is not + * valid for this service. + *

+ * + * @return details of the CAS service ticket + */ + public TicketResponse confirmTicketValid(String serviceTicket) + throws AuthenticationException; +} diff --git a/core/src/main/java/org/acegisecurity/providers/cas/cache/EhCacheBasedTicketCache.java b/core/src/main/java/org/acegisecurity/providers/cas/cache/EhCacheBasedTicketCache.java new file mode 100644 index 0000000000..1a7a1e841c --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/cas/cache/EhCacheBasedTicketCache.java @@ -0,0 +1,108 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.providers.cas.cache; + +import net.sf.acegisecurity.providers.cas.CasAuthenticationToken; +import net.sf.acegisecurity.providers.cas.StatelessTicketCache; + +import net.sf.ehcache.Cache; +import net.sf.ehcache.CacheException; +import net.sf.ehcache.CacheManager; +import net.sf.ehcache.Element; + +import org.springframework.beans.factory.InitializingBean; + +import org.springframework.dao.DataRetrievalFailureException; + + +/** + * Caches tickets using EHCACHE. + * + * @author Ben Alex + * @version $Id$ + */ +public class EhCacheBasedTicketCache implements StatelessTicketCache, + InitializingBean { + //~ Instance fields ======================================================== + + private Cache cache; + private CacheManager manager; + private int minutesToIdle = 20; + + //~ Methods ================================================================ + + public CasAuthenticationToken getByTicketId(String serviceTicket) { + Element element = null; + + try { + element = cache.get(serviceTicket); + } catch (CacheException cacheException) { + throw new DataRetrievalFailureException("Cache failure: " + + cacheException.getMessage()); + } + + if (element == null) { + System.out.println("not found"); + + return null; + } else { + System.out.println("found"); + + return (CasAuthenticationToken) element.getValue(); + } + } + + public void setMinutesToIdle(int minutesToIdle) { + this.minutesToIdle = minutesToIdle; + } + + /** + * Specifies how many minutes an entry will remain in the cache from when + * it was last accessed. This is effectively the session duration. + * + *

+ * Defaults to 20 minutes. + *

+ * + * @return Returns the minutes an element remains in the cache + */ + public int getMinutesToIdle() { + return minutesToIdle; + } + + public void afterPropertiesSet() throws Exception { + manager = CacheManager.create(); + + // Cache name, max memory, overflowToDisk, eternal, timeToLive, timeToIdle + cache = new Cache("ehCacheBasedTicketCache", Integer.MAX_VALUE, false, + false, minutesToIdle * 60, minutesToIdle * 60); + manager.addCache(cache); + } + + public void putTicketInCache(CasAuthenticationToken token) { + Element element = new Element(token.getCredentials().toString(), token); + System.out.println("Adding " + element.getKey()); + cache.put(element); + } + + public void removeTicketFromCache(CasAuthenticationToken token) { + this.removeTicketFromCache(token.getCredentials().toString()); + } + + public void removeTicketFromCache(String serviceTicket) { + cache.remove(serviceTicket); + } +} diff --git a/core/src/main/java/org/acegisecurity/providers/cas/package.html b/core/src/main/java/org/acegisecurity/providers/cas/package.html new file mode 100644 index 0000000000..698c9baf40 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/cas/package.html @@ -0,0 +1,6 @@ + + +An authentication provider that can process Yale Central Authentication Service (CAS) +service tickets and proxy tickets. + + diff --git a/core/src/main/java/org/acegisecurity/providers/cas/populator/DaoCasAuthoritiesPopulator.java b/core/src/main/java/org/acegisecurity/providers/cas/populator/DaoCasAuthoritiesPopulator.java new file mode 100644 index 0000000000..2b29cc446f --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/cas/populator/DaoCasAuthoritiesPopulator.java @@ -0,0 +1,67 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.providers.cas.populator; + +import net.sf.acegisecurity.AuthenticationException; +import net.sf.acegisecurity.GrantedAuthority; +import net.sf.acegisecurity.providers.cas.CasAuthoritiesPopulator; +import net.sf.acegisecurity.providers.dao.AuthenticationDao; + +import org.springframework.beans.factory.InitializingBean; + + +/** + * Populates the CAS authorities via an {@link AuthenticationDao}. + * + *

+ * The additional information (username, password, enabled status etc) an + * AuthenticationDao implementation provides about a + * User is ignored. Only the GrantedAuthoritys are + * relevant to this class. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public class DaoCasAuthoritiesPopulator implements CasAuthoritiesPopulator, + InitializingBean { + //~ Instance fields ======================================================== + + private AuthenticationDao authenticationDao; + + //~ Methods ================================================================ + + public void setAuthenticationDao(AuthenticationDao authenticationDao) { + this.authenticationDao = authenticationDao; + } + + public AuthenticationDao getAuthenticationDao() { + return authenticationDao; + } + + public GrantedAuthority[] getAuthorities(String casUserId) + throws AuthenticationException { + return this.authenticationDao.loadUserByUsername(casUserId) + .getAuthorities(); + } + + public void afterPropertiesSet() throws Exception { + if (this.authenticationDao == null) { + throw new IllegalArgumentException( + "An authenticationDao must be set"); + } + } +} diff --git a/core/src/main/java/org/acegisecurity/providers/cas/populator/package.html b/core/src/main/java/org/acegisecurity/providers/cas/populator/package.html new file mode 100644 index 0000000000..7e49e0f543 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/cas/populator/package.html @@ -0,0 +1,5 @@ + + +Implementations that populate GrantedAuthority[]s of CAS authentications. + + diff --git a/core/src/main/java/org/acegisecurity/providers/cas/proxy/AcceptAnyCasProxy.java b/core/src/main/java/org/acegisecurity/providers/cas/proxy/AcceptAnyCasProxy.java new file mode 100644 index 0000000000..f6fa468b90 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/cas/proxy/AcceptAnyCasProxy.java @@ -0,0 +1,55 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.providers.cas.proxy; + +import net.sf.acegisecurity.providers.cas.CasProxyDecider; +import net.sf.acegisecurity.providers.cas.ProxyUntrustedException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.util.List; + + +/** + * Accepts a proxied request from any other service. + * + *

+ * Also accepts the request if there was no proxy (ie the user directly + * authenticated against this service). + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public class AcceptAnyCasProxy implements CasProxyDecider { + //~ Static fields/initializers ============================================= + + private static final Log logger = LogFactory.getLog(AcceptAnyCasProxy.class); + + //~ Methods ================================================================ + + public void confirmProxyListTrusted(List proxyList) + throws ProxyUntrustedException { + if (proxyList == null) { + throw new IllegalArgumentException("proxyList cannot be null"); + } + + if (logger.isDebugEnabled()) { + logger.debug("Always accepting proxy list: " + proxyList.toString()); + } + } +} diff --git a/core/src/main/java/org/acegisecurity/providers/cas/proxy/NamedCasProxyDecider.java b/core/src/main/java/org/acegisecurity/providers/cas/proxy/NamedCasProxyDecider.java new file mode 100644 index 0000000000..5a0e60b58f --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/cas/proxy/NamedCasProxyDecider.java @@ -0,0 +1,87 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.providers.cas.proxy; + +import net.sf.acegisecurity.providers.cas.CasProxyDecider; +import net.sf.acegisecurity.providers.cas.ProxyUntrustedException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.InitializingBean; + +import java.util.List; + + +/** + * Accepts proxied requests if the closest proxy is named in the + * validProxies list. + * + *

+ * Also accepts the request if there was no proxy (ie the user directly + * authenticated against this service). + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public class NamedCasProxyDecider implements CasProxyDecider, InitializingBean { + //~ Static fields/initializers ============================================= + + private static final Log logger = LogFactory.getLog(NamedCasProxyDecider.class); + + //~ Instance fields ======================================================== + + private List validProxies; + + //~ Methods ================================================================ + + public void setValidProxies(List validProxies) { + this.validProxies = validProxies; + } + + public List getValidProxies() { + return validProxies; + } + + public void afterPropertiesSet() throws Exception { + if (this.validProxies == null) { + throw new IllegalArgumentException( + "A validProxies list must be set"); + } + } + + public void confirmProxyListTrusted(List proxyList) + throws ProxyUntrustedException { + if (proxyList == null) { + throw new IllegalArgumentException("proxyList cannot be null"); + } + + if (logger.isDebugEnabled()) { + logger.debug("Proxy list: " + proxyList.toString()); + } + + if (proxyList.size() == 0) { + // A Service Ticket (not a Proxy Ticket) + return; + } + + if (!validProxies.contains(proxyList.get(0))) { + throw new ProxyUntrustedException("Nearest proxy '" + + proxyList.get(0) + "' is untrusted"); + } + } +} diff --git a/core/src/main/java/org/acegisecurity/providers/cas/proxy/RejectProxyTickets.java b/core/src/main/java/org/acegisecurity/providers/cas/proxy/RejectProxyTickets.java new file mode 100644 index 0000000000..92aab08fc7 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/cas/proxy/RejectProxyTickets.java @@ -0,0 +1,63 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.providers.cas.proxy; + +import net.sf.acegisecurity.providers.cas.CasProxyDecider; +import net.sf.acegisecurity.providers.cas.ProxyUntrustedException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.util.List; + + +/** + * Accepts no proxied requests. + * + *

+ * This class should be used if only service tickets wish to be accepted (ie no + * proxy tickets at all). + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public class RejectProxyTickets implements CasProxyDecider { + //~ Static fields/initializers ============================================= + + private static final Log logger = LogFactory.getLog(RejectProxyTickets.class); + + //~ Methods ================================================================ + + public void confirmProxyListTrusted(List proxyList) + throws ProxyUntrustedException { + if (proxyList == null) { + throw new IllegalArgumentException("proxyList cannot be null"); + } + + if (proxyList.size() == 0) { + // A Service Ticket (not a Proxy Ticket) + return; + } + + if (logger.isDebugEnabled()) { + logger.debug("Proxies are unacceptable; proxy list provided: " + + proxyList.toString()); + } + + throw new ProxyUntrustedException("Proxy tickets are rejected"); + } +} diff --git a/core/src/main/java/org/acegisecurity/providers/cas/proxy/package.html b/core/src/main/java/org/acegisecurity/providers/cas/proxy/package.html new file mode 100644 index 0000000000..cc163a2f6f --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/cas/proxy/package.html @@ -0,0 +1,6 @@ + + +Implementations that decide whether proxy lists of +CAS authentications are trusted. + + diff --git a/core/src/main/java/org/acegisecurity/providers/cas/ticketvalidator/AbstractTicketValidator.java b/core/src/main/java/org/acegisecurity/providers/cas/ticketvalidator/AbstractTicketValidator.java new file mode 100644 index 0000000000..1cd25cc9c3 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/cas/ticketvalidator/AbstractTicketValidator.java @@ -0,0 +1,96 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.providers.cas.ticketvalidator; + +import net.sf.acegisecurity.providers.cas.TicketValidator; +import net.sf.acegisecurity.ui.cas.ServiceProperties; + +import org.springframework.beans.factory.InitializingBean; + + +/** + * Convenience abstract base for TicketValidators. + * + * @author Ben Alex + * @version $Id$ + */ +public abstract class AbstractTicketValidator implements TicketValidator, + InitializingBean { + //~ Instance fields ======================================================== + + private ServiceProperties serviceProperties; + private String casValidate; + private String trustStore; + + //~ Methods ================================================================ + + public void setCasValidate(String casValidate) { + this.casValidate = casValidate; + } + + /** + * Mandatory URL to CAS' proxy ticket valiation service. + * + *

+ * This is usually something like + * https://www.mycompany.com/cas/proxyValidate. + *

+ * + * @return the CAS proxy ticket validation URL + */ + public String getCasValidate() { + return casValidate; + } + + public void setServiceProperties(ServiceProperties serviceProperties) { + this.serviceProperties = serviceProperties; + } + + public ServiceProperties getServiceProperties() { + return serviceProperties; + } + + public void setTrustStore(String trustStore) { + this.trustStore = trustStore; + } + + /** + * Optional property which will be used to set the system property + * javax.net.ssl.trustStore. + * + * @return the javax.net.ssl.trustStore that will be set + * during bean initialization, or null to leave the + * system property unchanged + */ + public String getTrustStore() { + return trustStore; + } + + public void afterPropertiesSet() throws Exception { + if ((this.casValidate == null) || "".equals(casValidate)) { + throw new IllegalArgumentException("A casValidate URL must be set"); + } + + if (serviceProperties == null) { + throw new IllegalArgumentException( + "serviceProperties must be specified"); + } + + if ((trustStore != null) && (!"".equals(trustStore))) { + System.setProperty("javax.net.ssl.trustStore", trustStore); + } + } +} diff --git a/core/src/main/java/org/acegisecurity/providers/cas/ticketvalidator/CasProxyTicketValidator.java b/core/src/main/java/org/acegisecurity/providers/cas/ticketvalidator/CasProxyTicketValidator.java new file mode 100644 index 0000000000..17a405bd58 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/cas/ticketvalidator/CasProxyTicketValidator.java @@ -0,0 +1,128 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.providers.cas.ticketvalidator; + +import edu.yale.its.tp.cas.client.ProxyTicketValidator; + +import net.sf.acegisecurity.AuthenticationException; +import net.sf.acegisecurity.AuthenticationServiceException; +import net.sf.acegisecurity.BadCredentialsException; +import net.sf.acegisecurity.providers.cas.TicketResponse; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + + +/** + * Uses CAS' ProxyTicketValidator to validate a service ticket. + * + * @author Ben Alex + * @version $Id$ + */ +public class CasProxyTicketValidator extends AbstractTicketValidator { + //~ Static fields/initializers ============================================= + + private static final Log logger = LogFactory.getLog(CasProxyTicketValidator.class); + + //~ Instance fields ======================================================== + + private String proxyCallbackUrl; + + //~ Methods ================================================================ + + public void setProxyCallbackUrl(String proxyCallbackUrl) { + this.proxyCallbackUrl = proxyCallbackUrl; + } + + /** + * Optional callback URL to obtain a proxy-granting ticket from CAS. + * + *

+ * This callback URL belongs to the Acegi Security System for Spring + * secured application. We suggest you use CAS' + * ProxyTicketReceptor servlet to receive this callback and + * manage the proxy-granting ticket list. The callback URL is usually + * something like + * https://www.mycompany.com/application/casProxy/receptor. + *

+ * + *

+ * If left null, the CasAuthenticationToken will + * not have a proxy granting ticket IOU and there will be no + * proxy-granting ticket callback. Accordingly, the Acegi Securty System + * for Spring secured application will be unable to obtain a proxy ticket + * to call another CAS-secured service on behalf of the user. This is not + * really an issue for most applications. + *

+ * + * @return the proxy callback URL, or null if not used + */ + public String getProxyCallbackUrl() { + return proxyCallbackUrl; + } + + public TicketResponse confirmTicketValid(String serviceTicket) + throws AuthenticationException { + // Attempt to validate presented ticket using CAS' ProxyTicketValidator class + ProxyTicketValidator pv = new ProxyTicketValidator(); + + pv.setCasValidateUrl(super.getCasValidate()); + pv.setServiceTicket(serviceTicket); + pv.setService(super.getServiceProperties().getService()); + + if (super.getServiceProperties().isSendRenew()) { + logger.warn( + "The current CAS ProxyTicketValidator does not support the 'renew' property. The ticket cannot be validated as having been issued by a 'renew' authentication. It is expected this will be corrected in a future version of CAS' ProxyTicketValidator."); + } + + if ((this.proxyCallbackUrl != null) + && (!"".equals(this.proxyCallbackUrl))) { + pv.setProxyCallbackUrl(proxyCallbackUrl); + } + + return validateNow(pv); + } + + /** + * Perform the actual remote invocation. Protected to enable replacement + * during tests. + * + * @param pv the populated ProxyTicketValidator + * + * @return the TicketResponse + * + * @throws AuthenticationServiceException + * ifProxyTicketValidator internally fails + * @throws BadCredentialsException DOCUMENT ME! + */ + protected TicketResponse validateNow(ProxyTicketValidator pv) + throws AuthenticationServiceException, BadCredentialsException { + try { + pv.validate(); + } catch (Exception internalProxyTicketValidatorProblem) { + throw new AuthenticationServiceException(internalProxyTicketValidatorProblem + .getMessage()); + } + + if (!pv.isAuthenticationSuccesful()) { + throw new BadCredentialsException(pv.getErrorCode() + ": " + + pv.getErrorMessage()); + } + + return new TicketResponse(pv.getUser(), pv.getProxyList(), + pv.getPgtIou()); + } +} diff --git a/core/src/main/java/org/acegisecurity/providers/cas/ticketvalidator/package.html b/core/src/main/java/org/acegisecurity/providers/cas/ticketvalidator/package.html new file mode 100644 index 0000000000..26bceb87c2 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/cas/ticketvalidator/package.html @@ -0,0 +1,5 @@ + + +Implementations that validate service tickets. + + diff --git a/core/src/main/java/org/acegisecurity/ui/cas/CasProcessingFilter.java b/core/src/main/java/org/acegisecurity/ui/cas/CasProcessingFilter.java new file mode 100644 index 0000000000..c4dea63ece --- /dev/null +++ b/core/src/main/java/org/acegisecurity/ui/cas/CasProcessingFilter.java @@ -0,0 +1,111 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.ui.cas; + +import net.sf.acegisecurity.Authentication; +import net.sf.acegisecurity.AuthenticationException; +import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken; +import net.sf.acegisecurity.ui.AbstractProcessingFilter; + +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; + + +/** + * Processes a CAS service ticket. + * + *

+ * A service ticket consists of an opaque ticket string. It arrives at this + * filter by the user's browser successfully authenticating using CAS, and + * then receiving a HTTP redirect to a service. The opal ticket + * string is presented in the ticket request parameter. This + * filter monitors the service URL so it can receive the service + * ticket and process it. The CAS server knows which service URL + * to use via the {@link ServiceProperties#getService()} method. + *

+ * + *

+ * Processing the service ticket involves creating a + * UsernamePasswordAuthenticationToken which uses {@link + * #CAS_STATEFUL_IDENTIFIER} for the principal and the opaque + * ticket string as the credentials. + *

+ * + *

+ * The configured AuthenticationManager is expected to provide a + * provider that can recognise + * UsernamePasswordAuthenticationTokens containing this special + * principal name, and process them accordingly by validation + * with the CAS server. + *

+ * + *

+ * Do not use this class directly. Instead configure + * web.xml to use the {@link + * net.sf.acegisecurity.util.FilterToBeanProxy}. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public class CasProcessingFilter extends AbstractProcessingFilter { + //~ Static fields/initializers ============================================= + + /** + * Used to identify a CAS request for a stateful user agent, such as a web + * browser. + */ + public static final String CAS_STATEFUL_IDENTIFIER = "_cas_stateful_"; + + /** + * Used to identify a CAS request for a stateless user agent, such as a + * remoting protocol client (eg Hessian, Burlap, SOAP etc). Results in a + * more aggressive caching strategy being used, as the absence of a + * HttpSession will result in a new authentication attempt on + * every request. + */ + public static final String CAS_STATELESS_IDENTIFIER = "_cas_stateless_"; + + //~ Methods ================================================================ + + /** + * This filter by default responds to + * /j_acegi_cas_security_check. + * + * @return the default + */ + public String getDefaultFilterProcessesUrl() { + return "/j_acegi_cas_security_check"; + } + + public Authentication attemptAuthentication(HttpServletRequest request) + throws AuthenticationException { + String username = CAS_STATEFUL_IDENTIFIER; + String password = request.getParameter("ticket"); + + if (password == null) { + password = ""; + } + + UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, + password); + + return this.getAuthenticationManager().authenticate(authRequest); + } + + public void init(FilterConfig filterConfig) throws ServletException {} +} diff --git a/core/src/main/java/org/acegisecurity/ui/cas/CasProcessingFilterEntryPoint.java b/core/src/main/java/org/acegisecurity/ui/cas/CasProcessingFilterEntryPoint.java new file mode 100644 index 0000000000..dfbcc4b91b --- /dev/null +++ b/core/src/main/java/org/acegisecurity/ui/cas/CasProcessingFilterEntryPoint.java @@ -0,0 +1,102 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.ui.cas; + +import net.sf.acegisecurity.intercept.web.AuthenticationEntryPoint; + +import org.springframework.beans.factory.InitializingBean; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; + + +/** + * Used by the SecurityEnforcementFilter to commence + * authentication via the Yale Central Authentication Service (CAS). + * + *

+ * The user's browser will be redirected to the Yale CAS enterprise-wide login + * page. This page is specified by the loginUrl property. Once + * login is complete, the CAS login page will redirect to the page indicated + * by the service property. The service is a HTTP + * URL belonging to the current application. The service URL is + * monitored by the {@link CasProcessingFilter}, which will validate the CAS + * login was successful. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public class CasProcessingFilterEntryPoint implements AuthenticationEntryPoint, + InitializingBean { + //~ Instance fields ======================================================== + + private ServiceProperties serviceProperties; + private String loginUrl; + + //~ Methods ================================================================ + + public void setLoginUrl(String loginUrl) { + this.loginUrl = loginUrl; + } + + /** + * The enterprise-wide CAS login URL. Usually something like + * https://www.mycompany.com/cas/login. + * + * @return the enterprise-wide CAS login URL + */ + public String getLoginUrl() { + return loginUrl; + } + + public void setServiceProperties(ServiceProperties serviceProperties) { + this.serviceProperties = serviceProperties; + } + + public ServiceProperties getServiceProperties() { + return serviceProperties; + } + + public void afterPropertiesSet() throws Exception { + if ((loginUrl == null) || "".equals(loginUrl)) { + throw new IllegalArgumentException("loginUrl must be specified"); + } + + if (serviceProperties == null) { + throw new IllegalArgumentException( + "serviceProperties must be specified"); + } + } + + public void commence(ServletRequest request, ServletResponse response) + throws IOException, ServletException { + String url; + + if (serviceProperties.isSendRenew()) { + url = loginUrl + "?renew=true" + "&service=" + + serviceProperties.getService(); + } else { + url = loginUrl + "?service=" + serviceProperties.getService(); + } + + ((HttpServletResponse) response).sendRedirect(url); + } +} diff --git a/core/src/main/java/org/acegisecurity/ui/cas/ServiceProperties.java b/core/src/main/java/org/acegisecurity/ui/cas/ServiceProperties.java new file mode 100644 index 0000000000..d87258a8af --- /dev/null +++ b/core/src/main/java/org/acegisecurity/ui/cas/ServiceProperties.java @@ -0,0 +1,86 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.ui.cas; + +import org.springframework.beans.factory.InitializingBean; + + +/** + * Stores properties related to this CAS service. + * + *

+ * Each web application capable of processing CAS tickets is known as a + * service. This class stores the properties that are relevant to the local + * CAS service, being the application that is being secured by the Acegi + * Security System for Spring. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public class ServiceProperties implements InitializingBean { + //~ Instance fields ======================================================== + + private String service; + + private boolean sendRenew = false; + + //~ Methods ================================================================ + + public void setSendRenew(boolean sendRenew) { + this.sendRenew = sendRenew; + } + + /** + * Indicates whether the renew parameter should be sent to the + * CAS login URL and CAS validation URL. + *

If true, it will + * force CAS to authenticate the user again (even if the user has + * previously authenticated). During ticket validation it will require the + * ticket was generated as a consequence of an explicit login. High + * security applications would probably set this to true. + * Defaults to false, providing automated single sign on. + * + * @return whether to send the renew parameter to CAS + */ + public boolean isSendRenew() { + return sendRenew; + } + + public void setService(String service) { + this.service = service; + } + + /** + * Represents the service the user is authenticating to. + * + * This service is the callback URL + * belonging to the local Acegi Security System for Spring secured + * application. For example, + * https://www.mycompany.com/application/j_acegi_cas_security_check + * + * @return the URL of the service the user is authenticating to + */ + public String getService() { + return service; + } + + public void afterPropertiesSet() throws Exception { + if ((service == null) || "".equals(service)) { + throw new IllegalArgumentException("service must be specified"); + } + } +} diff --git a/core/src/main/java/org/acegisecurity/ui/cas/package.html b/core/src/main/java/org/acegisecurity/ui/cas/package.html new file mode 100644 index 0000000000..d327cda63c --- /dev/null +++ b/core/src/main/java/org/acegisecurity/ui/cas/package.html @@ -0,0 +1,6 @@ + + +Authenticates standard web browser users via +Yale Central Authentication Service (CAS). + + diff --git a/core/src/main/java/org/acegisecurity/userdetails/User.java b/core/src/main/java/org/acegisecurity/userdetails/User.java index 45c3e532eb..e5c5813c26 100644 --- a/core/src/main/java/org/acegisecurity/userdetails/User.java +++ b/core/src/main/java/org/acegisecurity/userdetails/User.java @@ -15,6 +15,8 @@ package net.sf.acegisecurity.providers.dao; +import java.io.Serializable; + import net.sf.acegisecurity.GrantedAuthority; @@ -24,7 +26,7 @@ import net.sf.acegisecurity.GrantedAuthority; * @author Ben Alex * @version $Id$ */ -public class User { +public class User implements Serializable { //~ Instance fields ======================================================== private String password; @@ -53,9 +55,10 @@ public class User { */ public User(String username, String password, boolean enabled, GrantedAuthority[] authorities) throws IllegalArgumentException { - if ((username == null) || (password == null) || (authorities == null)) { + if (((username == null) || "".equals(username)) || (password == null) + || "".equals(password) || (authorities == null)) { throw new IllegalArgumentException( - "Cannot pass null values to constructor"); + "Cannot pass null or empty values to constructor"); } for (int i = 0; i < authorities.length; i++) { diff --git a/core/src/test/java/org/acegisecurity/MockHttpServletRequest.java b/core/src/test/java/org/acegisecurity/MockHttpServletRequest.java index 6691240418..2a89e02b95 100644 --- a/core/src/test/java/org/acegisecurity/MockHttpServletRequest.java +++ b/core/src/test/java/org/acegisecurity/MockHttpServletRequest.java @@ -47,7 +47,7 @@ import javax.servlet.http.HttpSession; public class MockHttpServletRequest implements HttpServletRequest { //~ Instance fields ======================================================== - private HttpSession session; + private HttpSession session = new MockHttpSession(); private Map headersMap = new HashMap(); private Map paramMap = new HashMap(); private Principal principal; diff --git a/core/src/test/java/org/acegisecurity/providers/cas/CasAuthenticationProviderTests.java b/core/src/test/java/org/acegisecurity/providers/cas/CasAuthenticationProviderTests.java new file mode 100644 index 0000000000..139a8bd1e9 --- /dev/null +++ b/core/src/test/java/org/acegisecurity/providers/cas/CasAuthenticationProviderTests.java @@ -0,0 +1,404 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.providers.cas; + +import junit.framework.TestCase; + +import net.sf.acegisecurity.Authentication; +import net.sf.acegisecurity.AuthenticationException; +import net.sf.acegisecurity.BadCredentialsException; +import net.sf.acegisecurity.GrantedAuthority; +import net.sf.acegisecurity.GrantedAuthorityImpl; +import net.sf.acegisecurity.providers.TestingAuthenticationToken; +import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken; +import net.sf.acegisecurity.providers.cas.ticketvalidator.AbstractTicketValidator; +import net.sf.acegisecurity.ui.cas.CasProcessingFilter; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Vector; + + +/** + * Tests {@link CasAuthenticationProvider}. + * + * @author Ben Alex + * @version $Id$ + */ +public class CasAuthenticationProviderTests extends TestCase { + //~ Constructors =========================================================== + + public CasAuthenticationProviderTests() { + super(); + } + + public CasAuthenticationProviderTests(String arg0) { + super(arg0); + } + + //~ Methods ================================================================ + + public final void setUp() throws Exception { + super.setUp(); + } + + public static void main(String[] args) { + junit.textui.TestRunner.run(CasAuthenticationProviderTests.class); + } + + public void testAuthenticateStateful() throws Exception { + CasAuthenticationProvider cap = new CasAuthenticationProvider(); + cap.setCasAuthoritiesPopulator(new MockAuthoritiesPopulator()); + cap.setCasProxyDecider(new MockProxyDecider(true)); + cap.setKey("qwerty"); + + StatelessTicketCache cache = new MockStatelessTicketCache(); + cap.setStatelessTicketCache(cache); + cap.setTicketValidator(new MockTicketValidator(true)); + cap.afterPropertiesSet(); + + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(CasProcessingFilter.CAS_STATEFUL_IDENTIFIER, + "ST-123"); + + Authentication result = cap.authenticate(token); + + // Confirm ST-123 was NOT added to the cache + assertTrue(cache.getByTicketId("ST-456") == null); + + if (!(result instanceof CasAuthenticationToken)) { + fail("Should have returned a CasAuthenticationToken"); + } + + CasAuthenticationToken casResult = (CasAuthenticationToken) result; + assertEquals("marissa", casResult.getPrincipal()); + assertEquals("PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt", + casResult.getProxyGrantingTicketIou()); + assertEquals("https://localhost/portal/j_acegi_cas_security_check", + casResult.getProxyList().get(0)); + assertEquals("ST-123", casResult.getCredentials()); + assertEquals(new GrantedAuthorityImpl("ROLE_A"), + casResult.getAuthorities()[0]); + assertEquals(new GrantedAuthorityImpl("ROLE_B"), + casResult.getAuthorities()[1]); + assertEquals(cap.getKey().hashCode(), casResult.getKeyHash()); + + // Now confirm the CasAuthenticationToken is automatically re-accepted. + // To ensure TicketValidator not called again, set it to deliver an exception... + cap.setTicketValidator(new MockTicketValidator(false)); + + Authentication laterResult = cap.authenticate(result); + assertEquals(result, laterResult); + } + + public void testAuthenticateStateless() throws Exception { + CasAuthenticationProvider cap = new CasAuthenticationProvider(); + cap.setCasAuthoritiesPopulator(new MockAuthoritiesPopulator()); + cap.setCasProxyDecider(new MockProxyDecider(true)); + cap.setKey("qwerty"); + + StatelessTicketCache cache = new MockStatelessTicketCache(); + cap.setStatelessTicketCache(cache); + cap.setTicketValidator(new MockTicketValidator(true)); + cap.afterPropertiesSet(); + + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(CasProcessingFilter.CAS_STATELESS_IDENTIFIER, + "ST-456"); + + Authentication result = cap.authenticate(token); + + // Confirm ST-456 was added to the cache + assertTrue(cache.getByTicketId("ST-456") != null); + + if (!(result instanceof CasAuthenticationToken)) { + fail("Should have returned a CasAuthenticationToken"); + } + + assertEquals("marissa", result.getPrincipal()); + assertEquals("ST-456", result.getCredentials()); + + // Now try to authenticate again. To ensure TicketValidator not + // called again, set it to deliver an exception... + cap.setTicketValidator(new MockTicketValidator(false)); + + // Previously created UsernamePasswordAuthenticationToken is OK + Authentication newResult = cap.authenticate(token); + assertEquals("marissa", newResult.getPrincipal()); + assertEquals("ST-456", newResult.getCredentials()); + } + + public void testDetectsAMissingTicketId() throws Exception { + CasAuthenticationProvider cap = new CasAuthenticationProvider(); + cap.setCasAuthoritiesPopulator(new MockAuthoritiesPopulator()); + cap.setCasProxyDecider(new MockProxyDecider(true)); + cap.setKey("qwerty"); + + StatelessTicketCache cache = new MockStatelessTicketCache(); + cap.setStatelessTicketCache(cache); + cap.setTicketValidator(new MockTicketValidator(true)); + cap.afterPropertiesSet(); + + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(CasProcessingFilter.CAS_STATEFUL_IDENTIFIER, + ""); + + try { + Authentication result = cap.authenticate(token); + fail("Should have thrown BadCredentialsException"); + } catch (BadCredentialsException expected) { + assertEquals("Failed to provide a CAS service ticket to validate", + expected.getMessage()); + } + } + + public void testDetectsAnInvalidKey() throws Exception { + CasAuthenticationProvider cap = new CasAuthenticationProvider(); + cap.setCasAuthoritiesPopulator(new MockAuthoritiesPopulator()); + cap.setCasProxyDecider(new MockProxyDecider(true)); + cap.setKey("qwerty"); + + StatelessTicketCache cache = new MockStatelessTicketCache(); + cap.setStatelessTicketCache(cache); + cap.setTicketValidator(new MockTicketValidator(true)); + cap.afterPropertiesSet(); + + CasAuthenticationToken token = new CasAuthenticationToken("WRONG_KEY", + "test", "credentials", + new GrantedAuthority[] {new GrantedAuthorityImpl("XX")}, + new Vector(), "IOU-xxx"); + + try { + Authentication result = cap.authenticate(token); + fail("Should have thrown BadCredentialsException"); + } catch (BadCredentialsException expected) { + assertEquals("The presented CasAuthenticationToken does not contain the expected key", + expected.getMessage()); + } + } + + public void testDetectsMissingAuthoritiesPopulator() + throws Exception { + CasAuthenticationProvider cap = new CasAuthenticationProvider(); + cap.setCasProxyDecider(new MockProxyDecider()); + cap.setKey("qwerty"); + cap.setStatelessTicketCache(new MockStatelessTicketCache()); + cap.setTicketValidator(new MockTicketValidator(true)); + + try { + cap.afterPropertiesSet(); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("A casAuthoritiesPopulator must be set", + expected.getMessage()); + } + } + + public void testDetectsMissingKey() throws Exception { + CasAuthenticationProvider cap = new CasAuthenticationProvider(); + cap.setCasAuthoritiesPopulator(new MockAuthoritiesPopulator()); + cap.setCasProxyDecider(new MockProxyDecider()); + cap.setStatelessTicketCache(new MockStatelessTicketCache()); + cap.setTicketValidator(new MockTicketValidator(true)); + + try { + cap.afterPropertiesSet(); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("A Key is required so CasAuthenticationProvider can identify tokens it previously authenticated", + expected.getMessage()); + } + } + + public void testDetectsMissingProxyDecider() throws Exception { + CasAuthenticationProvider cap = new CasAuthenticationProvider(); + cap.setCasAuthoritiesPopulator(new MockAuthoritiesPopulator()); + cap.setKey("qwerty"); + cap.setStatelessTicketCache(new MockStatelessTicketCache()); + cap.setTicketValidator(new MockTicketValidator(true)); + + try { + cap.afterPropertiesSet(); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("A casProxyDecider must be set", expected.getMessage()); + } + } + + public void testDetectsMissingStatelessTicketCache() + throws Exception { + CasAuthenticationProvider cap = new CasAuthenticationProvider(); + cap.setCasAuthoritiesPopulator(new MockAuthoritiesPopulator()); + cap.setCasProxyDecider(new MockProxyDecider()); + cap.setKey("qwerty"); + cap.setTicketValidator(new MockTicketValidator(true)); + + try { + cap.afterPropertiesSet(); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("A statelessTicketCache must be set", + expected.getMessage()); + } + } + + public void testDetectsMissingTicketValidator() throws Exception { + CasAuthenticationProvider cap = new CasAuthenticationProvider(); + cap.setCasAuthoritiesPopulator(new MockAuthoritiesPopulator()); + cap.setCasProxyDecider(new MockProxyDecider(true)); + cap.setKey("qwerty"); + cap.setStatelessTicketCache(new MockStatelessTicketCache()); + + try { + cap.afterPropertiesSet(); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("A ticketValidator must be set", expected.getMessage()); + } + } + + public void testGettersSetters() throws Exception { + CasAuthenticationProvider cap = new CasAuthenticationProvider(); + cap.setCasAuthoritiesPopulator(new MockAuthoritiesPopulator()); + cap.setCasProxyDecider(new MockProxyDecider()); + cap.setKey("qwerty"); + cap.setStatelessTicketCache(new MockStatelessTicketCache()); + cap.setTicketValidator(new MockTicketValidator(true)); + cap.afterPropertiesSet(); + + assertTrue(cap.getCasAuthoritiesPopulator() != null); + assertTrue(cap.getCasProxyDecider() != null); + assertEquals("qwerty", cap.getKey()); + assertTrue(cap.getStatelessTicketCache() != null); + assertTrue(cap.getTicketValidator() != null); + } + + public void testIgnoresClassesItDoesNotSupport() throws Exception { + CasAuthenticationProvider cap = new CasAuthenticationProvider(); + cap.setCasAuthoritiesPopulator(new MockAuthoritiesPopulator()); + cap.setCasProxyDecider(new MockProxyDecider()); + cap.setKey("qwerty"); + cap.setStatelessTicketCache(new MockStatelessTicketCache()); + cap.setTicketValidator(new MockTicketValidator(true)); + cap.afterPropertiesSet(); + + TestingAuthenticationToken token = new TestingAuthenticationToken("user", + "password", + new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_A")}); + assertFalse(cap.supports(TestingAuthenticationToken.class)); + + // Try it anyway + assertEquals(null, cap.authenticate(token)); + } + + public void testIgnoresUsernamePasswordAuthenticationTokensWithoutCasIdentifiersAsPrincipal() + throws Exception { + CasAuthenticationProvider cap = new CasAuthenticationProvider(); + cap.setCasAuthoritiesPopulator(new MockAuthoritiesPopulator()); + cap.setCasProxyDecider(new MockProxyDecider()); + cap.setKey("qwerty"); + cap.setStatelessTicketCache(new MockStatelessTicketCache()); + cap.setTicketValidator(new MockTicketValidator(true)); + cap.afterPropertiesSet(); + + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("some_normal_user", + "password", + new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_A")}); + assertEquals(null, cap.authenticate(token)); + } + + public void testSupports() { + CasAuthenticationProvider cap = new CasAuthenticationProvider(); + assertTrue(cap.supports(UsernamePasswordAuthenticationToken.class)); + assertTrue(cap.supports(CasAuthenticationToken.class)); + } + + //~ Inner Classes ========================================================== + + private class MockAuthoritiesPopulator implements CasAuthoritiesPopulator { + public GrantedAuthority[] getAuthorities(String casUserId) + throws AuthenticationException { + return new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_A"), new GrantedAuthorityImpl( + "ROLE_B")}; + } + } + + private class MockProxyDecider implements CasProxyDecider { + private boolean acceptProxy; + + public MockProxyDecider(boolean acceptProxy) { + this.acceptProxy = acceptProxy; + } + + private MockProxyDecider() { + super(); + } + + public void confirmProxyListTrusted(List proxyList) + throws ProxyUntrustedException { + if (acceptProxy) { + return; + } else { + throw new ProxyUntrustedException("As requested from mock"); + } + } + } + + private class MockStatelessTicketCache implements StatelessTicketCache { + private Map cache = new HashMap(); + + public CasAuthenticationToken getByTicketId(String serviceTicket) { + return (CasAuthenticationToken) cache.get(serviceTicket); + } + + public void putTicketInCache(CasAuthenticationToken token) { + cache.put(token.getCredentials().toString(), token); + } + + public void removeTicketFromCache(CasAuthenticationToken token) { + throw new UnsupportedOperationException( + "mock method not implemented"); + } + + public void removeTicketFromCache(String serviceTicket) { + throw new UnsupportedOperationException( + "mock method not implemented"); + } + } + + private class MockTicketValidator extends AbstractTicketValidator { + private boolean returnTicket; + + public MockTicketValidator(boolean returnTicket) { + this.returnTicket = returnTicket; + } + + private MockTicketValidator() { + super(); + } + + public TicketResponse confirmTicketValid(String serviceTicket) + throws AuthenticationException { + if (returnTicket) { + List list = new Vector(); + list.add("https://localhost/portal/j_acegi_cas_security_check"); + + return new TicketResponse("marissa", list, + "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt"); + } + + throw new BadCredentialsException("As requested from mock"); + } + } +} diff --git a/core/src/test/java/org/acegisecurity/providers/cas/CasAuthenticationTokenTests.java b/core/src/test/java/org/acegisecurity/providers/cas/CasAuthenticationTokenTests.java new file mode 100644 index 0000000000..61f954e81d --- /dev/null +++ b/core/src/test/java/org/acegisecurity/providers/cas/CasAuthenticationTokenTests.java @@ -0,0 +1,283 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.providers.cas; + +import junit.framework.TestCase; + +import net.sf.acegisecurity.GrantedAuthority; +import net.sf.acegisecurity.GrantedAuthorityImpl; +import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken; + +import java.util.List; +import java.util.Vector; + + +/** + * Tests {@link CasAuthenticationToken}. + * + * @author Ben Alex + * @version $Id$ + */ +public class CasAuthenticationTokenTests extends TestCase { + //~ Constructors =========================================================== + + public CasAuthenticationTokenTests() { + super(); + } + + public CasAuthenticationTokenTests(String arg0) { + super(arg0); + } + + //~ Methods ================================================================ + + public final void setUp() throws Exception { + super.setUp(); + } + + public static void main(String[] args) { + junit.textui.TestRunner.run(CasAuthenticationTokenTests.class); + } + + public void testConstructorRejectsNulls() { + try { + new CasAuthenticationToken(null, "Test", "Password", + new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( + "ROLE_TWO")}, new Vector(), + "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt"); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertTrue(true); + } + + try { + new CasAuthenticationToken("key", null, "Password", + new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( + "ROLE_TWO")}, new Vector(), + "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt"); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertTrue(true); + } + + try { + new CasAuthenticationToken("key", "Test", null, + new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( + "ROLE_TWO")}, new Vector(), + "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt"); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertTrue(true); + } + + try { + new CasAuthenticationToken("key", "Test", "Password", null, + new Vector(), + "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt"); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertTrue(true); + } + + try { + new CasAuthenticationToken("key", "Test", "Password", + new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( + "ROLE_TWO")}, null, + "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt"); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertTrue(true); + } + + try { + new CasAuthenticationToken("key", "Test", "Password", + new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( + "ROLE_TWO")}, new Vector(), null); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertTrue(true); + } + + try { + new CasAuthenticationToken("key", "Test", "Password", + new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), null, new GrantedAuthorityImpl( + "ROLE_TWO")}, new Vector(), + "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt"); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertTrue(true); + } + } + + public void testEqualsWhenEqual() { + List proxyList1 = new Vector(); + proxyList1.add("https://localhost/newPortal/j_acegi_cas_security_check"); + + CasAuthenticationToken token1 = new CasAuthenticationToken("key", + "Test", "Password", + new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( + "ROLE_TWO")}, proxyList1, + "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt"); + + List proxyList2 = new Vector(); + proxyList2.add("https://localhost/newPortal/j_acegi_cas_security_check"); + + CasAuthenticationToken token2 = new CasAuthenticationToken("key", + "Test", "Password", + new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( + "ROLE_TWO")}, proxyList2, + "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt"); + + assertEquals(token1, token2); + } + + public void testGetters() { + // Build the proxy list returned in the ticket from CAS + List proxyList = new Vector(); + proxyList.add("https://localhost/newPortal/j_acegi_cas_security_check"); + + CasAuthenticationToken token = new CasAuthenticationToken("key", + "Test", "Password", + new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( + "ROLE_TWO")}, proxyList, + "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt"); + assertEquals("key".hashCode(), token.getKeyHash()); + assertEquals("Test", token.getPrincipal()); + assertEquals("Password", token.getCredentials()); + assertEquals("ROLE_ONE", token.getAuthorities()[0].getAuthority()); + assertEquals("ROLE_TWO", token.getAuthorities()[1].getAuthority()); + assertEquals("PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt", + token.getProxyGrantingTicketIou()); + assertEquals(proxyList, token.getProxyList()); + } + + public void testNoArgConstructor() { + try { + new CasAuthenticationToken(); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertTrue(true); + } + } + + public void testNotEqualsDueToAbstractParentEqualsCheck() { + List proxyList1 = new Vector(); + proxyList1.add("https://localhost/newPortal/j_acegi_cas_security_check"); + + CasAuthenticationToken token1 = new CasAuthenticationToken("key", + "Test", "Password", + new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( + "ROLE_TWO")}, proxyList1, + "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt"); + + List proxyList2 = new Vector(); + proxyList2.add("https://localhost/newPortal/j_acegi_cas_security_check"); + + CasAuthenticationToken token2 = new CasAuthenticationToken("key", + "OTHER_VALUE", "Password", + new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( + "ROLE_TWO")}, proxyList2, + "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt"); + + assertTrue(!token1.equals(token2)); + } + + public void testNotEqualsDueToDifferentAuthenticationClass() { + List proxyList1 = new Vector(); + proxyList1.add("https://localhost/newPortal/j_acegi_cas_security_check"); + + CasAuthenticationToken token1 = new CasAuthenticationToken("key", + "Test", "Password", + new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( + "ROLE_TWO")}, proxyList1, + "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt"); + + UsernamePasswordAuthenticationToken token2 = new UsernamePasswordAuthenticationToken("Test", + "Password", + new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( + "ROLE_TWO")}); + token2.setAuthenticated(true); + + assertTrue(!token1.equals(token2)); + } + + public void testNotEqualsDueToProxyGrantingTicket() { + List proxyList1 = new Vector(); + proxyList1.add("https://localhost/newPortal/j_acegi_cas_security_check"); + + CasAuthenticationToken token1 = new CasAuthenticationToken("key", + "Test", "Password", + new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( + "ROLE_TWO")}, proxyList1, + "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt"); + + List proxyList2 = new Vector(); + proxyList2.add("https://localhost/newPortal/j_acegi_cas_security_check"); + + CasAuthenticationToken token2 = new CasAuthenticationToken("key", + "Test", "Password", + new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( + "ROLE_TWO")}, proxyList2, "PGTIOU-SOME_OTHER_VALUE"); + + assertTrue(!token1.equals(token2)); + } + + public void testNotEqualsDueToProxyList() { + List proxyList1 = new Vector(); + proxyList1.add("https://localhost/newPortal/j_acegi_cas_security_check"); + + CasAuthenticationToken token1 = new CasAuthenticationToken("key", + "Test", "Password", + new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( + "ROLE_TWO")}, proxyList1, + "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt"); + + List proxyList2 = new Vector(); + proxyList2.add( + "https://localhost/SOME_OTHER_PORTAL/j_acegi_cas_security_check"); + + CasAuthenticationToken token2 = new CasAuthenticationToken("key", + "Test", "Password", + new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( + "ROLE_TWO")}, proxyList2, + "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt"); + + assertTrue(!token1.equals(token2)); + } + + public void testSetAuthenticatedIgnored() { + CasAuthenticationToken token = new CasAuthenticationToken("key", + "Test", "Password", + new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( + "ROLE_TWO")}, new Vector(), + "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt"); + assertTrue(token.isAuthenticated()); + token.setAuthenticated(false); // ignored + assertTrue(token.isAuthenticated()); + } + + public void testToString() { + CasAuthenticationToken token = new CasAuthenticationToken("key", + "Test", "Password", + new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( + "ROLE_TWO")}, new Vector(), + "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt"); + String result = token.toString(); + assertTrue(result.lastIndexOf("Proxy List:") != -1); + assertTrue(result.lastIndexOf("Proxy-Granting Ticket IOU:") != -1); + assertTrue(result.lastIndexOf("Credentials (Service/Proxy Ticket):") != -1); + } +} diff --git a/core/src/test/java/org/acegisecurity/providers/cas/TicketResponseTests.java b/core/src/test/java/org/acegisecurity/providers/cas/TicketResponseTests.java new file mode 100644 index 0000000000..ed2d121900 --- /dev/null +++ b/core/src/test/java/org/acegisecurity/providers/cas/TicketResponseTests.java @@ -0,0 +1,102 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.providers.cas; + +import junit.framework.TestCase; + +import java.util.List; +import java.util.Vector; + + +/** + * Tests {@link TicketResponse}. + * + * @author Ben Alex + * @version $Id$ + */ +public class TicketResponseTests extends TestCase { + //~ Constructors =========================================================== + + public TicketResponseTests() { + super(); + } + + public TicketResponseTests(String arg0) { + super(arg0); + } + + //~ Methods ================================================================ + + public final void setUp() throws Exception { + super.setUp(); + } + + public static void main(String[] args) { + junit.textui.TestRunner.run(TicketResponseTests.class); + } + + public void testConstructorAcceptsNullProxyGrantingTicketIOU() { + TicketResponse ticket = new TicketResponse("marissa", new Vector(), null); + assertEquals("", ticket.getProxyGrantingTicketIou()); + } + + public void testConstructorAcceptsNullProxyList() { + TicketResponse ticket = new TicketResponse("marissa", null, + "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt"); + assertEquals(new Vector(), ticket.getProxyList()); + } + + public void testConstructorRejectsNullUser() { + try { + new TicketResponse(null, new Vector(), + "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt"); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertTrue(true); + } + } + + public void testGetters() { + // Build the proxy list returned in the ticket from CAS + List proxyList = new Vector(); + proxyList.add("https://localhost/newPortal/j_acegi_cas_security_check"); + + TicketResponse ticket = new TicketResponse("marissa", proxyList, + "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt"); + assertEquals("marissa", ticket.getUser()); + assertEquals(proxyList, ticket.getProxyList()); + assertEquals("PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt", + ticket.getProxyGrantingTicketIou()); + } + + public void testNoArgConstructor() { + try { + new TicketResponse(); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertTrue(true); + } + } + + public void testToString() { + TicketResponse ticket = new TicketResponse("marissa", null, + "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt"); + String result = ticket.toString(); + assertTrue(result.lastIndexOf("Proxy List:") != -1); + assertTrue(result.lastIndexOf("Proxy-Granting Ticket IOU:") != -1); + assertTrue(result.lastIndexOf("User:") != -1); + } +} diff --git a/core/src/test/java/org/acegisecurity/providers/cas/cache/EhCacheBasedTicketCacheTests.java b/core/src/test/java/org/acegisecurity/providers/cas/cache/EhCacheBasedTicketCacheTests.java new file mode 100644 index 0000000000..d88efe42b3 --- /dev/null +++ b/core/src/test/java/org/acegisecurity/providers/cas/cache/EhCacheBasedTicketCacheTests.java @@ -0,0 +1,89 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.providers.cas.cache; + +import junit.framework.TestCase; + +import net.sf.acegisecurity.GrantedAuthority; +import net.sf.acegisecurity.GrantedAuthorityImpl; +import net.sf.acegisecurity.providers.cas.CasAuthenticationToken; + +import java.util.List; +import java.util.Vector; + + +/** + * Tests {@link EhCacheBasedTicketCache}. + * + * @author Ben Alex + * @version $Id$ + */ +public class EhCacheBasedTicketCacheTests extends TestCase { + //~ Constructors =========================================================== + + public EhCacheBasedTicketCacheTests() { + super(); + } + + public EhCacheBasedTicketCacheTests(String arg0) { + super(arg0); + } + + //~ Methods ================================================================ + + public final void setUp() throws Exception { + super.setUp(); + } + + public static void main(String[] args) { + junit.textui.TestRunner.run(EhCacheBasedTicketCacheTests.class); + } + + public void testCacheOperation() throws Exception { + EhCacheBasedTicketCache cache = new EhCacheBasedTicketCache(); + cache.afterPropertiesSet(); + + // Check it gets stored in the cache + cache.putTicketInCache(getToken()); + assertEquals(getToken(), + cache.getByTicketId("ST-0-ER94xMJmn6pha35CQRoZ")); + + // Check it gets removed from the cache + cache.removeTicketFromCache(getToken()); + assertNull(cache.getByTicketId("ST-0-ER94xMJmn6pha35CQRoZ")); + + // Check it doesn't return values for null or unknown service tickets + assertNull(cache.getByTicketId(null)); + assertNull(cache.getByTicketId("UNKNOWN_SERVICE_TICKET")); + } + + public void testGettersSetters() { + EhCacheBasedTicketCache cache = new EhCacheBasedTicketCache(); + cache.setMinutesToIdle(5); + assertEquals(5, cache.getMinutesToIdle()); + } + + private CasAuthenticationToken getToken() { + List proxyList = new Vector(); + proxyList.add("https://localhost/newPortal/j_acegi_cas_security_check"); + + return new CasAuthenticationToken("key", "marissa", + "ST-0-ER94xMJmn6pha35CQRoZ", + new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( + "ROLE_TWO")}, proxyList, + "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt"); + } +} diff --git a/core/src/test/java/org/acegisecurity/providers/cas/populator/DaoCasAuthoritiesPopulatorTests.java b/core/src/test/java/org/acegisecurity/providers/cas/populator/DaoCasAuthoritiesPopulatorTests.java new file mode 100644 index 0000000000..71ea54160d --- /dev/null +++ b/core/src/test/java/org/acegisecurity/providers/cas/populator/DaoCasAuthoritiesPopulatorTests.java @@ -0,0 +1,148 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.providers.cas.populator; + +import junit.framework.TestCase; + +import net.sf.acegisecurity.GrantedAuthority; +import net.sf.acegisecurity.GrantedAuthorityImpl; +import net.sf.acegisecurity.providers.dao.AuthenticationDao; +import net.sf.acegisecurity.providers.dao.User; +import net.sf.acegisecurity.providers.dao.UsernameNotFoundException; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataRetrievalFailureException; + + +/** + * Tests {@link DaoCasAuthoritiesPopulator}. + * + * @author Ben Alex + * @version $Id$ + */ +public class DaoCasAuthoritiesPopulatorTests extends TestCase { + //~ Constructors =========================================================== + + public DaoCasAuthoritiesPopulatorTests() { + super(); + } + + public DaoCasAuthoritiesPopulatorTests(String arg0) { + super(arg0); + } + + //~ Methods ================================================================ + + public final void setUp() throws Exception { + super.setUp(); + } + + public static void main(String[] args) { + junit.textui.TestRunner.run(DaoCasAuthoritiesPopulatorTests.class); + } + + public void testDetectsMissingAuthenticationDao() throws Exception { + DaoCasAuthoritiesPopulator populator = new DaoCasAuthoritiesPopulator(); + + try { + populator.afterPropertiesSet(); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("An authenticationDao must be set", + expected.getMessage()); + } + } + + public void testGetGrantedAuthoritiesForInvalidUsername() + throws Exception { + DaoCasAuthoritiesPopulator populator = new DaoCasAuthoritiesPopulator(); + populator.setAuthenticationDao(new MockAuthenticationDaoUserMarissa()); + populator.afterPropertiesSet(); + + try { + populator.getAuthorities("scott"); + fail("Should have thrown UsernameNotFoundException"); + } catch (UsernameNotFoundException expected) { + assertTrue(true); + } + } + + public void testGetGrantedAuthoritiesForValidUsername() + throws Exception { + DaoCasAuthoritiesPopulator populator = new DaoCasAuthoritiesPopulator(); + populator.setAuthenticationDao(new MockAuthenticationDaoUserMarissa()); + populator.afterPropertiesSet(); + + GrantedAuthority[] results = populator.getAuthorities("marissa"); + assertEquals(2, results.length); + assertEquals(new GrantedAuthorityImpl("ROLE_ONE"), results[0]); + assertEquals(new GrantedAuthorityImpl("ROLE_TWO"), results[1]); + } + + public void testGetGrantedAuthoritiesWhenDaoThrowsException() + throws Exception { + DaoCasAuthoritiesPopulator populator = new DaoCasAuthoritiesPopulator(); + populator.setAuthenticationDao(new MockAuthenticationDaoSimulateBackendError()); + populator.afterPropertiesSet(); + + try { + populator.getAuthorities("THE_DAO_WILL_FAIL"); + fail("Should have thrown DataRetrievalFailureException"); + } catch (DataRetrievalFailureException expected) { + assertTrue(true); + } + } + + public void testGettersSetters() { + DaoCasAuthoritiesPopulator populator = new DaoCasAuthoritiesPopulator(); + AuthenticationDao dao = new MockAuthenticationDaoUserMarissa(); + populator.setAuthenticationDao(dao); + assertEquals(dao, populator.getAuthenticationDao()); + } + + //~ Inner Classes ========================================================== + + private class MockAuthenticationDaoSimulateBackendError + implements AuthenticationDao { + public long getRefreshDuration() { + return 0; + } + + public User loadUserByUsername(String username) + throws UsernameNotFoundException, DataAccessException { + throw new DataRetrievalFailureException( + "This mock simulator is designed to fail"); + } + } + + private class MockAuthenticationDaoUserMarissa implements AuthenticationDao { + public long getRefreshDuration() { + return 0; + } + + public User loadUserByUsername(String username) + throws UsernameNotFoundException, DataAccessException { + if ("marissa".equals(username)) { + return new User("marissa", "koala", true, + new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( + "ROLE_TWO")}); + } else { + throw new UsernameNotFoundException("Could not find: " + + username); + } + } + } +} diff --git a/core/src/test/java/org/acegisecurity/providers/cas/proxy/AcceptAnyCasProxyTests.java b/core/src/test/java/org/acegisecurity/providers/cas/proxy/AcceptAnyCasProxyTests.java new file mode 100644 index 0000000000..57667badc7 --- /dev/null +++ b/core/src/test/java/org/acegisecurity/providers/cas/proxy/AcceptAnyCasProxyTests.java @@ -0,0 +1,66 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.providers.cas.proxy; + +import junit.framework.TestCase; + +import java.util.Vector; + + +/** + * Tests {@link AcceptAnyCasProxy}. + * + * @author Ben Alex + * @version $Id$ + */ +public class AcceptAnyCasProxyTests extends TestCase { + //~ Constructors =========================================================== + + public AcceptAnyCasProxyTests() { + super(); + } + + public AcceptAnyCasProxyTests(String arg0) { + super(arg0); + } + + //~ Methods ================================================================ + + public final void setUp() throws Exception { + super.setUp(); + } + + public static void main(String[] args) { + junit.textui.TestRunner.run(AcceptAnyCasProxyTests.class); + } + + public void testDoesNotAcceptNull() { + AcceptAnyCasProxy proxyDecider = new AcceptAnyCasProxy(); + + try { + proxyDecider.confirmProxyListTrusted(null); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("proxyList cannot be null", expected.getMessage()); + } + } + + public void testNormalOperation() { + AcceptAnyCasProxy proxyDecider = new AcceptAnyCasProxy(); + proxyDecider.confirmProxyListTrusted(new Vector()); + assertTrue(true); // as no Exception thrown + } +} diff --git a/core/src/test/java/org/acegisecurity/providers/cas/proxy/NamedCasProxyDeciderTests.java b/core/src/test/java/org/acegisecurity/providers/cas/proxy/NamedCasProxyDeciderTests.java new file mode 100644 index 0000000000..f4074f3e2d --- /dev/null +++ b/core/src/test/java/org/acegisecurity/providers/cas/proxy/NamedCasProxyDeciderTests.java @@ -0,0 +1,141 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.providers.cas.proxy; + +import junit.framework.TestCase; + +import net.sf.acegisecurity.providers.cas.ProxyUntrustedException; + +import java.util.List; +import java.util.Vector; + + +/** + * Tests {@link NamedCasProxyDecider}. + * + * @author Ben Alex + * @version $Id$ + */ +public class NamedCasProxyDeciderTests extends TestCase { + //~ Constructors =========================================================== + + public NamedCasProxyDeciderTests() { + super(); + } + + public NamedCasProxyDeciderTests(String arg0) { + super(arg0); + } + + //~ Methods ================================================================ + + public final void setUp() throws Exception { + super.setUp(); + } + + public static void main(String[] args) { + junit.textui.TestRunner.run(NamedCasProxyDeciderTests.class); + } + + public void testAcceptsIfNearestProxyIsAuthorized() + throws Exception { + NamedCasProxyDecider proxyDecider = new NamedCasProxyDecider(); + + // Build the ticket returned from CAS + List proxyList = new Vector(); + proxyList.add("https://localhost/newPortal/j_acegi_cas_security_check"); + + // Build the list of valid nearest proxies + List validProxies = new Vector(); + validProxies.add("https://localhost/portal/j_acegi_cas_security_check"); + validProxies.add( + "https://localhost/newPortal/j_acegi_cas_security_check"); + proxyDecider.setValidProxies(validProxies); + proxyDecider.afterPropertiesSet(); + + proxyDecider.confirmProxyListTrusted(proxyList); + assertTrue(true); + } + + public void testAcceptsIfNoProxiesInTicket() { + NamedCasProxyDecider proxyDecider = new NamedCasProxyDecider(); + List proxyList = new Vector(); // no proxies in list + + proxyDecider.confirmProxyListTrusted(proxyList); + assertTrue(true); + } + + public void testDetectsMissingValidProxiesList() throws Exception { + NamedCasProxyDecider proxyDecider = new NamedCasProxyDecider(); + + try { + proxyDecider.afterPropertiesSet(); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("A validProxies list must be set", + expected.getMessage()); + } + } + + public void testDoesNotAcceptNull() { + NamedCasProxyDecider proxyDecider = new NamedCasProxyDecider(); + + try { + proxyDecider.confirmProxyListTrusted(null); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("proxyList cannot be null", expected.getMessage()); + } + } + + public void testGettersSetters() { + NamedCasProxyDecider proxyDecider = new NamedCasProxyDecider(); + + // Build the list of valid nearest proxies + List validProxies = new Vector(); + validProxies.add("https://localhost/portal/j_acegi_cas_security_check"); + validProxies.add( + "https://localhost/newPortal/j_acegi_cas_security_check"); + proxyDecider.setValidProxies(validProxies); + + assertEquals(validProxies, proxyDecider.getValidProxies()); + } + + public void testRejectsIfNearestProxyIsNotAuthorized() + throws Exception { + NamedCasProxyDecider proxyDecider = new NamedCasProxyDecider(); + + // Build the ticket returned from CAS + List proxyList = new Vector(); + proxyList.add( + "https://localhost/untrustedWebApp/j_acegi_cas_security_check"); + + // Build the list of valid nearest proxies + List validProxies = new Vector(); + validProxies.add("https://localhost/portal/j_acegi_cas_security_check"); + validProxies.add( + "https://localhost/newPortal/j_acegi_cas_security_check"); + proxyDecider.setValidProxies(validProxies); + proxyDecider.afterPropertiesSet(); + + try { + proxyDecider.confirmProxyListTrusted(proxyList); + fail("Should have thrown ProxyUntrustedException"); + } catch (ProxyUntrustedException expected) { + assertTrue(true); + } + } +} diff --git a/core/src/test/java/org/acegisecurity/providers/cas/proxy/RejectProxyTicketsTests.java b/core/src/test/java/org/acegisecurity/providers/cas/proxy/RejectProxyTicketsTests.java new file mode 100644 index 0000000000..0fed184111 --- /dev/null +++ b/core/src/test/java/org/acegisecurity/providers/cas/proxy/RejectProxyTicketsTests.java @@ -0,0 +1,84 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.providers.cas.proxy; + +import junit.framework.TestCase; + +import net.sf.acegisecurity.providers.cas.ProxyUntrustedException; + +import java.util.List; +import java.util.Vector; + + +/** + * Tests {@link RejectProxyTickets}. + * + * @author Ben Alex + * @version $Id$ + */ +public class RejectProxyTicketsTests extends TestCase { + //~ Constructors =========================================================== + + public RejectProxyTicketsTests() { + super(); + } + + public RejectProxyTicketsTests(String arg0) { + super(arg0); + } + + //~ Methods ================================================================ + + public final void setUp() throws Exception { + super.setUp(); + } + + public static void main(String[] args) { + junit.textui.TestRunner.run(RejectProxyTicketsTests.class); + } + + public void testAcceptsIfNoProxiesInTicket() { + RejectProxyTickets proxyDecider = new RejectProxyTickets(); + List proxyList = new Vector(); // no proxies in list + + proxyDecider.confirmProxyListTrusted(proxyList); + assertTrue(true); + } + + public void testDoesNotAcceptNull() { + RejectProxyTickets proxyDecider = new RejectProxyTickets(); + + try { + proxyDecider.confirmProxyListTrusted(null); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("proxyList cannot be null", expected.getMessage()); + } + } + + public void testRejectsIfAnyProxyInList() { + RejectProxyTickets proxyDecider = new RejectProxyTickets(); + List proxyList = new Vector(); + proxyList.add("https://localhost/webApp/j_acegi_cas_security_check"); + + try { + proxyDecider.confirmProxyListTrusted(proxyList); + fail("Should have thrown ProxyUntrustedException"); + } catch (ProxyUntrustedException expected) { + assertTrue(true); + } + } +} diff --git a/core/src/test/java/org/acegisecurity/providers/cas/ticketvalidator/AbstractTicketValidatorTests.java b/core/src/test/java/org/acegisecurity/providers/cas/ticketvalidator/AbstractTicketValidatorTests.java new file mode 100644 index 0000000000..d24e2ffecc --- /dev/null +++ b/core/src/test/java/org/acegisecurity/providers/cas/ticketvalidator/AbstractTicketValidatorTests.java @@ -0,0 +1,143 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.providers.cas.ticketvalidator; + +import junit.framework.TestCase; + +import net.sf.acegisecurity.AuthenticationException; +import net.sf.acegisecurity.BadCredentialsException; +import net.sf.acegisecurity.providers.cas.TicketResponse; +import net.sf.acegisecurity.ui.cas.ServiceProperties; + +import java.util.Vector; + + +/** + * Tests {@link AbstractTicketValidator}. + * + * @author Ben Alex + * @version $Id$ + */ +public class AbstractTicketValidatorTests extends TestCase { + //~ Constructors =========================================================== + + public AbstractTicketValidatorTests() { + super(); + } + + public AbstractTicketValidatorTests(String arg0) { + super(arg0); + } + + //~ Methods ================================================================ + + public final void setUp() throws Exception { + super.setUp(); + } + + public static void main(String[] args) { + junit.textui.TestRunner.run(AbstractTicketValidatorTests.class); + } + + public void testDetectsMissingCasValidate() throws Exception { + AbstractTicketValidator tv = new MockAbstractTicketValidator(); + tv.setServiceProperties(new ServiceProperties()); + + try { + tv.afterPropertiesSet(); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("A casValidate URL must be set", expected.getMessage()); + } + } + + public void testDetectsMissingServiceProperties() throws Exception { + AbstractTicketValidator tv = new MockAbstractTicketValidator(); + tv.setCasValidate("https://company.com/cas/proxyvalidate"); + + try { + tv.afterPropertiesSet(); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("serviceProperties must be specified", + expected.getMessage()); + } + } + + public void testGetters() throws Exception { + AbstractTicketValidator tv = new MockAbstractTicketValidator(); + tv.setCasValidate("https://company.com/cas/proxyvalidate"); + assertEquals("https://company.com/cas/proxyvalidate", + tv.getCasValidate()); + + tv.setServiceProperties(new ServiceProperties()); + assertTrue(tv.getServiceProperties() != null); + + tv.afterPropertiesSet(); + + tv.setTrustStore("/some/file/cacerts"); + assertEquals("/some/file/cacerts", tv.getTrustStore()); + } + + public void testSystemPropertySetDuringAfterPropertiesSet() + throws Exception { + AbstractTicketValidator tv = new MockAbstractTicketValidator(); + tv.setCasValidate("https://company.com/cas/proxyvalidate"); + assertEquals("https://company.com/cas/proxyvalidate", + tv.getCasValidate()); + + tv.setServiceProperties(new ServiceProperties()); + assertTrue(tv.getServiceProperties() != null); + + tv.setTrustStore("/some/file/cacerts"); + assertEquals("/some/file/cacerts", tv.getTrustStore()); + + String before = System.getProperty("javax.net.ssl.trustStore"); + tv.afterPropertiesSet(); + assertEquals("/some/file/cacerts", + System.getProperty("javax.net.ssl.trustStore")); + + if (before == null) { + System.setProperty("javax.net.ssl.trustStore", ""); + } else { + System.setProperty("javax.net.ssl.trustStore", before); + } + } + + //~ Inner Classes ========================================================== + + private class MockAbstractTicketValidator extends AbstractTicketValidator { + private boolean returnTicket; + + public MockAbstractTicketValidator(boolean returnTicket) { + this.returnTicket = returnTicket; + } + + private MockAbstractTicketValidator() { + super(); + } + + public TicketResponse confirmTicketValid(String serviceTicket) + throws AuthenticationException { + if (returnTicket) { + return new TicketResponse("user", new Vector(), + "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt"); + } + + throw new BadCredentialsException("As requested by mock"); + } + } +} diff --git a/core/src/test/java/org/acegisecurity/providers/cas/ticketvalidator/CasProxyTicketValidatorTests.java b/core/src/test/java/org/acegisecurity/providers/cas/ticketvalidator/CasProxyTicketValidatorTests.java new file mode 100644 index 0000000000..92170205a7 --- /dev/null +++ b/core/src/test/java/org/acegisecurity/providers/cas/ticketvalidator/CasProxyTicketValidatorTests.java @@ -0,0 +1,138 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.providers.cas.ticketvalidator; + +import edu.yale.its.tp.cas.client.ProxyTicketValidator; + +import junit.framework.TestCase; + +import net.sf.acegisecurity.AuthenticationServiceException; +import net.sf.acegisecurity.BadCredentialsException; +import net.sf.acegisecurity.providers.cas.TicketResponse; +import net.sf.acegisecurity.ui.cas.ServiceProperties; + +import java.util.Vector; + + +/** + * Tests {@link CasProxyTicketValidator}. + * + * @author Ben Alex + * @version $Id$ + */ +public class CasProxyTicketValidatorTests extends TestCase { + //~ Constructors =========================================================== + + public CasProxyTicketValidatorTests() { + super(); + } + + public CasProxyTicketValidatorTests(String arg0) { + super(arg0); + } + + //~ Methods ================================================================ + + public final void setUp() throws Exception { + super.setUp(); + } + + public static void main(String[] args) { + junit.textui.TestRunner.run(CasProxyTicketValidatorTests.class); + } + + public void testGetters() { + CasProxyTicketValidator tv = new CasProxyTicketValidator(); + tv.setProxyCallbackUrl("http://my.com/webapp/casProxy/someValidator"); + assertEquals("http://my.com/webapp/casProxy/someValidator", + tv.getProxyCallbackUrl()); + } + + public void testNormalOperation() { + ServiceProperties sp = new ServiceProperties(); + sp.setSendRenew(true); + sp.setService("https://my.com/webapp//j_acegi_cas_security_check"); + + CasProxyTicketValidator tv = new MockCasProxyTicketValidator(true, false); + tv.setCasValidate("https://company.com/cas/proxyvalidate"); + tv.setServiceProperties(sp); + tv.setProxyCallbackUrl("http://my.com/webapp/casProxy/someValidator"); + + TicketResponse response = tv.confirmTicketValid( + "ST-0-ER94xMJmn6pha35CQRoZ"); + + assertEquals("user", response.getUser()); + } + + public void testProxyTicketValidatorInternalExceptionsGracefullyHandled() { + CasProxyTicketValidator tv = new MockCasProxyTicketValidator(false, true); + tv.setCasValidate("https://company.com/cas/proxyvalidate"); + tv.setServiceProperties(new ServiceProperties()); + tv.setProxyCallbackUrl("http://my.com/webapp/casProxy/someValidator"); + + try { + tv.confirmTicketValid("ST-0-ER94xMJmn6pha35CQRoZ"); + fail("Should have thrown AuthenticationServiceException"); + } catch (AuthenticationServiceException expected) { + assertTrue(true); + } + } + + public void testValidationFailsOkAndOperationWithoutAProxyCallbackUrl() { + CasProxyTicketValidator tv = new MockCasProxyTicketValidator(false, + false); + tv.setCasValidate("https://company.com/cas/proxyvalidate"); + tv.setServiceProperties(new ServiceProperties()); + + try { + tv.confirmTicketValid("ST-0-ER94xMJmn6pha35CQRoZ"); + fail("Should have thrown BadCredentialsExpected"); + } catch (BadCredentialsException expected) { + assertTrue(true); + } + } + + //~ Inner Classes ========================================================== + + private class MockCasProxyTicketValidator extends CasProxyTicketValidator { + private boolean returnTicket; + private boolean throwAuthenticationServiceException; + + public MockCasProxyTicketValidator(boolean returnTicket, + boolean throwAuthenticationServiceException) { + this.returnTicket = returnTicket; + this.throwAuthenticationServiceException = throwAuthenticationServiceException; + } + + private MockCasProxyTicketValidator() { + super(); + } + + protected TicketResponse validateNow(ProxyTicketValidator pv) + throws AuthenticationServiceException, BadCredentialsException { + if (returnTicket) { + return new TicketResponse("user", new Vector(), + "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt"); + } + + if (throwAuthenticationServiceException) { + throw new AuthenticationServiceException("As requested by mock"); + } + + throw new BadCredentialsException("As requested by mock"); + } + } +} diff --git a/core/src/test/java/org/acegisecurity/ui/cas/CasProcessingFilterEntryPointTests.java b/core/src/test/java/org/acegisecurity/ui/cas/CasProcessingFilterEntryPointTests.java new file mode 100644 index 0000000000..f3444b3a6c --- /dev/null +++ b/core/src/test/java/org/acegisecurity/ui/cas/CasProcessingFilterEntryPointTests.java @@ -0,0 +1,126 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.ui.cas; + +import junit.framework.TestCase; + +import net.sf.acegisecurity.MockHttpServletRequest; +import net.sf.acegisecurity.MockHttpServletResponse; + + +/** + * Tests {@link CasProcessingFilterEntryPoint}. + * + * @author Ben Alex + * @version $Id$ + */ +public class CasProcessingFilterEntryPointTests extends TestCase { + //~ Constructors =========================================================== + + public CasProcessingFilterEntryPointTests() { + super(); + } + + public CasProcessingFilterEntryPointTests(String arg0) { + super(arg0); + } + + //~ Methods ================================================================ + + public final void setUp() throws Exception { + super.setUp(); + } + + public static void main(String[] args) { + junit.textui.TestRunner.run(CasProcessingFilterEntryPointTests.class); + } + + public void testDetectsMissingLoginFormUrl() throws Exception { + CasProcessingFilterEntryPoint ep = new CasProcessingFilterEntryPoint(); + ep.setServiceProperties(new ServiceProperties()); + + try { + ep.afterPropertiesSet(); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("loginUrl must be specified", expected.getMessage()); + } + } + + public void testDetectsMissingServiceProperties() throws Exception { + CasProcessingFilterEntryPoint ep = new CasProcessingFilterEntryPoint(); + ep.setLoginUrl("https://cas/login"); + + try { + ep.afterPropertiesSet(); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("serviceProperties must be specified", + expected.getMessage()); + } + } + + public void testGettersSetters() { + CasProcessingFilterEntryPoint ep = new CasProcessingFilterEntryPoint(); + ep.setLoginUrl("https://cas/login"); + assertEquals("https://cas/login", ep.getLoginUrl()); + + ep.setServiceProperties(new ServiceProperties()); + assertTrue(ep.getServiceProperties() != null); + } + + public void testNormalOperationWithRenewFalse() throws Exception { + ServiceProperties sp = new ServiceProperties(); + sp.setSendRenew(false); + sp.setService( + "https://mycompany.com/bigWebApp/j_acegi_cas_security_check"); + + CasProcessingFilterEntryPoint ep = new CasProcessingFilterEntryPoint(); + ep.setLoginUrl("https://cas/login"); + ep.setServiceProperties(sp); + + MockHttpServletRequest request = new MockHttpServletRequest( + "/some_path"); + + MockHttpServletResponse response = new MockHttpServletResponse(); + + ep.afterPropertiesSet(); + ep.commence(request, response); + assertEquals("https://cas/login?service=https://mycompany.com/bigWebApp/j_acegi_cas_security_check", + response.getRedirect()); + } + + public void testNormalOperationWithRenewTrue() throws Exception { + ServiceProperties sp = new ServiceProperties(); + sp.setSendRenew(true); + sp.setService( + "https://mycompany.com/bigWebApp/j_acegi_cas_security_check"); + + CasProcessingFilterEntryPoint ep = new CasProcessingFilterEntryPoint(); + ep.setLoginUrl("https://cas/login"); + ep.setServiceProperties(sp); + + MockHttpServletRequest request = new MockHttpServletRequest( + "/some_path"); + + MockHttpServletResponse response = new MockHttpServletResponse(); + + ep.afterPropertiesSet(); + ep.commence(request, response); + assertEquals("https://cas/login?renew=true&service=https://mycompany.com/bigWebApp/j_acegi_cas_security_check", + response.getRedirect()); + } +} diff --git a/core/src/test/java/org/acegisecurity/ui/cas/CasProcessingFilterTests.java b/core/src/test/java/org/acegisecurity/ui/cas/CasProcessingFilterTests.java new file mode 100644 index 0000000000..b965deaee4 --- /dev/null +++ b/core/src/test/java/org/acegisecurity/ui/cas/CasProcessingFilterTests.java @@ -0,0 +1,93 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.ui.cas; + +import junit.framework.TestCase; + +import net.sf.acegisecurity.Authentication; +import net.sf.acegisecurity.AuthenticationException; +import net.sf.acegisecurity.MockAuthenticationManager; +import net.sf.acegisecurity.MockHttpServletRequest; +import net.sf.acegisecurity.MockHttpSession; + + +/** + * Tests {@link CasProcessingFilter}. + * + * @author Ben Alex + * @version $Id$ + */ +public class CasProcessingFilterTests extends TestCase { + //~ Constructors =========================================================== + + public CasProcessingFilterTests() { + super(); + } + + public CasProcessingFilterTests(String arg0) { + super(arg0); + } + + //~ Methods ================================================================ + + public final void setUp() throws Exception { + super.setUp(); + } + + public static void main(String[] args) { + junit.textui.TestRunner.run(CasProcessingFilterTests.class); + } + + public void testGetters() { + CasProcessingFilter filter = new CasProcessingFilter(); + assertEquals("/j_acegi_cas_security_check", + filter.getDefaultFilterProcessesUrl()); + } + + public void testNormalOperation() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(null, + new MockHttpSession()); + request.setParameter("ticket", "ST-0-ER94xMJmn6pha35CQRoZ"); + + MockAuthenticationManager authMgr = new MockAuthenticationManager(true); + + CasProcessingFilter filter = new CasProcessingFilter(); + filter.setAuthenticationManager(authMgr); + filter.init(null); + + Authentication result = filter.attemptAuthentication(request); + assertTrue(result != null); + } + + public void testNullServiceTicketHandledGracefully() + throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(null, + new MockHttpSession()); + + MockAuthenticationManager authMgr = new MockAuthenticationManager(false); + + CasProcessingFilter filter = new CasProcessingFilter(); + filter.setAuthenticationManager(authMgr); + filter.init(null); + + try { + filter.attemptAuthentication(request); + fail("Should have thrown AuthenticationException"); + } catch (AuthenticationException expected) { + assertTrue(true); + } + } +} diff --git a/core/src/test/java/org/acegisecurity/ui/cas/ServicePropertiesTests.java b/core/src/test/java/org/acegisecurity/ui/cas/ServicePropertiesTests.java new file mode 100644 index 0000000000..e1ffb755bf --- /dev/null +++ b/core/src/test/java/org/acegisecurity/ui/cas/ServicePropertiesTests.java @@ -0,0 +1,71 @@ +/* Copyright 2004 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.ui.cas; + +import junit.framework.TestCase; + + +/** + * Tests {@link ServiceProperties}. + * + * @author Ben Alex + * @version $Id$ + */ +public class ServicePropertiesTests extends TestCase { + //~ Constructors =========================================================== + + public ServicePropertiesTests() { + super(); + } + + public ServicePropertiesTests(String arg0) { + super(arg0); + } + + //~ Methods ================================================================ + + public final void setUp() throws Exception { + super.setUp(); + } + + public static void main(String[] args) { + junit.textui.TestRunner.run(ServicePropertiesTests.class); + } + + public void testDetectsMissingLoginFormUrl() throws Exception { + ServiceProperties sp = new ServiceProperties(); + + try { + sp.afterPropertiesSet(); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertEquals("service must be specified", expected.getMessage()); + } + } + + public void testGettersSetters() throws Exception { + ServiceProperties sp = new ServiceProperties(); + sp.setSendRenew(false); + assertFalse(sp.isSendRenew()); + sp.setSendRenew(true); + assertTrue(sp.isSendRenew()); + + sp.setService("https://mycompany.com/service"); + assertEquals("https://mycompany.com/service", sp.getService()); + + sp.afterPropertiesSet(); + } +} diff --git a/docs/reference/src/index.xml b/docs/reference/src/index.xml index 851885eeb1..9d6b0b70ab 100644 --- a/docs/reference/src/index.xml +++ b/docs/reference/src/index.xml @@ -294,7 +294,7 @@ handling each request. Handling involves a number of operations: - + Store the configuration attributes that are associated with each secure request. @@ -354,7 +354,7 @@ Return any result received from the SecurityInterceptorCallback. - + Whilst this may seem quite involved, don't worry. Developers interact with the security process by simply implementing basic @@ -854,6 +854,13 @@ AuthenticationProvider if you were not using container adapters. + + + CasAuthenticationProvider is able to + authenticate Yale Central Authentication Service (CAS) tickets. + This is discussed further in the CAS Single Sign On + section. + @@ -870,8 +877,26 @@ <bean id="daoAuthenticationProvider" class="net.sf.acegisecurity.providers.dao.DaoAuthenticationProvider"> <property name="authenticationDao"><ref bean="inMemoryDaoImpl"/></property> + <property name="saltSource"><ref bean="saltSource"/></property> + <property name="passwordEncoder"><ref bean="passwordEncoder"/></property> </bean> + The PasswordEncoder and + SaltSource are optional. A + PasswordEncoder provides encoding and decoding of + passwords obtained from the authentication repository. A + SaltSource enables the passwords to be populated + with a "salt", which enhances the security of the passwords in the + authentication repository. PasswordEncoder + implementations are provided with the Acegi Security System for Spring + covering MD5, SHA and cleartext encodings. Two + SaltSource implementations are also provided: + SystemWideSaltSource which encodes all passwords + with the same salt, and ReflectionSaltSource, which + inspects a given property of the returned User object to obtain the + salt. Please refer to the JavaDocs for further details on these + optional features. + For a class to be able to provide the DaoAuthenticationProvider with access to an authentication repository, it must implement the @@ -957,10 +982,13 @@ You can use different relational database management systems by modifying the DriverManagerDataSource shown above. Irrespective of the database used, a standard schema must be used as - indicated in dbinit.txt. Of particular note is the - database must return responses that treat the username as case - insensitive, in order to comply with the - AuthenticationDao contract. + indicated in dbinit.txt. + + If you default schema is unsuitable for your needs, + JdbcDaoImpl provides two properties that allow + customisation of the SQL statements. You may also subclass the + JdbcDaoImpl if further customisation is necessary. + Please refer to the JavaDocs for details. The Acegi Security System for Spring ships with a Hypersonic SQL instance that has the required authentication information and sample @@ -1487,11 +1515,13 @@ public boolean supports(Class clazz); HttpSession object and filters to authenticate the user. Another approach is HTTP Basic Authentication, which allows clients to use HTTP headers to present authentication information to - the Acegi Security System for Spring. The final approach is via - Container Adapters, which allow supported web containers to perform - the authentication themselves. HTTP Session Authentication is - discussed below, whilst Container Adapters are discussed in a separate - section. + the Acegi Security System for Spring. Alternatively, you can also use + Yale Central Authentication Service (CAS) for enterprise-wide single + sign on. The final approach is via Container Adapters, which allow + supported web containers to perform the authentication themselves. + HTTP Session and Basic Authentication is discussed below, whilst CAS + and Container Adapters are discussed in separate sections of this + document. @@ -1538,7 +1568,7 @@ public boolean supports(Class clazz); authenticationFailureUrl. The AuthenticationException will be placed into the HttpSession attribute indicated by - AuthenticationProcessingFilter.ACEGI_SECURITY_LAST_EXCEPTION_KEY, + AbstractProcessingFilter.ACEGI_SECURITY_LAST_EXCEPTION_KEY, enabling a reason to be provided to the user on the error page. If authentication is successful, the resulting @@ -1552,7 +1582,7 @@ public boolean supports(Class clazz); browser will need to be redirected to the target URL. The target URL is usually indicated by the HttpSession attribute specified by - AuthenticationProcessingFilter.ACEGI_SECURITY_TARGET_URL_KEY. + AbstractProcessingFilter.ACEGI_SECURITY_TARGET_URL_KEY. This attribute is automatically set by the SecurityEnforcementFilter when an AuthenticationException occurs, so that after login @@ -2100,6 +2130,569 @@ $CATALINA_HOME/bin/startup.sh + + Yale Central Authentication Service (CAS) Single Sign On + + + Overview + + Yale University produces an enterprise-wide single sign on + system known as CAS. Unlike other initiatives, Yale's Central + Authentication Service is open source, widely used, simple to + understand, platform independent, and supports proxy capabilities. The + Acegi Security System for Spring fully supports CAS, and provides an + easy migration path from single-application deployments of Acegi + Security through to multiple-application deployments secured by an + enterprise-wide CAS server. + + You can learn more about CAS at + http://www.yale.edu/tp/auth/. You will need to + visit this URL to download the CAS Server files. Whilst the Acegi + Security System for Spring includes two CAS libraries in the + "-with-dependencies" ZIP file, you will still need the CAS Java Server + Pages and web.xml to customise and deploy your CAS + server. + + + + How CAS Works + + Whilst the CAS web site above contains two documents that detail + the architecture of CAS, we present the general overview again here + within the context of the Acegi Security System for Spring. The + following refers to CAS 2.0, being the version of CAS that Acegi + Security for Spring supports. + + 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. You will also need to specify in the web.xml a + PasswordHandler. The + PasswordHandler has a simple method that returns a + boolean as to whether a given username and password is valid. Your + PasswordHandler implementation will need to link + into some type of backend authentication repository, such as an LDAP + server or database. + + If you're running an existing CAS server, you will have already + established a PasswordHandler. If you have not, + might prefer to use the Acegi Security System for Spring + CasPasswordHandler class. This class delegates + through to the standard Acegi Security + AuthenticationManager, enabling you to use a + security configuration you might already have in place. You do not + need to use the CasPasswordHandler class on your + CAS server unless you do not wish. The Acegi Security System for + Spring will function as a CAS client successfully irrespective of the + PasswordHandler you've chosen for your CAS + server. + + Apart from the CAS server itself, the other key player is of + course the secure web applications deployed throughout your + enterprise. These web applications are known as "services". There are + two types of services: standard services and proxy services. A proxy + service is able to request resources from other services on behalf of + the user. This will be explained more fully later. + + Services can be developed in a large variety of languages, due + to CAS 2.0's very light XML-based protocol. The Yale CAS home page + contains a clients archive which demonstrates CAS clients in Java, + Active Server Pages, Perl, Python and others. Naturally, Java support + is very strong given the CAS server is written in Java. You do not + need to use one of CAS' clients to interact with the CAS server from + Acegi Security System for Spring secured applications. This is handled + transparently for you. + + The basic interaction between a web browser, CAS server and an + Acegi Security for System Spring secured service is as follows: + + + + The web user is browsing the service's public pages. CAS or + Acegi Security is not involved. + + + + The user eventually requests a page that is either secure or + one of the beans it uses is secure. Acegi Security's + SecurityEnforcementFilter will detect the + AuthenticationException. + + + + Because the user has no Authentication + object in + HttpSessionIntegrationFilter.ACEGI_SECURITY_AUTHENTICATION_KEY, + the SecurityEnforcementFilter will call the configured + AuthenticationEntryPoint. If using CAS, this + will be the CasProcessingFilterEntryPoint + class. + + + + The CasProcessingFilterEntry point will redirect the user's + browser to the CAS server. It will also indicate a + service parameter, which is the callback URL + for the Acegi Security service. For example, the URL the browser + is redirected to might be + https://my.company.com/cas/login?service=https://server3.company.com/webapp/j_acegi_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 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_acegi_cas_security_check?ticket=ST-0-ER94xMJmn6pha35CQRoZ. + + + + Back in the service web application, the + CasProcessingFilter is always listening for + requests to /j_acegi_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 + CasProcessingFilter.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 + UsernamePasswordAuthenticationTokens containing + the CAS-specific principal (such as + CasProcessingFilter.CAS_STATEFUL_IDENTIFIER) + and CasAuthenticationTokens (discussed + later). + + + + CasAuthenticationProvider will validate + the service ticket using a TicketValidator + implementation. Acegi Security includes one implementation, the + CasProxyTicketValidator. This implementation + uses a CAS-supplied ticket validator. The + CasProxyTicketValidator makes a HTTPS request + to the CAS server in order to validate the service ticket. The + CasProxyTicketValidator may also include a + proxy callback parameter, which is included in this example: + https://my.company.com/cas/proxyValidate?service=https://server3.company.com/webapp/j_acegi_cas_security_check&ticket=ST-0-ER94xMJmn6pha35CQRoZ&pgtUrl=https://server3.company.com/webapp/casProxy/receptor. + + + + Back of the CAS server, the proxy validation request will be + received. If the presented service ticket matches the service URL + requested initially, 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 pgtUrl, 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. The HTTPS connection will + be used to send a proxy granting ticket to the original web + application. For example, + https://server3.company.com/webapp/casProxy/receptor?pgtIou=PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt&pgtId=PGT-1-si9YkkHLrtACBo64rmsi3v2nf7cpCResXg5MpESZFArbaZiOKH. + We suggest you use CAS' ProxyTicketReceptor + servlet to receive these proxy-granting tickets, if they are + required. + + + + The CasProxyTicketValidator 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 the Acegi + Security System: 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 CasAuthoritiesPopulator to advise the + GrantedAuthority objects that apply to the user + contained in the TicketResponse. Acegi Security + includes a DaoCasAuthoritiesPopulator which + simply uses the AuthenticationDao + infrastructure to find the User and their + associated GrantedAuthoritys. Note that the + password and enabled/disabled status of Users + returned by the AuthenticationDao are ignored, + as the CAS server is responsible for authentication decisions. + DaoCasAuthoritiesPopulator is only concerned + with retrieving the GrantedAuthoritys. + + + + If there were no problems, + CasAuthenticationProvider constructs a + CasAuthenticationToken including the details + contained in the TicketResponse and the + GrantedAuthoritys. The + CasAuthenticationToken contains the hash of a + key, so that the CasAuthenticationProvider + knows it created it. + + + + Control then returns to + CasProcessingFilter, which places the created + CasAuthenticationToken into the + HttpSession attribute named + HttpSessionIntegrationFilter.ACEGI_SECURITY_AUTHENTICATION_KEY. + + + + The user's browser is redirected to the original page that + caused the AuthenticationException. + + + + As the Authentication object is now in + the well-known location, it is handled like any other + authentication approach. Usually the + AutoIntegrationFilter will be used to associate + the Authentication object with the + ContextHolder for the duration of each + request. + + + + It's good that you're still here! It might sound involved, but + you can relax as the Acegi Security System for Spring classes hide + much of the complexity. Let's now look at how this is + configured. + + + + CAS Server Installation (Optional) + + As mentioned above, the Acegi Security System for Spring + includes a PasswordHandler that bridges your + existing AuthenticationManager into CAS. You do not + need to use this PasswordHandler to use Acegi + Security on the client side (any CAS + PasswordHandler will do). + + To install, you will need to download and extract the CAS server + archive. We used version 2.0.12 Beta 3. There will be a + /web directory in the root of the deployment. Copy + an applicationContext.xml containing your + AuthenticationManager as well as the + CasPasswordHandler into the + /web/WEB-INF directory. A sample + applicationContext.xml is included below: + + <bean id="inMemoryDaoImpl" class="net.sf.acegisecurity.providers.dao.memory.InMemoryDaoImpl"> + <property name="userMap"> + <value> + marissa=koala,ROLES_IGNORED_BY_CAS + dianne=emu,ROLES_IGNORED_BY_CAS + scott=wombat,ROLES_IGNORED_BY_CAS + peter=opal,disabled,ROLES_IGNORED_BY_CAS + </value> + </property> +</bean> + +<bean id="daoAuthenticationProvider" class="net.sf.acegisecurity.providers.dao.DaoAuthenticationProvider"> + <property name="authenticationDao"><ref bean="inMemoryDaoImpl"/></property> +</bean> + +<bean id="authenticationManager" class="net.sf.acegisecurity.providers.ProviderManager"> + <property name="providers"> + <list> + <ref bean="daoAuthenticationProvider"/> + </list> + </property> +</bean> + +<bean id="casPasswordHandler" class="net.sf.acegisecurity.adapters.cas.CasPasswordHandler"> + <property name="authenticationManager"><ref bean="authenticationManager"/></property> +</bean> + + Note the granted authorities are ignored by CAS. It has no way + of communciating the granted authorities to calling applications. CAS + is only concerned with username and passwords. + + Next you will need to edit the existing + /web/WEB-INF/web.xml file. Add (or edit in the case + of the authHandler property) the following + lines: + + <context-param> + <param-name>edu.yale.its.tp.cas.authHandler</param-name> + <param-value>net.sf.acegisecurity.adapters.cas.CasPasswordHandlerProxy</param-value> +</context-param> + +<context-param> + <param-name>contextConfigLocation</param-name> + <param-value>/WEB-INF/applicationContext.xml</param-value> +</context-param> + +<listener> + <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> +</listener> + + Copy the spring.jar and + acegi-security.jar files into + /web/WEB-INF/lib. Now use the ant + dist task in the build.xml in the root of + the directory structure. This will create + /lib/cas.war, which is ready for deployment to your + servlet container. + + + + CAS Acegi Security System Client Installation + + The web application side of CAS is made easy due to the Acegi + Security System for Spring. It is assumed you already know the basics + of using the Acegi Security System for Spring, so these are not + covered again below. Only the CAS-specific beans are mentioned. + + You will need to add a ServiceProperties bean + to your application context. This represents your service: + + <bean id="serviceProperties" class="net.sf.acegisecurity.ui.cas.ServiceProperties"> + <property name="service"><value>https://localhost:8443/contacts-cas/j_acegi_cas_security_check</value></property> + <property name="sendRenew"><value>false</value></property> +</bean> + + The service must equal a URL that will be + monitored by the CasProcessingFilter. 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: + + <bean id="casProcessingFilter" class="net.sf.acegisecurity.ui.cas.CasProcessingFilter"> + <property name="authenticationManager"><ref bean="authenticationManager"/></property> + <property name="authenticationFailureUrl"><value>/casfailed.jsp</value></property> + <property name="defaultTargetUrl"><value>/</value></property> + <property name="filterProcessesUrl"><value>/j_acegi_cas_security_check</value></property> +</bean> + +<bean id="securityEnforcementFilter" class="net.sf.acegisecurity.intercept.web.SecurityEnforcementFilter"> + <property name="filterSecurityInterceptor"><ref bean="filterInvocationInterceptor"/></property> + <property name="authenticationEntryPoint"><ref bean="casProcessingFilterEntryPoint"/></property> +</bean> + +<bean id="casProcessingFilterEntryPoint" class="net.sf.acegisecurity.ui.cas.CasProcessingFilterEntryPoint"> + <property name="loginUrl"><value>https://localhost:8443/cas/login</value></property> + <property name="serviceProperties"><ref bean="serviceProperties"/></property> +</bean> + + You will also need to add the + CasProcessingFilter to web.xml: + + <filter> + <filter-name>Acegi CAS Processing Filter</filter-name> + <filter-class>net.sf.acegisecurity.util.FilterToBeanProxy</filter-class> + <init-param> + <param-name>targetClass</param-name> + <param-value>net.sf.acegisecurity.ui.cas.CasProcessingFilter</param-value> + </init-param> +</filter> + +<filter-mapping> + <filter-name>Acegi CAS Processing Filter</filter-name> + <url-pattern>/*</url-pattern> +</filter-mapping> + + The CasProcessingFilter has very similar + properties to the AuthenticationProcessingFilter + (used for form-based logins). Each property is + self-explanatory. + + For CAS to operate, the + SecurityEnforcementFilter must have its + authenticationEntryPoint property set to the + CasProcessingFilterEntryPoint bean. + + The CasProcessingFilterEntryPoint must refer + to the ServiceProperties bean (discussed above) and + provide the URL to the enterprise's CAS login server. This is where + the user's browser will be redirected. + + Next you need to add an AuthenticationManager + that uses CasAuthenticationProvider and its + collaborators: + + <bean id="authenticationManager" class="net.sf.acegisecurity.providers.ProviderManager"> + <property name="providers"> + <list> + <ref bean="casAuthenticationProvider"/> + </list> + </property> +</bean> + +<bean id="casAuthenticationProvider" class="net.sf.acegisecurity.providers.cas.CasAuthenticationProvider"> + <property name="casAuthoritiesPopulator"><ref bean="casAuthoritiesPopulator"/></property> + <property name="casProxyDecider"><ref bean="casProxyDecider"/></property> + <property name="ticketValidator"><ref bean="casProxyTicketValidator"/></property> + <property name="statelessTicketCache"><ref bean="statelessTicketCache"/></property> + <property name="key"><value>my_password_for_this_auth_provider_only</value></property> +</bean> + +<bean id="casProxyTicketValidator" class="net.sf.acegisecurity.providers.cas.ticketvalidator.CasProxyTicketValidator"> + <property name="casValidate"><value>https://localhost:8443/cas/proxyValidate</value></property> + <property name="proxyCallbackUrl"><value>https://localhost:8443/contacts-cas/casProxy/receptor</value></property> + <property name="serviceProperties"><ref bean="serviceProperties"/></property> + <!-- <property name="trustStore"><value>/some/path/to/your/lib/security/cacerts</value></property> --> +</bean> + +<bean id="statelessTicketCache" class="net.sf.acegisecurity.providers.cas.cache.EhCacheBasedTicketCache"> + <property name="minutesToIdle"><value>20</value></property> +</bean> + +<bean id="casAuthoritiesPopulator" class="net.sf.acegisecurity.providers.cas.populator.DaoCasAuthoritiesPopulator"> + <property name="authenticationDao"><ref bean="inMemoryDaoImpl"/></property> +</bean> + +<bean id="casProxyDecider" class="net.sf.acegisecurity.providers.cas.proxy.RejectProxyTickets"/> + + The beans are all reasonable self-explanatory if you refer back + to the "How CAS Works" section. Careful readers might notice one + surprise: the statelessTicketCache property of the + CasAuthenticationProvider. This is discussed in + detail in the "Advanced CAS Usage" section. + + Note the CasProxyTicketValidator has a + remarked out trustStore property. This property + might be helpful if you experience HTTPS certificate issues. Also note + the proxyCallbackUrl is set so the service can + receive a proxy-granting ticket. As mentioned above, this is optional + and unnecessary if you do not require proxy-granting tickets. If you + do use this feature, you will need to configure a suitable servlet to + receive the proxy-granting tickets. We suggest you use CAS' + ProxyTicketReceptor by adding the following to your + web application's web.xml: + + <servlet> + <servlet-name>casproxy</servlet-name> + <servlet-class>edu.yale.its.tp.cas.proxy.ProxyTicketReceptor</servlet-class> +</servlet> + +<servlet-mapping> + <servlet-name>casproxy</servlet-name> + <url-pattern>/casProxy/*</url-pattern> +</servlet-mapping> + + This completes the configuration of 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 the Acegi Security + System for Spring need to be concerned about the fact CAS handled + authentication. + + There is also a contacts-cas.war file in the + sample applications directory. This sample application uses the above + settings and can be deployed to see CAS in operation. + + + + Advanced CAS Usage + + [DRAFT - COMMENTS WELCOME] + + The CasAuthenticationProvider distinguishes + between stateful and stateless clients. A stateful client is + considered any that originates via the + CasProcessingFilter. A stateless client is any that + presents an authentication request via the + UsernamePasswordAuthenticationToken with a + principal equal to + CasProcessingFilter.CAS_STATELESS_IDENTIFIER. + + Stateless clients are likely to be via remoting protocols such + as Hessian and Burlap. The BasicProcessingFilter is + still used in this case, but the remoting protocol client is expected + to present a username equal to the static string above, and a password + equal to a CAS service ticket. Clients should acquire a CAS service + ticket directly from the CAS server. + + Because remoting protocols have no way of presenting themselves + within the context of a HttpSession, it isn't + possible to rely on the HttpSession's + HttpSessionIntegrationFilter.ACEGI_SECURITY_AUTHENTICATION_KEY + attribute to locate the CasAuthenticationToken. Furthermore, because + the CAS server invalidates a service ticket after it has been + validated by the TicketValidator, presenting the same service ticket + on subsequent requests will not work. It is similarly very difficult + to obtain a proxy-granting ticket for a remoting protocol client, as + they are often operational on client machines which do not have HTTPS + certificates that would be trusted by the CAS server. + + 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 requests with a + principal equal to + CasProcessingFilter.CAS_STATELESS_IDENTIFIER. What + happens is the CasAuthenticationProvider will store the resulting + CasAuthenticationToken in the StatelessTicketCache, keyed on the + service ticket. Accordingly, remoting protocol clients can present the + same service ticket and the CasAuthenticationProvider will not need to + contact the CAS server for validation. + + The other aspect of advanced CAS usage involves creating proxy + tickets from the proxy-granting ticket. As indicated above, we + recommend you use CAS' ProxyTicketReceptor to + receive these tickets. The ProxyTicketReceptor + provides a static method that enables you to obtain a proxy ticket by + presenting the proxy-granting IOU ticket. You can obtain the + proxy-granting IOU ticket by calling + CasAuthenticationToken.getProxyGrantingTicketIou(). + + It is hoped you find CAS integration easy and useful with the + Acegi Security System for Spring classes. Welcome to enterprise-wide + single sign on! + + + Contacts Sample Application diff --git a/lib/cas/LICENSE b/lib/cas/LICENSE new file mode 100644 index 0000000000..b96bdc210e --- /dev/null +++ b/lib/cas/LICENSE @@ -0,0 +1,29 @@ + Copyright (c) 2000-2003 Yale University. All rights reserved. + + THIS SOFTWARE IS PROVIDED "AS IS," AND ANY EXPRESS OR IMPLIED + WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ARE EXPRESSLY + DISCLAIMED. IN NO EVENT SHALL YALE UNIVERSITY OR ITS EMPLOYEES BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED, THE COSTS OF + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED IN ADVANCE OF THE POSSIBILITY OF SUCH + DAMAGE. + + Redistribution and use of this software in source or binary forms, + with or without modification, are permitted, provided that the + following conditions are met: + + 1. Any redistribution must include the above copyright notice and + disclaimer and this list of conditions in any related documentation + and, if feasible, in the redistributed software. + + 2. Any redistribution must include the acknowledgment, "This product + includes software developed by Yale University," in any related + documentation and, if feasible, in the redistributed software. + + 3. The names "Yale" and "Yale University" must not be used to endorse + or promote products derived from this software. diff --git a/lib/cas/version.txt b/lib/cas/version.txt new file mode 100644 index 0000000000..bd53c5c24d --- /dev/null +++ b/lib/cas/version.txt @@ -0,0 +1,2 @@ +CAS Server 2.0.12 beta 3 +CAS Client 2.0.10 diff --git a/lib/ehcache/LICENSE.txt b/lib/ehcache/LICENSE.txt new file mode 100644 index 0000000000..3306070dfc --- /dev/null +++ b/lib/ehcache/LICENSE.txt @@ -0,0 +1,58 @@ +/* + * $Id$ + * + * ==================================================================== + * + * The Apache Software License, Version 1.1 + * + * Copyright (c) 1999-2003 The Apache Software Foundation. All rights + * reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, if + * any, must include the following acknowlegement: + * "This product includes software developed by the + * Apache Software Foundation (http://www.apache.org/)." + * Alternately, this acknowlegement may appear in the software itself, + * if and wherever such third-party acknowlegements normally appear. + * + * 4. The names "The Jakarta Project", "Commons", and "Apache Software + * Foundation" must not be used to endorse or promote products derived + * from this software without prior written permission. For written + * permission, please contact apache@apache.org. + * + * 5. Products derived from this software may not be called "Apache" + * nor may "Apache" appear in their names without prior written + * permission of the Apache Group. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ diff --git a/lib/ehcache/version.txt b/lib/ehcache/version.txt new file mode 100644 index 0000000000..d0e277e3da --- /dev/null +++ b/lib/ehcache/version.txt @@ -0,0 +1 @@ +EHCACHE 0.7 diff --git a/samples/contacts/build.xml b/samples/contacts/build.xml index 456f892d63..70077351e4 100644 --- a/samples/contacts/build.xml +++ b/samples/contacts/build.xml @@ -156,6 +156,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/contacts/etc/cas/applicationContext.xml b/samples/contacts/etc/cas/applicationContext.xml new file mode 100644 index 0000000000..d3528c8445 --- /dev/null +++ b/samples/contacts/etc/cas/applicationContext.xml @@ -0,0 +1,229 @@ + + + + + + + + + + + + my_run_as_password + + + + + + my_run_as_password + + + + + + + + + + + + + + + marissa=PASSWORD_NOT_USED,ROLE_TELLER,ROLE_SUPERVISOR + dianne=PASSWORD_NOT_USED,ROLE_TELLER + scott=PASSWORD_NOT_USED,ROLE_TELLER + peter=PASSWORD_NOT_USED_AND_DISABLED_IGNORED,disabled,ROLE_TELLER + + + + + + + + + + + Contacts Realm + + + + + + + + my_password_for_this_auth_provider_only + + + + https://localhost:8443/cas/proxyValidate + https://localhost:8443/contacts-cas/casProxy/receptor + + + + + + 20 + + + + + + + + + + + https://localhost:8443/contacts-cas/j_acegi_cas_security_check + false + + + + + + + + + + + + + false + + + + + + + + + + + + + + + + + sample.contact.ContactManager.delete=ROLE_SUPERVISOR,RUN_AS_SERVER + sample.contact.ContactManager.getAllByOwner=CONTACT_OWNED_BY_CURRENT_USER,RUN_AS_SERVER + sample.contact.ContactManager.save=CONTACT_OWNED_BY_CURRENT_USER,RUN_AS_SERVER + sample.contact.ContactManager.getById=ROLE_TELLER,RUN_AS_SERVER + + + + + + + + + + + + sample.contact.ContactManager.delete=ROLE_RUN_AS_SERVER + sample.contact.ContactManager.getAllByOwner=ROLE_RUN_AS_SERVER + sample.contact.ContactManager.save=ROLE_RUN_AS_SERVER + sample.contact.ContactManager.getById=ROLE_RUN_AS_SERVER + + + + + + + + sample.contact.ContactManager + + + publicContactManagerSecurity + publicContactManagerTarget + + + + + + + + + + sample.contact.ContactManager + + + backendContactManagerSecurity + backendContactManagerTarget + + + + + + + + + + + /casfailed.jsp + / + /j_acegi_cas_security_check + + + + + + + + + https://localhost:8443/cas/login + + + + + false + + + + + + + + + + + + + + + CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON + \A/secure/super.*\Z=ROLE_WE_DONT_HAVE + \A/secure/.*\Z=ROLE_SUPERVISOR,ROLE_TELLER + + + + + + + diff --git a/samples/contacts/etc/cas/casfailed.jsp b/samples/contacts/etc/cas/casfailed.jsp new file mode 100644 index 0000000000..0d0b9d3246 --- /dev/null +++ b/samples/contacts/etc/cas/casfailed.jsp @@ -0,0 +1,20 @@ +<%@ taglib prefix='c' uri='http://java.sun.com/jstl/core' %> +<%@ page import="net.sf.acegisecurity.ui.AbstractProcessingFilter" %> +<%@ page import="net.sf.acegisecurity.AuthenticationException" %> +<%-- This page will be copied into WAR's root directory if using CAS --%> + + + + Login to CAS failed! + + + +

Login to CAS failed!

+ + + Your CAS credentials were rejected.

+ Reason: <%= ((AuthenticationException) session.getAttribute(AbstractProcessingFilter.ACEGI_SECURITY_LAST_EXCEPTION_KEY)).getMessage() %> +
+ + + diff --git a/samples/contacts/etc/cas/web.xml b/samples/contacts/etc/cas/web.xml new file mode 100644 index 0000000000..fe02e4c524 --- /dev/null +++ b/samples/contacts/etc/cas/web.xml @@ -0,0 +1,160 @@ + + + + + + + + Contacts Sample Application + + + Example of an application secured using Acegi Security System for Spring. + + + + + edu.yale.its.tp.cas.proxyUrl + http://localhost:8433/cas/proxy + + + + + contextConfigLocation + /WEB-INF/applicationContext.xml + + + + Acegi CAS Processing Filter + net.sf.acegisecurity.util.FilterToBeanProxy + + targetClass + net.sf.acegisecurity.ui.cas.CasProcessingFilter + + + + + Acegi HTTP BASIC Authorization Filter + net.sf.acegisecurity.util.FilterToBeanProxy + + targetClass + net.sf.acegisecurity.ui.basicauth.BasicProcessingFilter + + + + + Acegi Security System for Spring Auto Integration Filter + net.sf.acegisecurity.ui.AutoIntegrationFilter + + + + Acegi HTTP Request Security Filter + net.sf.acegisecurity.util.FilterToBeanProxy + + targetClass + net.sf.acegisecurity.intercept.web.SecurityEnforcementFilter + + + + + Acegi CAS Processing Filter + /* + + + + Acegi HTTP BASIC Authorization Filter + /* + + + + Acegi Security System for Spring Auto Integration Filter + /* + + + + Acegi HTTP Request Security Filter + /* + + + + + org.springframework.web.context.ContextLoaderListener + + + + + contacts + org.springframework.web.servlet.DispatcherServlet + 1 + + + + caucho + org.springframework.web.servlet.DispatcherServlet + 2 + + + + + casproxy + edu.yale.its.tp.cas.proxy.ProxyTicketReceptor + 3 + + + + + contacts + *.htm + + + + + caucho + /caucho/* + + + + casproxy + /casProxy/* + + + + index.jsp + + + + /spring + /WEB-INF/spring.tld + + + diff --git a/samples/contacts/etc/ssl/acegisecurity.txt b/samples/contacts/etc/ssl/acegisecurity.txt new file mode 100644 index 0000000000..42aa266e0b --- /dev/null +++ b/samples/contacts/etc/ssl/acegisecurity.txt @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIIC1DCCAj0CBECA2vQwDQYJKoZIhvcNAQEEBQAwgbAxEDAOBgNVBAYTB1Vua25vd24xEDAOBgNV +BAgTB1Vua25vd24xEDAOBgNVBAcTB1Vua25vd24xOTA3BgNVBAoTMFRFU1QgQ0VSVElGSUNBVEUg +T05MWS4gRE8gTk9UIFVTRSBJTiBQUk9EVUNUSU9OLjEpMCcGA1UECxMgQWNlZ2kgU2VjdXJpdHkg +U3lzdGVtIGZvciBTcHJpbmcxEjAQBgNVBAMTCWxvY2FsaG9zdDAeFw0wNDA0MTcwNzIxMjRaFw0z +MTA5MDIwNzIxMjRaMIGwMRAwDgYDVQQGEwdVbmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYD +VQQHEwdVbmtub3duMTkwNwYDVQQKEzBURVNUIENFUlRJRklDQVRFIE9OTFkuIERPIE5PVCBVU0Ug +SU4gUFJPRFVDVElPTi4xKTAnBgNVBAsTIEFjZWdpIFNlY3VyaXR5IFN5c3RlbSBmb3IgU3ByaW5n +MRIwEAYDVQQDEwlsb2NhbGhvc3QwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBANwJCiDthTNC +SJZ87CYkhWWDBciaFRvuDldzgEGwEUF5gNczd8Er66Pvh+Ir350hjE4LsDfi5iQNOuhbRR37LvW5 +7CrKG3W+vq7K3Zr9JEqhP/U2ocPLQQF4/NbBKStRacGGY1O3koTqp9W8gE0vSlC3/KhoOoPWHkGh +NZXOxuwLAgMBAAEwDQYJKoZIhvcNAQEEBQADgYEAdTlsziREdIR00/+tufCq/ACHSo2nJr1yRzIi +cVOXJBws0f+M3TUSIaODFv/54bZnWtjlWGa55uhc425+LrkOtqus7cMoNnyte/C6g/gcnArkKVhL +C68LGqARe9DK1ycquA4KmgiKyhi9a54kKA6BC4bmmEI98HpB6uxxvOB+ChE= +-----END CERTIFICATE----- diff --git a/samples/contacts/etc/ssl/howto.txt b/samples/contacts/etc/ssl/howto.txt new file mode 100644 index 0000000000..170f653351 --- /dev/null +++ b/samples/contacts/etc/ssl/howto.txt @@ -0,0 +1,82 @@ +$Id$ + +CAS requires HTTPS be used for all operations, with the certificate used +having been signed by a certificate in the cacerts files shipped with Java. + +If you're using a HTTPS certificate signed by a well known authority +(like Verisign), you can safely ignore the procedure below (although you +might find the troubleshooting section at the end helpful). + +The following demonstrates how to create a self-signed certificate and add +it to the cacerts file. If you just want to use the certificate we have +already created and shipped with the Acegi Security System for Spring, you +can skip directly to step 3. + + +1. keytool -keystore keystore -alias acegisecurity -genkey -keyalg RSA -validity 9999 -storepass password -keypass password + +What is your first and last name? + [Unknown]: localhost +What is the name of your organizational unit? + [Unknown]: Acegi Security System for Spring +What is the name of your organization? + [Unknown]: TEST CERTIFICATE ONLY. DO NOT USE IN PRODUCTION. +What is the name of your City or Locality? + [Unknown]: +What is the name of your State or Province? + [Unknown]: +What is the two-letter country code for this unit? + [Unknown]: +Is CN=localhost, OU=Acegi Security System for Spring, O=TEST CERTIFICATE ONLY. D +O NOT USE IN PRODUCTION., L=Unknown, ST=Unknown, C=Unknown correct? + [no]: yes + + +2. keytool -export -v -rfc -alias acegisecurity -file acegisecurity.txt -keystore keystore -storepass password + +3. copy acegisecurity.txt %JAVA_HOME%\lib\security + +4. copy keystore %YOUR_WEB_CONTAINER_LOCATION% + + NOTE: You will need to configure your web container as appropriate. + We recommend you test the certificate works by visiting + https://localhost:8443. When prompted by your browser, select to + install the certificate. + +5. cd %JAVA_HOME%\lib\security + +6. keytool -import -v -file acegisecurity.txt -keypass password -keystore cacerts -storepass changeit -alias acegisecurity + +Owner: CN=localhost, OU=Acegi Security System for Spring, O=TEST CERTIFICATE ONL +Y. DO NOT USE IN PRODUCTION., L=Unknown, ST=Unknown, C=Unknown +Issuer: CN=localhost, OU=Acegi Security System for Spring, O=TEST CERTIFICATE ON +LY. DO NOT USE IN PRODUCTION., L=Unknown, ST=Unknown, C=Unknown +Serial number: 4080daf4 +Valid from: Sat Apr 17 07:21:24 GMT 2004 until: Tue Sep 02 07:21:24 GMT 2031 +Certificate fingerprints: + MD5: B4:AC:A8:24:34:99:F1:A9:F8:1D:A5:6C:BF:0A:34:FA + SHA1: F1:E6:B1:3A:01:39:2D:CF:06:FA:82:AB:86:0D:77:9D:06:93:D6:B0 +Trust this certificate? [no]: yes +Certificate was added to keystore +[Saving cacerts] + + +7. Finished. You can now run the sample application as if you purchased a + properly signed certificate. For production applications, of course you should + use an appropriately signed certificate so your web visitors will trust it + (such as issued by Thawte, Verisign etc). + +TROUBLESHOOTING + +* A "sun.security.validator.ValidatorException: No trusted certificate + found" indicates the cacerts is not being used or it did not correctly + import the certificate. To rule out your web container replacing or in + some way modifying the trust manager, set the + CasAuthenticationProvider.trustStore property to the full file system + location to your cacerts file. + +* If your web container is ignoring your cacerts file, double-check it + is stored in $JAVA_HOME\lib\security\cacerts. $JAVA_HOME might be + pointing to the SDK, not JRE. In that case, copy + $JAVA_HOME\jre\lib\security\cacerts to $JAVA_HOME\lib\security\cacerts + diff --git a/samples/contacts/etc/ssl/keystore b/samples/contacts/etc/ssl/keystore new file mode 100644 index 0000000000..e01bb9c0be Binary files /dev/null and b/samples/contacts/etc/ssl/keystore differ diff --git a/samples/contacts/project.properties b/samples/contacts/project.properties index 9ed9aafd4a..7bc46fc0fd 100644 --- a/samples/contacts/project.properties +++ b/samples/contacts/project.properties @@ -3,6 +3,7 @@ name.filter=contacts name.ca=contacts-container-adapter +name.cas=contacts-cas src.dir=src war.dir=war lib.dir=${basedir}/../../lib