diff --git a/modules/cas/bin/deploy.bat b/modules/cas/bin/deploy.bat new file mode 100644 index 00000000..8bdcc4b0 --- /dev/null +++ b/modules/cas/bin/deploy.bat @@ -0,0 +1,19 @@ +@echo off +rem /** +rem * Copyright (c) 2013-Now http://jeesite.com All rights reserved. +rem * No deletion without permission, or be held responsible to law. +rem * +rem * Author: ThinkGem@163.com +rem */ +echo. +echo [信息] 部署工程版本到Nexus服务器。 +echo. + +%~d0 +cd %~dp0 + +cd .. +call mvn clean deploy -Dmaven.test.skip=true -Pdeploy + +cd bin +pause \ No newline at end of file diff --git a/modules/cas/bin/deploy.sh b/modules/cas/bin/deploy.sh new file mode 100644 index 00000000..c849d626 --- /dev/null +++ b/modules/cas/bin/deploy.sh @@ -0,0 +1,16 @@ +#!/bin/sh +# /** +# * Copyright (c) 2013-Now http://jeesite.com All rights reserved. +# * No deletion without permission, or be held responsible to law. +# * +# * Author: ThinkGem@163.com +# * +# */ +echo "" +echo "[淇℃伅] 閮ㄧ讲宸ョ▼鐗堟湰鍒癗exus鏈嶅姟鍣ㄣ" +echo "" + +cd .. +mvn clean deploy -Dmaven.test.skip=true -Pdeploy + +cd bin \ No newline at end of file diff --git a/modules/cas/bin/package.bat b/modules/cas/bin/package.bat new file mode 100644 index 00000000..d88a16b3 --- /dev/null +++ b/modules/cas/bin/package.bat @@ -0,0 +1,19 @@ +@echo off +rem /** +rem * Copyright (c) 2013-Now http://jeesite.com All rights reserved. +rem * No deletion without permission, or be held responsible to law. +rem * +rem * Author: ThinkGem@163.com +rem */ +echo. +echo [信息] 打包安装工程,生成jar包文件。 +echo. + +%~d0 +cd %~dp0 + +cd .. +call mvn clean install -Dmaven.test.skip=true -Ppackage + +cd bin +pause \ No newline at end of file diff --git a/modules/cas/bin/package.sh b/modules/cas/bin/package.sh new file mode 100644 index 00000000..b1a81373 --- /dev/null +++ b/modules/cas/bin/package.sh @@ -0,0 +1,16 @@ +#!/bin/sh +# /** +# * Copyright (c) 2013-Now http://jeesite.com All rights reserved. +# * No deletion without permission, or be held responsible to law. +# * +# * Author: ThinkGem@163.com +# * +# */ +echo "" +echo "[淇℃伅] 鎵撳寘Web宸ョ▼锛岀敓鎴恮ar/jar鍖呮枃浠躲" +echo "" + +cd .. +mvn clean install -Dmaven.test.skip=true -Ppackage + +cd bin \ No newline at end of file diff --git a/modules/cas/pom.xml b/modules/cas/pom.xml new file mode 100644 index 00000000..c0b57d18 --- /dev/null +++ b/modules/cas/pom.xml @@ -0,0 +1,52 @@ + + + 4.0.0 + + + com.jeesite + jeesite-parent + 5.4.boot3-SNAPSHOT + ../../parent/pom.xml + + + jeesite-module-cas + jar + + JeeSite Module CAS + http://jeesite.com + 2013-Now + + + + + com.jeesite + jeesite-framework + ${project.parent.version} + + + + + org.opensaml + opensaml + 1.1 + + + + + + + thinkgem + WangZhen + thinkgem at 163.com + Project lead + +8 + + + + + JeeSite + http://jeesite.com + + + \ No newline at end of file diff --git a/modules/cas/src/main/java/com/jeesite/common/shiro/cas/CasBaseFilter.java b/modules/cas/src/main/java/com/jeesite/common/shiro/cas/CasBaseFilter.java new file mode 100644 index 00000000..f890ec7c --- /dev/null +++ b/modules/cas/src/main/java/com/jeesite/common/shiro/cas/CasBaseFilter.java @@ -0,0 +1,155 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * 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 com.jeesite.common.shiro.cas; + +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.web.filter.authc.AuthenticatingFilter; +import org.apache.shiro.web.util.WebUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import java.io.IOException; + +/** + * This filter validates the CAS service ticket to authenticate the user. It must be configured on the URL recognized + * by the CAS server. For example, in {@code shiro.ini}: + *
+ * [main]
+ * casFilter = org.apache.shiro.cas.CasFilter
+ * ...
+ *
+ * [urls]
+ * /shiro-cas = casFilter
+ * ...
+ * 
+ * (example : http://host:port/mycontextpath/shiro-cas) + * + * @since 1.2 + * @see buji-pac4j + * @ deprecated replaced with Shiro integration in buji-pac4j. + */ +public class CasBaseFilter extends AuthenticatingFilter { + + private static Logger logger = LoggerFactory.getLogger(CasBaseFilter.class); + + // the name of the parameter service ticket in url + private static final String TICKET_PARAMETER = "ticket"; + + // the url where the application is redirected if the CAS service ticket validation failed (example : /mycontextpatch/cas_error.jsp) + private String failureUrl; + + /** + * The token created for this authentication is a CasToken containing the CAS service ticket received on the CAS service url (on which + * the filter must be configured). + * + * @param request the incoming request + * @param response the outgoing response + * @throws Exception if there is an error processing the request. + */ + @Override + protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception { + HttpServletRequest httpRequest = (HttpServletRequest) request; + String ticket = httpRequest.getParameter(TICKET_PARAMETER); + return new CasToken(ticket); + } + + /** + * Execute login by creating {@link #createToken(javax.servlet.ServletRequest, javax.servlet.ServletResponse) token} and logging subject + * with this token. + * + * @param request the incoming request + * @param response the outgoing response + * @throws Exception if there is an error processing the request. + */ + @Override + protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { + return executeLogin(request, response); + } + + /** + * Returns false to always force authentication (user is never considered authenticated by this filter). + * + * @param request the incoming request + * @param response the outgoing response + * @param mappedValue the filter-specific config value mapped to this filter in the URL rules mappings. + * @return false + */ + @Override + protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { + return false; + } + + /** + * If login has been successful, redirect user to the original protected url. + * + * @param token the token representing the current authentication + * @param subject the current authenticated subjet + * @param request the incoming request + * @param response the outgoing response + * @throws Exception if there is an error processing the request. + */ + @Override + protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, + ServletResponse response) throws Exception { + issueSuccessRedirect(request, response); + return false; + } + + /** + * If login has failed, redirect user to the CAS error page (no ticket or ticket validation failed) except if the user is already + * authenticated, in which case redirect to the default success url. + * + * @param token the token representing the current authentication + * @param ae the current authentication exception + * @param request the incoming request + * @param response the outgoing response + */ + @Override + protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException ae, ServletRequest request, + ServletResponse response) { + if (logger.isDebugEnabled()) { + logger.debug( "Authentication exception", ae ); + } + // is user authenticated or in remember me mode ? + Subject subject = getSubject(request, response); + if (subject.isAuthenticated() || subject.isRemembered()) { + try { + issueSuccessRedirect(request, response); + } catch (Exception e) { + logger.error("Cannot redirect to the default success url", e); + } + } else { + try { + WebUtils.issueRedirect(request, response, failureUrl); + } catch (IOException e) { + logger.error("Cannot redirect to failure url : {}", failureUrl, e); + } + } + return false; + } + + public void setFailureUrl(String failureUrl) { + this.failureUrl = failureUrl; + } +} diff --git a/modules/cas/src/main/java/com/jeesite/common/shiro/cas/CasCreateUser.java b/modules/cas/src/main/java/com/jeesite/common/shiro/cas/CasCreateUser.java new file mode 100644 index 00000000..01707371 --- /dev/null +++ b/modules/cas/src/main/java/com/jeesite/common/shiro/cas/CasCreateUser.java @@ -0,0 +1,18 @@ +package com.jeesite.common.shiro.cas; + +import com.jeesite.modules.sys.entity.User; + +import java.util.Map; + +/** + * Cas鐧诲綍鏃讹紝鏈」鐩病鏈夎处鍙锋椂璋冪敤鏂规硶 + * @author ThinkGem + */ +public interface CasCreateUser { + + /** + * Cas鐧诲綍鏃讹紝鏈」鐩病鏈夎处鍙锋椂璋冪敤鏂规硶 + */ + void createUser(User user, Map attributes); + +} diff --git a/modules/cas/src/main/java/com/jeesite/common/shiro/cas/CasOutHandler.java b/modules/cas/src/main/java/com/jeesite/common/shiro/cas/CasOutHandler.java new file mode 100644 index 00000000..3c50d42c --- /dev/null +++ b/modules/cas/src/main/java/com/jeesite/common/shiro/cas/CasOutHandler.java @@ -0,0 +1,205 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * 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 com.jeesite.common.shiro.cas; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.shiro.subject.PrincipalCollection; +import org.apache.shiro.subject.support.DefaultSubjectContext; +import org.jasig.cas.client.session.HashMapBackedSessionMappingStorage; +import org.jasig.cas.client.session.SessionMappingStorage; +import org.jasig.cas.client.util.CommonUtils; +import org.jasig.cas.client.util.XmlUtils; + +import com.jeesite.common.shiro.realm.LoginInfo; + +/** + * Performs CAS single sign-out operations in an API-agnostic fashion. + * + * @author Marvin S. Addison 2015-12-6 ThinkGem 澧炲姞鎵嬪姩鎸囧畾ticket鍜宭ogoutRequest鍙傛暟 + * @version $Revision: 24094 $ $Date: 2011-06-20 21:39:49 -0400 (Mon, 20 Jun 2011) $ + * @since 3.1.12 + * + */ +public final class CasOutHandler { + + /** Logger instance */ + private final Log log = LogFactory.getLog(getClass()); + + /** Mapping of token IDs and session IDs to HTTP sessions */ + private SessionMappingStorage sessionMappingStorage = new HashMapBackedSessionMappingStorage(); + + /** The name of the artifact parameter. This is used to capture the session identifier. */ + private String artifactParameterName = "ticket"; + + /** Parameter name that stores logout request */ + private String logoutParameterName = "logoutRequest"; + + + public void setSessionMappingStorage(final SessionMappingStorage storage) { + this.sessionMappingStorage = storage; + } + + public SessionMappingStorage getSessionMappingStorage() { + return this.sessionMappingStorage; + } + + /** + * @param name Name of the authentication token parameter. + */ + public void setArtifactParameterName(final String name) { + this.artifactParameterName = name; + } + + /** + * @param name Name of parameter containing CAS logout request message. + */ + public void setLogoutParameterName(final String name) { + this.logoutParameterName = name; + } + + /** + * Initializes the component for use. + */ + public void init() { + CommonUtils.assertNotNull(this.artifactParameterName, "artifactParameterName cannot be null."); + CommonUtils.assertNotNull(this.logoutParameterName, "logoutParameterName cannot be null."); + CommonUtils.assertNotNull(this.sessionMappingStorage, "sessionMappingStorage cannote be null."); + } + + /** + * Determines whether the given request contains an authentication token. + * + * @param request HTTP reqest. + * + * @return True if request contains authentication token, false otherwise. + */ + public boolean isTokenRequest(final HttpServletRequest request) { + return CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.artifactParameterName)); + } + + /** + * Determines whether the given request is a CAS logout request. + * + * @param request HTTP request. + * + * @return True if request is logout request, false otherwise. + */ + public boolean isLogoutRequest(final HttpServletRequest request) { + return "POST".equals(request.getMethod()) && !isMultipartRequest(request) && + CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.logoutParameterName)); + } + + /** + * Associates a token request with the current HTTP session by recording the mapping + * in the the configured {@link SessionMappingStorage} container. + * + * @param request HTTP request containing an authentication token. + */ + public void recordSession(final HttpServletRequest request) { + recordSession(request, null); + } + + /** + * Associates a token request with the current HTTP session by recording the mapping + * in the the configured {@link SessionMappingStorage} container. + * + * @param request HTTP request containing an authentication token. + */ + public void recordSession(final HttpServletRequest request, String ticket) { + +// final HttpSession session = request.getSession(true); + final HttpSession session = request.getSession(); + + final String token; + if (ticket != null){ + token = ticket; + }else{ + token = CommonUtils.safeGetParameter(request, this.artifactParameterName); + } + + if (log.isDebugEnabled()) { + log.debug("Recording session for token " + token); + } + + try { + this.sessionMappingStorage.removeBySessionById(session.getId()); + } catch (final Exception e) { + // ignore if the session is already marked as invalid. Nothing we can do! + } + sessionMappingStorage.addSessionById(token, session); + } + + /** + * Destroys the current HTTP session for the given CAS logout request. + * + * @param request HTTP request containing a CAS logout message. + */ + public LoginInfo destroySession(final HttpServletRequest request) { + final String logoutMessage = CommonUtils.safeGetParameter(request, this.logoutParameterName); + if (log.isTraceEnabled()) { + log.trace("Logout request:\n" + logoutMessage); + } + LoginInfo loginInfo = null; + final String token = XmlUtils.getTextForElement(logoutMessage, "SessionIndex"); + if (CommonUtils.isNotBlank(token)) { + final HttpSession session = this.sessionMappingStorage.removeSessionByMappingId(token); + + if (session != null) { + String sessionID = session.getId(); + if (log.isDebugEnabled()) { + log.debug("Invalidating session [" + sessionID + "] for token [" + token + "]"); + } + try { + PrincipalCollection pc = (PrincipalCollection) session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY); + loginInfo = (pc != null ? (LoginInfo) pc.getPrimaryPrincipal() : null); + session.invalidate(); + } catch (final IllegalStateException e) { + log.debug("Error invalidating session.", e); + } + } + } + return loginInfo; + } + +// /** +// * Destroys the current HTTP session for the given CAS logout request. +// * +// * @param request HTTP request containing a CAS logout message. +// * v4.0.5涔嬪悗锛屾浛浠f柟娉曪細onLogoutSuccess(loginInfo, request) +// */ +// @Deprecated +// public User destroySession(final HttpServletRequest request, String logoutRequest) { +// this.logoutParameterName = logoutRequest; +// LoginInfo loginInfo = destroySession(request); +// if (loginInfo != null) { +// return UserUtils.get(loginInfo.getId()); +// } +// return null; +// } + + private boolean isMultipartRequest(final HttpServletRequest request) { + return request.getContentType() != null && request.getContentType().toLowerCase().startsWith("multipart"); + } + +} diff --git a/modules/cas/src/main/java/com/jeesite/common/shiro/cas/CasOutSessionListener.java b/modules/cas/src/main/java/com/jeesite/common/shiro/cas/CasOutSessionListener.java new file mode 100644 index 00000000..39285415 --- /dev/null +++ b/modules/cas/src/main/java/com/jeesite/common/shiro/cas/CasOutSessionListener.java @@ -0,0 +1,62 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * 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 com.jeesite.common.shiro.cas; + +import jakarta.servlet.http.HttpSession; +import jakarta.servlet.http.HttpSessionEvent; +import jakarta.servlet.http.HttpSessionListener; + +import org.jasig.cas.client.session.SessionMappingStorage; + +import com.jeesite.common.utils.SpringUtils; + +/** + * Listener to detect when an HTTP session is destroyed and remove it from the map of + * managed sessions. Also allows for the programmatic removal of sessions. + *

+ * Enables the CAS Single Sign out feature. + * + * Scott Battaglia + * @version $Revision$ Date$ 2015-12-6 + * @since 3.1 + */ +public final class CasOutSessionListener implements HttpSessionListener { + + private CasOutHandler casOutHandler; + + @Override + public void sessionCreated(final HttpSessionEvent event) { + // nothing to do at the moment + } + + @Override + public void sessionDestroyed(final HttpSessionEvent event) { + final HttpSession session = event.getSession(); + getSessionMappingStorage().removeBySessionById(session.getId()); + } + + public SessionMappingStorage getSessionMappingStorage() { + if (casOutHandler == null){ + casOutHandler = SpringUtils.getBean(CasOutHandler.class); + } + return casOutHandler.getSessionMappingStorage(); + } + +} diff --git a/modules/cas/src/main/java/com/jeesite/common/shiro/cas/CasSubjectFactory.java b/modules/cas/src/main/java/com/jeesite/common/shiro/cas/CasSubjectFactory.java new file mode 100644 index 00000000..d212c82f --- /dev/null +++ b/modules/cas/src/main/java/com/jeesite/common/shiro/cas/CasSubjectFactory.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * 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 com.jeesite.common.shiro.cas; + +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.subject.SubjectContext; +import org.apache.shiro.web.mgt.DefaultWebSubjectFactory; + +/** + * {@link org.apache.shiro.mgt.SubjectFactory Subject} implementation to be used in CAS-enabled applications. + * + * @since 1.2 + * @see buji-pac4j + * @ deprecated replaced with Shiro integration in buji-pac4j. + */ +public class CasSubjectFactory extends DefaultWebSubjectFactory { + + @Override + public Subject createSubject(SubjectContext context) { + + //the authenticated flag is only set by the SecurityManager after a successful authentication attempt. + boolean authenticated = context.isAuthenticated(); + + //although the SecurityManager 'sees' the submission as a successful authentication, in reality, the + //login might have been just a CAS rememberMe login. If so, set the authenticated flag appropriately: + if (authenticated) { + + AuthenticationToken token = context.getAuthenticationToken(); + + if (token != null && token instanceof CasToken) { + CasToken casToken = (CasToken) token; + // set the authenticated flag of the context to true only if the CAS subject is not in a remember me mode + if (casToken.isRememberMe()) { + context.setAuthenticated(false); + } + } + } + + return super.createSubject(context); + } +} diff --git a/modules/cas/src/main/java/com/jeesite/common/shiro/cas/CasToken.java b/modules/cas/src/main/java/com/jeesite/common/shiro/cas/CasToken.java new file mode 100644 index 00000000..41d2a1a1 --- /dev/null +++ b/modules/cas/src/main/java/com/jeesite/common/shiro/cas/CasToken.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * 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 com.jeesite.common.shiro.cas; + +import org.apache.shiro.authc.RememberMeAuthenticationToken; + +/** + * This class represents a token for a CAS authentication (service ticket + user id + remember me). + * + * @since 1.2 + * @see buji-pac4j + * @ deprecated replaced with Shiro integration in buji-pac4j. + */ +public class CasToken implements RememberMeAuthenticationToken { + + private static final long serialVersionUID = 8587329689973009598L; + + // the service ticket returned by the CAS server + private String ticket = null; + + // the user identifier + private String userId = null; + + // is the user in a remember me mode ? + private boolean isRememberMe = false; + + public CasToken(String ticket) { + this.ticket = ticket; + } + + public Object getPrincipal() { + return userId; + } + + public Object getCredentials() { + return ticket; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public boolean isRememberMe() { + return isRememberMe; + } + + public void setRememberMe(boolean isRememberMe) { + this.isRememberMe = isRememberMe; + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/authentication/AttributePrincipal.java b/modules/cas/src/main/java/org/jasig/cas/client/authentication/AttributePrincipal.java new file mode 100644 index 00000000..d17c97fd --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/authentication/AttributePrincipal.java @@ -0,0 +1,53 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.authentication; + +import java.io.Serializable; +import java.security.Principal; +import java.util.Map; + +/** + * Extension to the standard Java Principal that includes a way to retrieve proxy tickets for a particular user + * and attributes. + *

+ * Developer's who don't want their code tied to CAS merely need to work with the Java Principal then. Working with + * the CAS-specific features requires knowledge of the AttributePrincipal class. + * + * @author Scott Battaglia + * @version $Revision$ $Date$ + * @since 3.1 + */ +public interface AttributePrincipal extends Principal, Serializable { + + /** + * Retrieves a CAS proxy ticket for this specific principal. + * + * @param service the service we wish to proxy this user to. + * @return a String representing the proxy ticket. + */ + String getProxyTicketFor(String service); + + /** + * The Map of key/value pairs associated with this principal. + * @return the map of key/value pairs associated with this principal. + */ + Map getAttributes(); + +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/authentication/AttributePrincipalImpl.java b/modules/cas/src/main/java/org/jasig/cas/client/authentication/AttributePrincipalImpl.java new file mode 100644 index 00000000..e93978b3 --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/authentication/AttributePrincipalImpl.java @@ -0,0 +1,112 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.authentication; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.jasig.cas.client.proxy.ProxyRetriever; +import org.jasig.cas.client.util.CommonUtils; + +import java.util.Collections; +import java.util.Map; + +/** + * Concrete implementation of the AttributePrincipal interface. + * + * @author Scott Battaglia + * @version $Revision$ $Date$ + * @since 3.1 + */ +public class AttributePrincipalImpl extends SimplePrincipal implements AttributePrincipal { + + private static final Log LOG = LogFactory.getLog(AttributePrincipalImpl.class); + + /** Unique Id for Serialization */ + private static final long serialVersionUID = -1443182634624927187L; + + /** Map of key/value pairs about this principal. */ + private final Map attributes; + + /** The CAS 2 ticket used to retrieve a proxy ticket. */ + private final String proxyGrantingTicket; + + /** The method to retrieve a proxy ticket from a CAS server. */ + private final ProxyRetriever proxyRetriever; + + /** + * Constructs a new principal with an empty map of attributes. + * + * @param name the unique identifier for the principal. + */ + public AttributePrincipalImpl(final String name) { + this(name, Collections.emptyMap()); + } + + /** + * Constructs a new principal with the supplied name and attributes. + * + * @param name the unique identifier for the principal. + * @param attributes the key/value pairs for this principal. + */ + public AttributePrincipalImpl(final String name, final Map attributes) { + this(name, attributes, null, null); + } + + /** + * Constructs a new principal with the supplied name and the proxying capabilities. + * + * @param name the unique identifier for the principal. + * @param proxyGrantingTicket the ticket associated with this principal. + * @param proxyRetriever the ProxyRetriever implementation to call back to the CAS server. + */ + public AttributePrincipalImpl(final String name, final String proxyGrantingTicket, final ProxyRetriever proxyRetriever) { + this(name, Collections.emptyMap(), proxyGrantingTicket, proxyRetriever); + } + + /** + * Constructs a new principal witht he supplied name, attributes, and proxying capabilities. + * + * @param name the unique identifier for the principal. + * @param attributes the key/value pairs for this principal. + * @param proxyGrantingTicket the ticket associated with this principal. + * @param proxyRetriever the ProxyRetriever implementation to call back to the CAS server. + */ + public AttributePrincipalImpl(final String name, final Map attributes, final String proxyGrantingTicket, final ProxyRetriever proxyRetriever) { + super(name); + this.attributes = attributes; + this.proxyGrantingTicket = proxyGrantingTicket; + this.proxyRetriever = proxyRetriever; + + CommonUtils.assertNotNull(this.attributes, "attributes cannot be null."); + } + + public Map getAttributes() { + return this.attributes; + } + + public String getProxyTicketFor(String service) { + if (proxyGrantingTicket != null) { + return this.proxyRetriever.getProxyTicketIdFor(this.proxyGrantingTicket, service); + } + + LOG.debug("No ProxyGrantingTicket was supplied, so no Proxy Ticket can be retrieved."); + return null; + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/authentication/AuthenticationFilter.java b/modules/cas/src/main/java/org/jasig/cas/client/authentication/AuthenticationFilter.java new file mode 100644 index 00000000..7f3c9292 --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/authentication/AuthenticationFilter.java @@ -0,0 +1,158 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.authentication; + +import org.jasig.cas.client.util.AbstractCasFilter; +import org.jasig.cas.client.util.CommonUtils; +import org.jasig.cas.client.validation.Assertion; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import java.io.IOException; + +/** + * Filter implementation to intercept all requests and attempt to authenticate + * the user by redirecting them to CAS (unless the user has a ticket). + *

+ * This filter allows you to specify the following parameters (at either the context-level or the filter-level): + *

+ * + *

Please see AbstractCasFilter for additional properties.

+ * + * @author Scott Battaglia + * @version $Revision: 11768 $ $Date: 2007-02-07 15:44:16 -0500 (Wed, 07 Feb 2007) $ + * @since 3.0 + */ +public class AuthenticationFilter extends AbstractCasFilter { + + /** + * The URL to the CAS Server login. + */ + private String casServerLoginUrl; + + /** + * Whether to send the renew request or not. + */ + private boolean renew = false; + + /** + * Whether to send the gateway request or not. + */ + private boolean gateway = false; + + private GatewayResolver gatewayStorage = new DefaultGatewayResolverImpl(); + + protected void initInternal(final FilterConfig filterConfig) throws ServletException { + if (!isIgnoreInitConfiguration()) { + super.initInternal(filterConfig); + setCasServerLoginUrl(getPropertyFromInitParams(filterConfig, "casServerLoginUrl", null)); + log.trace("Loaded CasServerLoginUrl parameter: " + this.casServerLoginUrl); + setRenew(parseBoolean(getPropertyFromInitParams(filterConfig, "renew", "false"))); + log.trace("Loaded renew parameter: " + this.renew); + setGateway(parseBoolean(getPropertyFromInitParams(filterConfig, "gateway", "false"))); + log.trace("Loaded gateway parameter: " + this.gateway); + + final String gatewayStorageClass = getPropertyFromInitParams(filterConfig, "gatewayStorageClass", null); + + if (gatewayStorageClass != null) { + try { + this.gatewayStorage = (GatewayResolver) Class.forName(gatewayStorageClass).newInstance(); + } catch (final Exception e) { + log.error(e,e); + throw new ServletException(e); + } + } + } + } + + public void init() { + super.init(); + CommonUtils.assertNotNull(this.casServerLoginUrl, "casServerLoginUrl cannot be null."); + } + + public final void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException { + final HttpServletRequest request = (HttpServletRequest) servletRequest; + final HttpServletResponse response = (HttpServletResponse) servletResponse; + final HttpSession session = request.getSession(false); + final Assertion assertion = session != null ? (Assertion) session.getAttribute(CONST_CAS_ASSERTION) : null; + + if (assertion != null) { + filterChain.doFilter(request, response); + return; + } + + final String serviceUrl = constructServiceUrl(request, response); + final String ticket = CommonUtils.safeGetParameter(request,getArtifactParameterName()); + final boolean wasGatewayed = this.gatewayStorage.hasGatewayedAlready(request, serviceUrl); + + if (CommonUtils.isNotBlank(ticket) || wasGatewayed) { + filterChain.doFilter(request, response); + return; + } + + final String modifiedServiceUrl; + + log.debug("no ticket and no assertion found"); + if (this.gateway) { + log.debug("setting gateway attribute in session"); + modifiedServiceUrl = this.gatewayStorage.storeGatewayInformation(request, serviceUrl); + } else { + modifiedServiceUrl = serviceUrl; + } + + if (log.isDebugEnabled()) { + log.debug("Constructed service url: " + modifiedServiceUrl); + } + + final String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl, getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway); + + if (log.isDebugEnabled()) { + log.debug("redirecting to \"" + urlToRedirectTo + "\""); + } + + response.sendRedirect(urlToRedirectTo); + } + + public final void setRenew(final boolean renew) { + this.renew = renew; + } + + public final void setGateway(final boolean gateway) { + this.gateway = gateway; + } + + public final void setCasServerLoginUrl(final String casServerLoginUrl) { + this.casServerLoginUrl = casServerLoginUrl; + } + + public final void setGatewayStorage(final GatewayResolver gatewayStorage) { + this.gatewayStorage = gatewayStorage; + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/authentication/DefaultGatewayResolverImpl.java b/modules/cas/src/main/java/org/jasig/cas/client/authentication/DefaultGatewayResolverImpl.java new file mode 100644 index 00000000..45b0c28b --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/authentication/DefaultGatewayResolverImpl.java @@ -0,0 +1,47 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.authentication; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; + +public final class DefaultGatewayResolverImpl implements GatewayResolver { + + public static final String CONST_CAS_GATEWAY = "_const_cas_gateway_"; + + public boolean hasGatewayedAlready(final HttpServletRequest request, + final String serviceUrl) { + final HttpSession session = request.getSession(false); + + if (session == null) { + return false; + } + + final boolean result = session.getAttribute(CONST_CAS_GATEWAY) != null; + session.removeAttribute(CONST_CAS_GATEWAY); + return result; + } + + public String storeGatewayInformation(final HttpServletRequest request, + final String serviceUrl) { + request.getSession(true).setAttribute(CONST_CAS_GATEWAY, "yes"); + return serviceUrl; + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/authentication/GatewayResolver.java b/modules/cas/src/main/java/org/jasig/cas/client/authentication/GatewayResolver.java new file mode 100644 index 00000000..2f4f5243 --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/authentication/GatewayResolver.java @@ -0,0 +1,52 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.authentication; + +import jakarta.servlet.http.HttpServletRequest; + +/** + * Implementations of this should only have a default constructor if + * you plan on constructing them via the web.xml. + * + * @author Scott Battaglia + * @version $Revision$ + * @since 1.0 + * + */ +public interface GatewayResolver { + + /** + * Determines if the request has been gatewayed already. Should also do gateway clean up. + * + * @param request the Http Servlet Request + * @param serviceUrl the service url + * @return true if yes, false otherwise. + */ + boolean hasGatewayedAlready(HttpServletRequest request, String serviceUrl); + + /** + * Storage the request for gatewaying and return the service url, which can be modified. + * + * @param request the HttpServletRequest. + * @param serviceUrl the service url + * @return the potentially modified service url to redirect to + */ + String storeGatewayInformation(HttpServletRequest request, String serviceUrl); +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/authentication/Saml11AuthenticationFilter.java b/modules/cas/src/main/java/org/jasig/cas/client/authentication/Saml11AuthenticationFilter.java new file mode 100644 index 00000000..656394c8 --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/authentication/Saml11AuthenticationFilter.java @@ -0,0 +1,45 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.authentication; + +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; + +/** + * Extension to the default Authentication filter that sets the required SAML1.1 artifact parameter name and service parameter name. + *

+ * Note, the "final" on this class helps ensure the compliance required in the initInternal method. + * + * @author Scott Battaglia + * @since 3.1.12 + * @version $Revision$ $Date$ + */ +public final class Saml11AuthenticationFilter extends AuthenticationFilter { + + protected void initInternal(final FilterConfig filterConfig) throws ServletException { + super.initInternal(filterConfig); + + log.warn("SAML1.1 compliance requires the [artifactParameterName] and [serviceParameterName] to be set to specified values."); + log.warn("This filter will overwrite any user-provided values (if any are provided)"); + + setArtifactParameterName("SAMLart"); + setServiceParameterName("TARGET"); + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/authentication/SimpleGroup.java b/modules/cas/src/main/java/org/jasig/cas/client/authentication/SimpleGroup.java new file mode 100644 index 00000000..0a74123a --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/authentication/SimpleGroup.java @@ -0,0 +1,71 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.authentication; + +import java.security.Principal; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; + +/** + * Simple security group implementation + * + * @author Marvin S. Addison + * @version $Revision$ + * @since 3.1.11 + * + */ +public final class SimpleGroup extends SimplePrincipal { + + /** SimpleGroup.java */ + private static final long serialVersionUID = 4382154818494550205L; + + /** Group members */ + private final Set members = new HashSet(); + + /** + * Creates a new group with the given name. + * @param name Group name. + */ + public SimpleGroup(final String name) { + super(name); + } + + public boolean addMember(final Principal user) { + return this.members.add(user); + } + + public boolean isMember(final Principal member) { + return this.members.contains(member); + } + + public Enumeration members() { + return Collections.enumeration(this.members); + } + + public boolean removeMember(final Principal user) { + return this.members.remove(user); + } + + public String toString() { + return super.toString() + ": " + members.toString(); + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/authentication/SimplePrincipal.java b/modules/cas/src/main/java/org/jasig/cas/client/authentication/SimplePrincipal.java new file mode 100644 index 00000000..fba5e09a --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/authentication/SimplePrincipal.java @@ -0,0 +1,73 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.authentication; + +import java.io.Serializable; +import java.security.Principal; + +import org.jasig.cas.client.util.CommonUtils; + +/** + * Simple security principal implementation. + * + * @author Marvin S. Addison + * @version $Revision$ + * @since 3.1.11 + * + */ +public class SimplePrincipal implements Principal, Serializable { + + /** SimplePrincipal.java */ + private static final long serialVersionUID = -5645357206342793145L; + + /** The unique identifier for this principal. */ + private final String name; + + /** + * Creates a new principal with the given name. + * @param name Principal name. + */ + public SimplePrincipal(final String name) { + this.name = name; + CommonUtils.assertNotNull(this.name, "name cannot be null."); + } + + public final String getName() { + return this.name; + } + + public String toString() { + return getName(); + } + + public boolean equals(final Object o) { + if (o == null) { + return false; + } else if (!(o instanceof SimplePrincipal)) { + return false; + } else { + return getName().equals(((SimplePrincipal)o).getName()); + } + } + + public int hashCode() { + return 37 * getName().hashCode(); + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/jaas/AssertionPrincipal.java b/modules/cas/src/main/java/org/jasig/cas/client/jaas/AssertionPrincipal.java new file mode 100644 index 00000000..6abeff65 --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/jaas/AssertionPrincipal.java @@ -0,0 +1,60 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.jaas; + +import java.io.Serializable; + +import org.jasig.cas.client.authentication.SimplePrincipal; +import org.jasig.cas.client.validation.Assertion; + +/** + * Principal implementation that contains the CAS ticket validation assertion. + * + * @author Marvin S. Addison + * @version $Revision$ + * @since 3.1.11 + * + */ +public class AssertionPrincipal extends SimplePrincipal implements Serializable { + + /** AssertionPrincipal.java */ + private static final long serialVersionUID = 2288520214366461693L; + + /** CAS assertion describing authenticated state */ + private Assertion assertion; + + /** + * Creates a new principal containing the CAS assertion. + * + * @param name Principal name. + * @param assertion CAS assertion. + */ + public AssertionPrincipal(final String name, final Assertion assertion) { + super(name); + this.assertion = assertion; + } + + /** + * @return CAS ticket validation assertion. + */ + public Assertion getAssertion() { + return this.assertion; + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/jaas/CasLoginModule.java b/modules/cas/src/main/java/org/jasig/cas/client/jaas/CasLoginModule.java new file mode 100644 index 00000000..cfec83a1 --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/jaas/CasLoginModule.java @@ -0,0 +1,493 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.jaas; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.jasig.cas.client.authentication.SimpleGroup; +import org.jasig.cas.client.authentication.SimplePrincipal; +import org.jasig.cas.client.util.CommonUtils; +import org.jasig.cas.client.util.ReflectUtils; +import org.jasig.cas.client.validation.Assertion; +import org.jasig.cas.client.validation.TicketValidator; + +import javax.security.auth.Subject; +import javax.security.auth.callback.*; +import javax.security.auth.login.LoginException; +import javax.security.auth.spi.LoginModule; +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.io.IOException; +import java.security.Principal; +import java.util.*; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +/** + * JAAS login module that delegates to a CAS {@link TicketValidator} component + * for authentication, and on success populates a {@link Subject} with principal + * data including NetID and principal attributes. The module expects to be provided + * with the CAS ticket (required) and service (optional) parameters via + * {@link PasswordCallback} and {@link NameCallback}, respectively, by the + * {@link CallbackHandler} that is part of the JAAS framework in which the servlet + * resides. + * + *

+ * Module configuration options: + *

+ * + *

+ * Module options not explicitly listed above are treated as attributes of the + * given ticket validator class, e.g. tolerance in the following example. + * + *

+ * Sample jaas.config file entry for this module: + *

+ * cas {
+ *   org.jasig.cas.client.jaas.CasLoginModule required
+ *     ticketValidatorClass="org.jasig.cas.client.validation.Saml11TicketValidator"
+ *     casServerUrlPrefix="https://cas.example.com/cas"
+ *     tolerance="20000"
+ *     service="https://webapp.example.com/webapp"
+ *     defaultRoles="admin,operator"
+ *     roleAttributeNames="memberOf,eduPersonAffiliation"
+ *     principalGroupName="CallerPrincipal"
+ *     roleGroupName="Roles";
+ * }
+ * 
+ * + * @author Marvin S. Addison + * @version $Revision$ $Date$ + * @since 3.1.11 + * + */ +public class CasLoginModule implements LoginModule { + /** Constant for login name stored in shared state. */ + public static final String LOGIN_NAME = "javax.security.auth.login.name"; + + /** + * Default group name for storing caller principal. + * The default value supports JBoss, but is configurable to hopefully + * support other JEE containers. + */ + public static final String DEFAULT_PRINCIPAL_GROUP_NAME = "CallerPrincipal"; + + /** + * Default group name for storing role membership data. + * The default value supports JBoss, but is configurable to hopefully + * support other JEE containers. + */ + public static final String DEFAULT_ROLE_GROUP_NAME = "Roles"; + + /** + * Default assertion cache timeout in minutes. Default is 8 hours. + */ + public static final int DEFAULT_CACHE_TIMEOUT = 480; + + /** + * Stores mapping of ticket to assertion to support JAAS providers that + * attempt to periodically re-authenticate to renew principal. Since + * CAS tickets are one-time-use, a cached assertion must be provided on + * re-authentication. + */ + protected static final Map ASSERTION_CACHE = new HashMap(); + + /** Executor responsible for assertion cache cleanup */ + protected static Executor cacheCleanerExecutor = Executors.newSingleThreadExecutor(); + + /** Logger instance */ + protected final Log log = LogFactory.getLog(getClass()); + + /** JAAS authentication subject */ + protected Subject subject; + + /** JAAS callback handler */ + protected CallbackHandler callbackHandler; + + /** CAS ticket validator */ + protected TicketValidator ticketValidator; + + /** CAS service parameter used if no service is provided via TextCallback on login */ + protected String service; + + /** CAS assertion */ + protected Assertion assertion; + + /** CAS ticket credential */ + protected TicketCredential ticket; + + /** Login module shared state */ + protected Map sharedState; + + /** Roles to be added to all authenticated principals by default */ + protected String[] defaultRoles; + + /** Names of attributes in the CAS assertion that should be used for role data */ + protected Set roleAttributeNames = new HashSet(); + + /** Name of JAAS Group containing caller principal */ + protected String principalGroupName = DEFAULT_PRINCIPAL_GROUP_NAME; + + /** Name of JAAS Group containing role data */ + protected String roleGroupName = DEFAULT_ROLE_GROUP_NAME; + + /** Enables or disable assertion caching */ + protected boolean cacheAssertions; + + /** Assertion cache timeout in minutes */ + protected int cacheTimeout = DEFAULT_CACHE_TIMEOUT; + + /** + * Initializes the CAS login module. + * @param subject Authentication subject. + * @param handler Callback handler. + * @param state Shared state map. + * @param options Login module options. The following are supported: + *
    + *
  • service - CAS service URL used for service ticket validation.
  • + *
  • ticketValidatorClass - fully-qualified class name of service ticket validator component.
  • + *
  • defaultRoles (optional) - comma-delimited list of roles to be added to all authenticated principals.
  • + *
  • roleAttributeNames (optional) - comma-delimited list of attributes in the CAS assertion that contain role data.
  • + *
  • principalGroupName (optional) - name of JAAS Group containing caller principal.
  • + *
  • roleGroupName (optional) - name of JAAS Group containing role data
  • + *
  • cacheAssertions (optional) - whether or not to cache assertions. + * Some JAAS providers attempt to reauthenticate users after an indeterminate + * period of time. Since the credential used for authentication is a CAS ticket, + * which by default are single use, reauthentication fails. Assertion caching addresses this + * behavior.
  • + *
  • cacheTimeout (optional) - assertion cache timeout in minutes.
  • + *
+ */ + + + public void initialize(final Subject subject, final CallbackHandler handler, final Map state, final Map options) { + this.assertion = null; + this.callbackHandler = handler; + this.subject = subject; + this.sharedState = new HashMap(state); + + String ticketValidatorClass = null; + + for (final String key : options.keySet()) { + log.trace("Processing option " + key); + if ("service".equals(key)) { + this.service = (String) options.get(key); + log.debug("Set service=" + this.service); + } else if ("ticketValidatorClass".equals(key)) { + ticketValidatorClass = (String) options.get(key); + log.debug("Set ticketValidatorClass=" + ticketValidatorClass); + } else if ("defaultRoles".equals(key)) { + final String roles = (String) options.get(key); + log.trace("Got defaultRoles value " + roles); + this.defaultRoles = roles.split(",\\s*"); + log.debug("Set defaultRoles=" + Arrays.asList(this.defaultRoles)); + } else if ("roleAttributeNames".equals(key)) { + final String attrNames = (String) options.get(key); + log.trace("Got roleAttributeNames value " + attrNames); + final String[] attributes = attrNames.split(",\\s*"); + this.roleAttributeNames.addAll(Arrays.asList(attributes)); + log.debug("Set roleAttributeNames=" + this.roleAttributeNames); + } else if ("principalGroupName".equals(key)) { + this.principalGroupName = (String) options.get(key); + log.debug("Set principalGroupName=" + this.principalGroupName); + } else if ("roleGroupName".equals(key)) { + this.roleGroupName = (String) options.get(key); + log.debug("Set roleGroupName=" + this.roleGroupName); + } else if ("cacheAssertions".equals(key)) { + this.cacheAssertions = Boolean.parseBoolean((String) options.get(key)); + log.debug("Set cacheAssertions=" + this.cacheAssertions); + } else if ("cacheTimeout".equals(key)) { + this.cacheTimeout = Integer.parseInt((String) options.get(key)); + log.debug("Set cacheTimeout=" + this.cacheTimeout); + } + } + + if (this.cacheAssertions) { + cacheCleanerExecutor.execute(new CacheCleaner()); + } + + CommonUtils.assertNotNull(ticketValidatorClass, "ticketValidatorClass is required."); + this.ticketValidator = createTicketValidator(ticketValidatorClass, options); + } + + public boolean login() throws LoginException { + log.debug("Performing login."); + final NameCallback serviceCallback = new NameCallback("service"); + final PasswordCallback ticketCallback = new PasswordCallback("ticket", false); + try { + this.callbackHandler.handle(new Callback[] { ticketCallback, serviceCallback }); + } catch (final IOException e) { + log.info("Login failed due to IO exception in callback handler: " + e); + throw (LoginException) new LoginException("IO exception in callback handler: " + e).initCause(e); + } catch (final UnsupportedCallbackException e) { + log.info("Login failed due to unsupported callback: " + e); + throw (LoginException) new LoginException("Callback handler does not support PasswordCallback and TextInputCallback.").initCause(e); + } + + if (ticketCallback.getPassword() != null) { + this.ticket = new TicketCredential(new String(ticketCallback.getPassword())); + final String service = CommonUtils.isNotBlank(serviceCallback.getName()) ? serviceCallback.getName() : this.service; + + if (this.cacheAssertions) { + synchronized(ASSERTION_CACHE) { + if (ASSERTION_CACHE.get(ticket) != null) { + log.debug("Assertion found in cache."); + this.assertion = ASSERTION_CACHE.get(ticket); + } + } + } + + if (this.assertion == null) { + log.debug("CAS assertion is null; ticket validation required."); + if (CommonUtils.isBlank(service)) { + log.info("Login failed because required CAS service parameter not provided."); + throw new LoginException("Neither login module nor callback handler provided required service parameter."); + } + try { + if (log.isDebugEnabled()) { + log.debug("Attempting ticket validation with service=" + service + " and ticket=" + ticket); + } + this.assertion = this.ticketValidator.validate(this.ticket.getName(), service); + + } catch (final Exception e) { + log.info("Login failed due to CAS ticket validation failure: " + e); + throw (LoginException) new LoginException("CAS ticket validation failed: " + e).initCause(e); + } + } + log.info("Login succeeded."); + } else { + log.info("Login failed because callback handler did not provide CAS ticket."); + throw new LoginException("Callback handler did not provide CAS ticket."); + } + return true; + } + + public boolean abort() throws LoginException { + if (this.ticket != null) { + this.ticket = null; + } + if (this.assertion != null) { + this.assertion = null; + } + return true; + } + + public boolean commit() throws LoginException { + if (this.assertion != null) { + if (this.ticket != null) { + this.subject.getPrivateCredentials().add(this.ticket); + } else { + throw new LoginException("Ticket credential not found."); + } + + final AssertionPrincipal casPrincipal = new AssertionPrincipal(this.assertion.getPrincipal().getName(), this.assertion); + this.subject.getPrincipals().add(casPrincipal); + + // Add group containing principal as sole member + // Supports JBoss JAAS use case + final SimpleGroup principalGroup = new SimpleGroup(this.principalGroupName); + principalGroup.addMember(casPrincipal); + this.subject.getPrincipals().add(principalGroup); + + // Add group principal containing role data + final SimpleGroup roleGroup = new SimpleGroup(this.roleGroupName); + + for (final String defaultRole : defaultRoles) { + roleGroup.addMember(new SimplePrincipal(defaultRole)); + } + + final Map attributes = this.assertion.getPrincipal().getAttributes(); + for (final String key : attributes.keySet()) { + if (this.roleAttributeNames.contains(key)) { + // Attribute value is Object if singular or Collection if plural + final Object value = attributes.get(key); + if (value instanceof Collection) { + for (final Object o : (Collection) value) { + roleGroup.addMember(new SimplePrincipal(o.toString())); + } + } else { + roleGroup.addMember(new SimplePrincipal(value.toString())); + } + } + } + this.subject.getPrincipals().add(roleGroup); + + // Place principal name in shared state for downstream JAAS modules (module chaining use case) + this.sharedState.put(LOGIN_NAME, new Object()); // casPrincipal.getName()); + + if (log.isDebugEnabled()) { + if (log.isDebugEnabled()) { + log.debug("Created JAAS subject with principals: " + subject.getPrincipals()); + } + } + + if (this.cacheAssertions) { + if (log.isDebugEnabled()) { + log.debug("Caching assertion for principal " + this.assertion.getPrincipal()); + } + ASSERTION_CACHE.put(this.ticket, this.assertion); + } + } else { + // Login must have failed if there is no assertion defined + // Need to clean up state + if (this.ticket != null) { + this.ticket = null; + } + } + return true; + } + + public boolean logout() throws LoginException { + log.debug("Performing logout."); + + // Remove all CAS principals + removePrincipalsOfType(AssertionPrincipal.class); + removePrincipalsOfType(SimplePrincipal.class); + removePrincipalsOfType(SimpleGroup.class); + + // Remove all CAS credentials + removeCredentialsOfType(TicketCredential.class); + + log.info("Logout succeeded."); + return true; + } + + + /** + * Creates a {@link TicketValidator} instance from a class name and map of property name/value pairs. + * @param className Fully-qualified name of {@link TicketValidator} concrete class. + * @param propertyMap Map of property name/value pairs to set on validator instance. + * @return Ticket validator with properties set. + */ + private TicketValidator createTicketValidator(final String className, final Map propertyMap) { + CommonUtils.assertTrue(propertyMap.containsKey("casServerUrlPrefix"), "Required property casServerUrlPrefix not found."); + + final Class validatorClass = ReflectUtils.loadClass(className); + final TicketValidator validator = ReflectUtils.newInstance(validatorClass, propertyMap.get("casServerUrlPrefix")); + + try { + final BeanInfo info = Introspector.getBeanInfo(validatorClass); + + for (final String property : propertyMap.keySet()) { + if (!"casServerUrlPrefix".equals(property)) { + log.debug("Attempting to set TicketValidator property " + property); + final String value = (String) propertyMap.get(property); + final PropertyDescriptor pd = ReflectUtils.getPropertyDescriptor(info, property); + if (pd != null) { + ReflectUtils.setProperty(property, convertIfNecessary(pd, value), validator, info); + log.debug("Set " + property + "=" + value); + } else { + log.warn("Cannot find property " + property + " on " + className); + } + } + } + } catch (final IntrospectionException e) { + throw new RuntimeException("Error getting bean info for " + validatorClass, e); + } + + return validator; + } + + /** + * Attempts to do simple type conversion from a string value to the type expected + * by the given property. + * + * Currently only conversion to int, long, and boolean are supported. + * + * @param pd Property descriptor of target property to set. + * @param value Property value as a string. + * @return Value converted to type expected by property if a conversion strategy exists. + */ + private static Object convertIfNecessary(final PropertyDescriptor pd, final String value) { + if (String.class.equals(pd.getPropertyType())) { + return value; + } else if (boolean.class.equals(pd.getPropertyType())) { + return Boolean.valueOf(value); + } else if (int.class.equals(pd.getPropertyType())) { + return Integer.valueOf(value); + } else if (long.class.equals(pd.getPropertyType())) { + return Long.valueOf(value); + } else { + throw new IllegalArgumentException("No conversion strategy exists for property " + pd.getName() + " of type " + pd.getPropertyType()); + } + } + + /** + * Removes all principals of the given type from the JAAS subject. + * @param clazz Type of principal to remove. + */ + private void removePrincipalsOfType(final Class clazz) { + this.subject.getPrincipals().removeAll(this.subject.getPrincipals(clazz)); + } + + /** + * Removes all credentials of the given type from the JAAS subject. + * @param clazz Type of principal to remove. + */ + private void removeCredentialsOfType(final Class clazz) { + this.subject.getPrivateCredentials().removeAll(this.subject.getPrivateCredentials(clazz)); + } + + /** Removes expired entries from the assertion cache. */ + private class CacheCleaner implements Runnable { + public void run() { + if (log.isDebugEnabled()) { + log.debug("Cleaning assertion cache of size " + CasLoginModule.ASSERTION_CACHE.size()); + } + final Iterator> iter = + CasLoginModule.ASSERTION_CACHE.entrySet().iterator(); + final Calendar cutoff = Calendar.getInstance(); + cutoff.add(Calendar.MINUTE, -CasLoginModule.this.cacheTimeout); + while (iter.hasNext()) { + final Assertion assertion = iter.next().getValue(); + final Calendar created = Calendar.getInstance(); + created.setTime(assertion.getValidFromDate()); + if (created.before(cutoff)) { + if (log.isDebugEnabled()) { + log.debug("Removing expired assertion for principal " + assertion.getPrincipal()); + } + iter.remove(); + } + } + } + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/jaas/ServiceAndTicketCallbackHandler.java b/modules/cas/src/main/java/org/jasig/cas/client/jaas/ServiceAndTicketCallbackHandler.java new file mode 100644 index 00000000..62bced72 --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/jaas/ServiceAndTicketCallbackHandler.java @@ -0,0 +1,71 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.jaas; + +import java.io.IOException; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; + +/** + * Callback handler that provides the CAS service and ticket to a + * {@link NameCallback} and {@link PasswordCallback} respectively, + * which meets the requirements of the {@link CasLoginModule} JAAS module. + * + * @author Marvin S. Addison + * @version $Revision$ + * @since 3.1.11 + * + */ +public class ServiceAndTicketCallbackHandler implements CallbackHandler { + + /** CAS service URL */ + private final String service; + + /** CAS service ticket */ + private final String ticket; + + /** + * Creates a new instance with the given service and ticket. + * + * @param service CAS service URL. + * @param ticket CAS service ticket. + */ + public ServiceAndTicketCallbackHandler(final String service, final String ticket) { + this.service = service; + this.ticket = ticket; + } + + public void handle(final Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (final Callback callback : callbacks) { + if (callback instanceof NameCallback) { + ((NameCallback) callback).setName(this.service); + } else if (callback instanceof PasswordCallback) { + ((PasswordCallback) callback).setPassword(this.ticket.toCharArray()); + } else { + throw new UnsupportedCallbackException(callback, "Callback not supported."); + } + } + } + +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/jaas/TicketCredential.java b/modules/cas/src/main/java/org/jasig/cas/client/jaas/TicketCredential.java new file mode 100644 index 00000000..2f7e92e5 --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/jaas/TicketCredential.java @@ -0,0 +1,72 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.jaas; + +import java.security.Principal; + +/** + * Strongly-typed wrapper for a ticket credential. + * + * @author Marvin S. Addison + * @version $Revision$ $Date$ + * @since 3.1.12 + * + */ +public final class TicketCredential implements Principal { + + /** Hash code seed value */ + private static final int HASHCODE_SEED = 17; + + /** Ticket ID string */ + private String ticket; + + /** + * Creates a new instance that wraps the given ticket. + * @param ticket Ticket identifier string. + */ + public TicketCredential(final String ticket) { + this.ticket = ticket; + } + + public String getName() { + return this.ticket; + } + + public String toString() { + return this.ticket; + } + + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final TicketCredential that = (TicketCredential) o; + + if (ticket != null ? !ticket.equals(that.ticket) : that.ticket != null) return false; + + return true; + } + + public int hashCode() { + int hash = HASHCODE_SEED; + hash = hash * 31 + (ticket == null ? 0 : ticket.hashCode()); + return hash; + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/proxy/AbstractEncryptedProxyGrantingTicketStorageImpl.java b/modules/cas/src/main/java/org/jasig/cas/client/proxy/AbstractEncryptedProxyGrantingTicketStorageImpl.java new file mode 100644 index 00000000..b9c3e4df --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/proxy/AbstractEncryptedProxyGrantingTicketStorageImpl.java @@ -0,0 +1,92 @@ +package org.jasig.cas.client.proxy; + +import javax.crypto.Cipher; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.DESKeySpec; +import javax.crypto.spec.DESedeKeySpec; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; + +/** + * Provides encryption capabilities. Not entirely safe to configure since we have no way of controlling the + * key and cipher being set. + * + * @author Scott Battaglia + * @version $Revision$ $Date$ + * @since 3.2.0 + */ +public abstract class AbstractEncryptedProxyGrantingTicketStorageImpl implements ProxyGrantingTicketStorage { + + public static final String DEFAULT_ENCRYPTION_ALGORITHM = "DESede"; + + private Key key; + + private String cipherAlgorithm = DEFAULT_ENCRYPTION_ALGORITHM; + + public final void setSecretKey(final String key) throws NoSuchAlgorithmException, InvalidKeyException, InvalidKeySpecException { + this.key = SecretKeyFactory.getInstance(this.cipherAlgorithm).generateSecret(new DESedeKeySpec(key.getBytes())); + } + + public final void setSecretKey(final Key key) { + this.key = key; + } + + /** + * Note: you MUST call this method before calling setSecretKey if you're not using the default algorithm. You've been warned. + * + * @param cipherAlgorithm the cipher algorithm. + */ + public final void setCipherAlgorithm(final String cipherAlgorithm) { + this.cipherAlgorithm = cipherAlgorithm; + } + + public final void save(final String proxyGrantingTicketIou, final String proxyGrantingTicket) { + saveInternal(proxyGrantingTicketIou, encrypt(proxyGrantingTicket)); + } + + public final String retrieve(final String proxyGrantingTicketIou) { + return decrypt(retrieveInternal(proxyGrantingTicketIou)); + } + + protected abstract void saveInternal(String proxyGrantingTicketIou, String proxyGrantingTicket); + + protected abstract String retrieveInternal(String proxyGrantingTicketIou); + + private String encrypt(final String value) { + if (this.key == null) { + return value; + } + + if (value == null) { + return null; + } + + try { + final Cipher cipher = Cipher.getInstance(this.cipherAlgorithm); + cipher.init(Cipher.ENCRYPT_MODE, this.key); + return new String(cipher.doFinal(value.getBytes())); + } catch (final Exception e) { + throw new RuntimeException(e); + } + } + + private String decrypt(final String value) { + if (this.key == null) { + return value; + } + + if (value == null) { + return null; + } + + try { + final Cipher cipher = Cipher.getInstance(this.cipherAlgorithm); + cipher.init(Cipher.DECRYPT_MODE, this.key); + return new String(cipher.doFinal(value.getBytes())); + } catch (final Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/proxy/Cas20ProxyRetriever.java b/modules/cas/src/main/java/org/jasig/cas/client/proxy/Cas20ProxyRetriever.java new file mode 100644 index 00000000..45adbaed --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/proxy/Cas20ProxyRetriever.java @@ -0,0 +1,96 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.proxy; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.jasig.cas.client.util.CommonUtils; +import org.jasig.cas.client.util.XmlUtils; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; + +/** + * Implementation of a ProxyRetriever that follows the CAS 2.0 specification. + * For more information on the CAS 2.0 specification, please see the specification + * document. + *

+ * In general, this class will make a call to the CAS server with some specified + * parameters and receive an XML response to parse. + * + * @author Scott Battaglia + * @version $Revision: 11729 $ $Date: 2007-09-26 14:22:30 -0400 (Tue, 26 Sep 2007) $ + * @since 3.0 + */ +public final class Cas20ProxyRetriever implements ProxyRetriever { + + /** Unique Id for serialization. */ + private static final long serialVersionUID = 560409469568911791L; + + /** + * Instance of Commons Logging. + */ + private final Log log = LogFactory.getLog(this.getClass()); + + /** + * Url to CAS server. + */ + private final String casServerUrl; + + private final String encoding; + + /** + * Main Constructor. + * + * @param casServerUrl the URL to the CAS server (i.e. http://localhost/cas/) + * @param encoding the encoding to use. + */ + public Cas20ProxyRetriever(final String casServerUrl, final String encoding) { + CommonUtils.assertNotNull(casServerUrl, "casServerUrl cannot be null."); + this.casServerUrl = casServerUrl; + this.encoding = encoding; + } + + public String getProxyTicketIdFor(final String proxyGrantingTicketId, + final String targetService) { + + final String url = constructUrl(proxyGrantingTicketId, targetService); + final String response = CommonUtils.getResponseFromServer(url, this.encoding); + final String error = XmlUtils.getTextForElement(response, "proxyFailure"); + + if (CommonUtils.isNotEmpty(error)) { + log.debug(error); + return null; + } + + return XmlUtils.getTextForElement(response, "proxyTicket"); + } + + private String constructUrl(final String proxyGrantingTicketId, final String targetService) { + try { + return this.casServerUrl + (this.casServerUrl.endsWith("/") ? "" : "/") + "proxy" + "?pgt=" + + proxyGrantingTicketId + "&targetService=" + + URLEncoder.encode(targetService, "UTF-8"); + } catch (final UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/proxy/CleanUpTimerTask.java b/modules/cas/src/main/java/org/jasig/cas/client/proxy/CleanUpTimerTask.java new file mode 100644 index 00000000..e48986c4 --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/proxy/CleanUpTimerTask.java @@ -0,0 +1,45 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.proxy; + +import java.util.TimerTask; + +/** + * A {@link TimerTask} implementation which performs the + * actual 'cleaning' by calling {@link ProxyGrantingTicketStorage#cleanUp()}. + *

+ * By default, the {@link org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter} configures + * a task that cleans up the {@link org.jasig.cas.client.proxy.ProxyGrantingTicketStorage} associated with it. + * + * @author Brad Cupit (brad [at] lsu {dot} edu) + * @version $Revision$ $Date$ + * @since 3.1.6 + */ +public final class CleanUpTimerTask extends TimerTask { + + private final ProxyGrantingTicketStorage proxyGrantingTicketStorage; + + public CleanUpTimerTask(final ProxyGrantingTicketStorage proxyGrantingTicketStorage) { + this.proxyGrantingTicketStorage = proxyGrantingTicketStorage; + } + public void run() { + this.proxyGrantingTicketStorage.cleanUp(); + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/proxy/ProxyGrantingTicketStorage.java b/modules/cas/src/main/java/org/jasig/cas/client/proxy/ProxyGrantingTicketStorage.java new file mode 100644 index 00000000..d4bf58ae --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/proxy/ProxyGrantingTicketStorage.java @@ -0,0 +1,56 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.proxy; + +/** + * Interface for the storage and retrieval of ProxyGrantingTicketIds by mapping + * them to a specific ProxyGrantingTicketIou. + * + * @author Scott Battaglia + * @version $Revision: 11729 $ $Date: 2007-09-26 14:22:30 -0400 (Tue, 26 Sep 2007) $ + * @since 3.0 + */ +public interface ProxyGrantingTicketStorage { + + /** + * Method to save the ProxyGrantingTicket to the backing storage facility. + * + * @param proxyGrantingTicketIou used as the key + * @param proxyGrantingTicket used as the value + */ + public void save(String proxyGrantingTicketIou, String proxyGrantingTicket); + + /** + * Method to retrieve a ProxyGrantingTicket based on the + * ProxyGrantingTicketIou. Note that implementations are not guaranteed to + * return the same result if retrieve is called twice with the same + * proxyGrantingTicketIou. + * + * @param proxyGrantingTicketIou used as the key + * @return the ProxyGrantingTicket Id or null if it can't be found + */ + public String retrieve(String proxyGrantingTicketIou); + + /** + * Called on a regular basis by an external timer, + * giving implementations a chance to remove stale data. + */ + public void cleanUp(); +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/proxy/ProxyGrantingTicketStorageImpl.java b/modules/cas/src/main/java/org/jasig/cas/client/proxy/ProxyGrantingTicketStorageImpl.java new file mode 100644 index 00000000..a1d13941 --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/proxy/ProxyGrantingTicketStorageImpl.java @@ -0,0 +1,140 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.proxy; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Implementation of {@link ProxyGrantingTicketStorage} that is backed by a + * HashMap that keeps a ProxyGrantingTicket for a specified amount of time. + *

+ * {@link ProxyGrantingTicketStorage#cleanUp()} must be called on a regular basis to + * keep the HashMap from growing indefinitely. + * + * @author Scott Battaglia + * @author Brad Cupit (brad [at] lsu {dot} edu) + * @version $Revision: 11729 $ $Date: 2007-09-26 14:22:30 -0400 (Tue, 26 Sep 2007) $ + * @since 3.0 + */ +public final class ProxyGrantingTicketStorageImpl implements ProxyGrantingTicketStorage { + + private final Log log = LogFactory.getLog(getClass()); + + /** + * Default timeout in milliseconds. + */ + private static final long DEFAULT_TIMEOUT = 60000; + + /** + * Map that stores the PGTIOU to PGT mappings. + */ + private final ConcurrentMap cache = new ConcurrentHashMap(); + + /** + * time, in milliseconds, before a {@link ProxyGrantingTicketHolder} + * is considered expired and ready for removal. + * + * @see ProxyGrantingTicketStorageImpl#DEFAULT_TIMEOUT + */ + private long timeout; + + /** + * Constructor set the timeout to the default value. + */ + public ProxyGrantingTicketStorageImpl() { + this(DEFAULT_TIMEOUT); + } + + /** + * Sets the amount of time to hold on to a ProxyGrantingTicket if its never + * been retrieved. + * + * @param timeout the time to hold on to the ProxyGrantingTicket + */ + public ProxyGrantingTicketStorageImpl(final long timeout) { + this.timeout = timeout; + } + + /** + * NOTE: you can only retrieve a ProxyGrantingTicket once with this method. + * Its removed after retrieval. + */ + public String retrieve(final String proxyGrantingTicketIou) { + final ProxyGrantingTicketHolder holder = this.cache.get(proxyGrantingTicketIou); + + if (holder == null) { + log.info("No Proxy Ticket found for [" + proxyGrantingTicketIou + "]."); + return null; + } + + this.cache.remove(proxyGrantingTicketIou); + + if (log.isDebugEnabled()) { + log.debug("Returned ProxyGrantingTicket of [" + holder.getProxyGrantingTicket() + "]"); + } + return holder.getProxyGrantingTicket(); + } + + public void save(final String proxyGrantingTicketIou, final String proxyGrantingTicket) { + final ProxyGrantingTicketHolder holder = new ProxyGrantingTicketHolder(proxyGrantingTicket); + + if (log.isDebugEnabled()) { + log.debug("Saving ProxyGrantingTicketIOU and ProxyGrantingTicket combo: [" + proxyGrantingTicketIou + ", " + proxyGrantingTicket + "]"); + } + this.cache.put(proxyGrantingTicketIou, holder); + } + + /** + * Cleans up old, expired proxy tickets. This method must be + * called regularly via an external thread or timer. + */ + public void cleanUp() { + for (final Map.Entry holder : this.cache.entrySet()) { + if (holder.getValue().isExpired(this.timeout)) { + this.cache.remove(holder.getKey()); + } + } + } + + private static final class ProxyGrantingTicketHolder { + + private final String proxyGrantingTicket; + + private final long timeInserted; + + protected ProxyGrantingTicketHolder(final String proxyGrantingTicket) { + this.proxyGrantingTicket = proxyGrantingTicket; + this.timeInserted = System.currentTimeMillis(); + } + + public String getProxyGrantingTicket() { + return this.proxyGrantingTicket; + } + + final boolean isExpired(final long timeout) { + return System.currentTimeMillis() - this.timeInserted > timeout; + } + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/proxy/ProxyRetriever.java b/modules/cas/src/main/java/org/jasig/cas/client/proxy/ProxyRetriever.java new file mode 100644 index 00000000..14c006f7 --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/proxy/ProxyRetriever.java @@ -0,0 +1,43 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.proxy; + +import java.io.Serializable; + +/** + * Interface to abstract the retrieval of a proxy ticket to make the + * implementation a black box to the client. + * + * @author Scott Battaglia + * @version $Revision: 11729 $ $Date: 2007-09-26 14:22:30 -0400 (Tue, 26 Sep 2007) $ + * @since 3.0 + */ +public interface ProxyRetriever extends Serializable { + + /** + * Retrieves a proxy ticket for a specific targetService. + * + * @param proxyGrantingTicketId the ProxyGrantingTicketId + * @param targetService the service we want to proxy. + * @return the ProxyTicket Id if Granted, null otherwise. + */ + String getProxyTicketIdFor(String proxyGrantingTicketId, + String targetService); +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/session/HashMapBackedSessionMappingStorage.java b/modules/cas/src/main/java/org/jasig/cas/client/session/HashMapBackedSessionMappingStorage.java new file mode 100644 index 00000000..c3653c62 --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/session/HashMapBackedSessionMappingStorage.java @@ -0,0 +1,85 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.session; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.util.HashMap; +import java.util.Map; + +import jakarta.servlet.http.HttpSession; + +/** + * HashMap backed implementation of SessionMappingStorage. + * + * @author Scott Battaglia + * @version $Revision$ $Date$ + * @since 3.1 + * + */ +public final class HashMapBackedSessionMappingStorage implements SessionMappingStorage { + + /** + * Maps the ID from the CAS server to the Session. + */ + private final Map MANAGED_SESSIONS = new HashMap(); + + /** + * Maps the Session ID to the key from the CAS Server. + */ + private final Map ID_TO_SESSION_KEY_MAPPING = new HashMap(); + + private final Log log = LogFactory.getLog(getClass()); + + public synchronized void addSessionById(String mappingId, HttpSession session) { + ID_TO_SESSION_KEY_MAPPING.put(session.getId(), mappingId); + MANAGED_SESSIONS.put(mappingId, session); + + } + + public synchronized void removeBySessionById(String sessionId) { + if (log.isDebugEnabled()) { + log.debug("Attempting to remove Session=[" + sessionId + "]"); + } + + final String key = ID_TO_SESSION_KEY_MAPPING.get(sessionId); + + if (log.isDebugEnabled()) { + if (key != null) { + log.debug("Found mapping for session. Session Removed."); + } else { + log.debug("No mapping for session found. Ignoring."); + } + } + MANAGED_SESSIONS.remove(key); + ID_TO_SESSION_KEY_MAPPING.remove(sessionId); + } + + public synchronized HttpSession removeSessionByMappingId(String mappingId) { + final HttpSession session = MANAGED_SESSIONS.get(mappingId); + + if (session != null) { + removeBySessionById(session.getId()); + } + + return session; + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/session/SessionMappingStorage.java b/modules/cas/src/main/java/org/jasig/cas/client/session/SessionMappingStorage.java new file mode 100644 index 00000000..9c7f7134 --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/session/SessionMappingStorage.java @@ -0,0 +1,55 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.session; + +import jakarta.servlet.http.HttpSession; + +/** + * Stores the mapping between sessions and keys to be retrieved later. + * + * @author Scott Battaglia + * @version $Revision$ $Date$ + * @since 3.1 + * + */ +public interface SessionMappingStorage { + + /** + * Remove the HttpSession based on the mappingId. + * + * @param mappingId the id the session is keyed under. + * @return the HttpSession if it exists. + */ + HttpSession removeSessionByMappingId(String mappingId); + + /** + * Remove a session by its Id. + * @param sessionId the id of the session. + */ + void removeBySessionById(String sessionId); + + /** + * Add a session by its mapping Id. + * @param mappingId the id to map the session to. + * @param session the HttpSession. + */ + void addSessionById(String mappingId, HttpSession session); + +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/session/SingleSignOutFilter.java b/modules/cas/src/main/java/org/jasig/cas/client/session/SingleSignOutFilter.java new file mode 100644 index 00000000..87adf219 --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/session/SingleSignOutFilter.java @@ -0,0 +1,86 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.session; + +import org.jasig.cas.client.util.AbstractConfigurationFilter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import java.io.IOException; + +/** + * Implements the Single Sign Out protocol. It handles registering the session and destroying the session. + * + * @author Scott Battaglia + * @version $Revision$ $Date$ + * @since 3.1 + */ +public final class SingleSignOutFilter extends AbstractConfigurationFilter { + + private static final SingleSignOutHandler handler = new SingleSignOutHandler(); + + public void init(final FilterConfig filterConfig) throws ServletException { + if (!isIgnoreInitConfiguration()) { + handler.setArtifactParameterName(getPropertyFromInitParams(filterConfig, "artifactParameterName", "ticket")); + handler.setLogoutParameterName(getPropertyFromInitParams(filterConfig, "logoutParameterName", "logoutRequest")); + } + handler.init(); + } + + public void setArtifactParameterName(final String name) { + handler.setArtifactParameterName(name); + } + + public void setLogoutParameterName(final String name) { + handler.setLogoutParameterName(name); + } + + public void setSessionMappingStorage(final SessionMappingStorage storage) { + handler.setSessionMappingStorage(storage); + } + + public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException { + final HttpServletRequest request = (HttpServletRequest) servletRequest; + + if (handler.isTokenRequest(request)) { + handler.recordSession(request); + } else if (handler.isLogoutRequest(request)) { + handler.destroySession(request); + // Do not continue up filter chain + return; + } else { + log.trace("Ignoring URI " + request.getRequestURI()); + } + + filterChain.doFilter(servletRequest, servletResponse); + } + + public void destroy() { + // nothing to do + } + + protected static SingleSignOutHandler getSingleSignOutHandler() { + return handler; + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/session/SingleSignOutHandler.java b/modules/cas/src/main/java/org/jasig/cas/client/session/SingleSignOutHandler.java new file mode 100644 index 00000000..ce24ad39 --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/session/SingleSignOutHandler.java @@ -0,0 +1,162 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.session; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.jasig.cas.client.util.CommonUtils; +import org.jasig.cas.client.util.XmlUtils; + +/** + * Performs CAS single sign-out operations in an API-agnostic fashion. + * + * @author Marvin S. Addison + * @version $Revision$ $Date$ + * @since 3.1.12 + * + */ +public final class SingleSignOutHandler { + + /** Logger instance */ + private final Log log = LogFactory.getLog(getClass()); + + /** Mapping of token IDs and session IDs to HTTP sessions */ + private SessionMappingStorage sessionMappingStorage = new HashMapBackedSessionMappingStorage(); + + /** The name of the artifact parameter. This is used to capture the session identifier. */ + private String artifactParameterName = "ticket"; + + /** Parameter name that stores logout request */ + private String logoutParameterName = "logoutRequest"; + + + public void setSessionMappingStorage(final SessionMappingStorage storage) { + this.sessionMappingStorage = storage; + } + + public SessionMappingStorage getSessionMappingStorage() { + return this.sessionMappingStorage; + } + + /** + * @param name Name of the authentication token parameter. + */ + public void setArtifactParameterName(final String name) { + this.artifactParameterName = name; + } + + /** + * @param name Name of parameter containing CAS logout request message. + */ + public void setLogoutParameterName(final String name) { + this.logoutParameterName = name; + } + + /** + * Initializes the component for use. + */ + public void init() { + CommonUtils.assertNotNull(this.artifactParameterName, "artifactParameterName cannot be null."); + CommonUtils.assertNotNull(this.logoutParameterName, "logoutParameterName cannot be null."); + CommonUtils.assertNotNull(this.sessionMappingStorage, "sessionMappingStorage cannote be null."); + } + + /** + * Determines whether the given request contains an authentication token. + * + * @param request HTTP reqest. + * + * @return True if request contains authentication token, false otherwise. + */ + public boolean isTokenRequest(final HttpServletRequest request) { + return CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.artifactParameterName)); + } + + /** + * Determines whether the given request is a CAS logout request. + * + * @param request HTTP request. + * + * @return True if request is logout request, false otherwise. + */ + public boolean isLogoutRequest(final HttpServletRequest request) { + return "POST".equals(request.getMethod()) && !isMultipartRequest(request) && + CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.logoutParameterName)); + } + + /** + * Associates a token request with the current HTTP session by recording the mapping + * in the the configured {@link SessionMappingStorage} container. + * + * @param request HTTP request containing an authentication token. + */ + public void recordSession(final HttpServletRequest request) { + final HttpSession session = request.getSession(true); + + final String token = CommonUtils.safeGetParameter(request, this.artifactParameterName); + if (log.isDebugEnabled()) { + log.debug("Recording session for token " + token); + } + + try { + this.sessionMappingStorage.removeBySessionById(session.getId()); + } catch (final Exception e) { + // ignore if the session is already marked as invalid. Nothing we can do! + } + sessionMappingStorage.addSessionById(token, session); + } + + /** + * Destroys the current HTTP session for the given CAS logout request. + * + * @param request HTTP request containing a CAS logout message. + */ + public void destroySession(final HttpServletRequest request) { + final String logoutMessage = CommonUtils.safeGetParameter(request, this.logoutParameterName); + if (log.isTraceEnabled()) { + log.trace ("Logout request:\n" + logoutMessage); + } + + final String token = XmlUtils.getTextForElement(logoutMessage, "SessionIndex"); + if (CommonUtils.isNotBlank(token)) { + final HttpSession session = this.sessionMappingStorage.removeSessionByMappingId(token); + + if (session != null) { + String sessionID = session.getId(); + + if (log.isDebugEnabled()) { + log.debug ("Invalidating session [" + sessionID + "] for token [" + token + "]"); + } + try { + session.invalidate(); + } catch (final IllegalStateException e) { + log.debug("Error invalidating session.", e); + } + } + } + } + + private boolean isMultipartRequest(final HttpServletRequest request) { + return request.getContentType() != null && request.getContentType().toLowerCase().startsWith("multipart"); + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/session/SingleSignOutHttpSessionListener.java b/modules/cas/src/main/java/org/jasig/cas/client/session/SingleSignOutHttpSessionListener.java new file mode 100644 index 00000000..4536529c --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/session/SingleSignOutHttpSessionListener.java @@ -0,0 +1,61 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.session; + +import jakarta.servlet.http.HttpSession; +import jakarta.servlet.http.HttpSessionEvent; +import jakarta.servlet.http.HttpSessionListener; + +/** + * Listener to detect when an HTTP session is destroyed and remove it from the map of + * managed sessions. Also allows for the programmatic removal of sessions. + *

+ * Enables the CAS Single Sign out feature. + * + * Scott Battaglia + * @version $Revision$ Date$ + * @since 3.1 + */ +public final class SingleSignOutHttpSessionListener implements HttpSessionListener { + + private SessionMappingStorage sessionMappingStorage; + + public void sessionCreated(final HttpSessionEvent event) { + // nothing to do at the moment + } + + public void sessionDestroyed(final HttpSessionEvent event) { + if (sessionMappingStorage == null) { + sessionMappingStorage = getSessionMappingStorage(); + } + final HttpSession session = event.getSession(); + sessionMappingStorage.removeBySessionById(session.getId()); + } + + /** + * Obtains a {@link SessionMappingStorage} object. Assumes this method will always return the same + * instance of the object. It assumes this because it generally lazily calls the method. + * + * @return the SessionMappingStorage + */ + protected static SessionMappingStorage getSessionMappingStorage() { + return SingleSignOutFilter.getSingleSignOutHandler().getSessionMappingStorage(); + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/ssl/AnyHostnameVerifier.java b/modules/cas/src/main/java/org/jasig/cas/client/ssl/AnyHostnameVerifier.java new file mode 100644 index 00000000..14fe4ec9 --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/ssl/AnyHostnameVerifier.java @@ -0,0 +1,40 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.ssl; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSession; + +/** + * Hostname verifier that performs no host name verification for an SSL peer + * such that all hosts are allowed. + * + * @author Marvin Addison + * @version $Revision$ $Date$ + * @since 3.1.10 + */ +public final class AnyHostnameVerifier implements HostnameVerifier { + + /** {@inheritDoc} */ + public boolean verify(final String hostname, final SSLSession session) { + return true; + } + +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/ssl/RegexHostnameVerifier.java b/modules/cas/src/main/java/org/jasig/cas/client/ssl/RegexHostnameVerifier.java new file mode 100644 index 00000000..6bb70740 --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/ssl/RegexHostnameVerifier.java @@ -0,0 +1,55 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.jasig.cas.client.ssl; + +import java.util.regex.Pattern; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSession; + +/** + * Validates an SSL peer's hostname using a regular expression that a candidate + * host must match in order to be verified. + * + * @author Marvin Addison + * @version $Revision$ $Date$ + * @since 3.1.10 + * + */ +public final class RegexHostnameVerifier implements HostnameVerifier { + + /** Allowed hostname pattern */ + private Pattern pattern; + + + /** + * Creates a new instance using the given regular expression. + * + * @param regex Regular expression describing allowed hosts. + */ + public RegexHostnameVerifier(final String regex) { + this.pattern = Pattern.compile(regex); + } + + + /** {@inheritDoc} */ + public boolean verify(final String hostname, final SSLSession session) { + return pattern.matcher(hostname).matches(); + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/ssl/WhitelistHostnameVerifier.java b/modules/cas/src/main/java/org/jasig/cas/client/ssl/WhitelistHostnameVerifier.java new file mode 100644 index 00000000..c09002a7 --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/ssl/WhitelistHostnameVerifier.java @@ -0,0 +1,69 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.ssl; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSession; + +/** + * Verifies a SSL peer host name based on an explicit whitelist of allowed hosts. + * + * @author Marvin Addison + * @version $Revision$ $Date$ + * @since 3.1.10 + * + */ +public final class WhitelistHostnameVerifier implements HostnameVerifier { + + /** Allowed hosts */ + private String[] allowedHosts; + + + /** + * Creates a new instance using the given array of allowed hosts. + * + * @param allowed Array of allowed hosts. + */ + public WhitelistHostnameVerifier(final String[] allowed) { + this.allowedHosts = allowed; + } + + + /** + * Creates a new instance using the given list of allowed hosts. + * + * @param allowedList Comma-separated list of allowed hosts. + */ + public WhitelistHostnameVerifier(final String allowedList) { + this.allowedHosts = allowedList.split(",\\s*"); + } + + /** {@inheritDoc} */ + public boolean verify(final String hostname, final SSLSession session) { + + for (final String allowedHost : this.allowedHosts) { + if (hostname.equalsIgnoreCase(allowedHost)) { + return true; + } + } + return false; + } + +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/util/AbstractCasFilter.java b/modules/cas/src/main/java/org/jasig/cas/client/util/AbstractCasFilter.java new file mode 100644 index 00000000..ea1ebc1a --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/util/AbstractCasFilter.java @@ -0,0 +1,154 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * Abstract filter that contains code that is common to all CAS filters. + *

+ * The following filter options can be configured (either at the context-level or filter-level). + *

    + *
  • serverName - the name of the CAS client server, in the format: localhost:8080 or localhost:8443 or localhost or https://localhost:8443
  • + *
  • service - the completely qualified service url, i.e. https://localhost/cas-client/app
  • + *
+ *

Please note that one of the two above parameters must be set.

+ * + * @author Scott Battaglia + * @version $Revision$ $Date$ + * @since 3.1 + */ +public abstract class AbstractCasFilter extends AbstractConfigurationFilter { + + /** Represents the constant for where the assertion will be located in memory. */ + public static final String CONST_CAS_ASSERTION = "_const_cas_assertion_"; + + /** Instance of commons logging for logging purposes. */ + protected final Log log = LogFactory.getLog(getClass()); + + /** Defines the parameter to look for for the artifact. */ + private String artifactParameterName = "ticket"; + + /** Defines the parameter to look for for the service. */ + private String serviceParameterName = "service"; + + /** Sets where response.encodeUrl should be called on service urls when constructed. */ + private boolean encodeServiceUrl = true; + + /** + * The name of the server. Should be in the following format: {protocol}:{hostName}:{port}. + * Standard ports can be excluded. */ + private String serverName; + + /** The exact url of the service. */ + private String service; + + public final void init(final FilterConfig filterConfig) throws ServletException { + if (!isIgnoreInitConfiguration()) { + setServerName(getPropertyFromInitParams(filterConfig, "serverName", null)); + log.trace("Loading serverName property: " + this.serverName); + setService(getPropertyFromInitParams(filterConfig, "service", null)); + log.trace("Loading service property: " + this.service); + setArtifactParameterName(getPropertyFromInitParams(filterConfig, "artifactParameterName", "ticket")); + log.trace("Loading artifact parameter name property: " + this.artifactParameterName); + setServiceParameterName(getPropertyFromInitParams(filterConfig, "serviceParameterName", "service")); + log.trace("Loading serviceParameterName property: " + this.serviceParameterName); + setEncodeServiceUrl(parseBoolean(getPropertyFromInitParams(filterConfig, "encodeServiceUrl", "true"))); + log.trace("Loading encodeServiceUrl property: " + this.encodeServiceUrl); + + initInternal(filterConfig); + } + init(); + } + + /** Controls the ordering of filter initialization and checking by defining a method that runs before the init. + * @param filterConfig the original filter configuration. + * @throws ServletException if there is a problem. + * + */ + protected void initInternal(final FilterConfig filterConfig) throws ServletException { + // template method + } + + /** + * Initialization method. Called by Filter's init method or by Spring. Similar in concept to the InitializingBean interface's + * afterPropertiesSet(); + */ + public void init() { + CommonUtils.assertNotNull(this.artifactParameterName, "artifactParameterName cannot be null."); + CommonUtils.assertNotNull(this.serviceParameterName, "serviceParameterName cannot be null."); + CommonUtils.assertTrue(CommonUtils.isNotEmpty(this.serverName) || CommonUtils.isNotEmpty(this.service), "serverName or service must be set."); + CommonUtils.assertTrue(CommonUtils.isBlank(this.serverName) || CommonUtils.isBlank(this.service), "serverName and service cannot both be set. You MUST ONLY set one."); + } + + // empty implementation as most filters won't need this. + public void destroy() { + // nothing to do + } + + protected final String constructServiceUrl(final HttpServletRequest request, final HttpServletResponse response) { + return CommonUtils.constructServiceUrl(request, response, this.service, this.serverName, this.artifactParameterName, this.encodeServiceUrl); + } + + /** + * Note that trailing slashes should not be used in the serverName. As a convenience for this common misconfiguration, we strip them from the provided + * value. + * + * @param serverName the serverName. If this method is called, this should not be null. This AND service should not be both configured. + */ + public final void setServerName(final String serverName) { + if (serverName != null && serverName.endsWith("/")) { + this.serverName = serverName.substring(0, serverName.length()-1); + log.info(String.format("Eliminated extra slash from serverName [%s]. It is now [%s]", serverName, this.serverName)); + } else { + this.serverName = serverName; + } + } + + public final void setService(final String service) { + this.service = service; + } + + public final void setArtifactParameterName(final String artifactParameterName) { + this.artifactParameterName = artifactParameterName; + } + + public final void setServiceParameterName(final String serviceParameterName) { + this.serviceParameterName = serviceParameterName; + } + + public final void setEncodeServiceUrl(final boolean encodeServiceUrl) { + this.encodeServiceUrl = encodeServiceUrl; + } + + public final String getArtifactParameterName() { + return this.artifactParameterName; + } + + public final String getServiceParameterName() { + return this.serviceParameterName; + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/util/AbstractConfigurationFilter.java b/modules/cas/src/main/java/org/jasig/cas/client/util/AbstractConfigurationFilter.java new file mode 100644 index 00000000..d3ceefe0 --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/util/AbstractConfigurationFilter.java @@ -0,0 +1,128 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.util; + +import javax.naming.InitialContext; +import javax.naming.NamingException; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterConfig; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Abstracts out the ability to configure the filters from the initial properties provided. + * + * @author Scott Battaglia + * @version $Revision$ $Date$ + * @since 3.1 + */ +public abstract class AbstractConfigurationFilter implements Filter { + + protected final Log log = LogFactory.getLog(getClass()); + + private boolean ignoreInitConfiguration = false; + + /** + * Retrieves the property from the FilterConfig. First it checks the FilterConfig's initParameters to see if it + * has a value. + * If it does, it returns that, otherwise it retrieves the ServletContext's initParameters and returns that value if any. + *

+ * Finally, it will check JNDI if all other methods fail. All the JNDI properties should be stored under either java:comp/env/cas/SHORTFILTERNAME/{propertyName} + * or java:comp/env/cas/{propertyName} + *

+ * Essentially the documented order is: + *

    + *
  1. FilterConfig.getInitParameter
  2. + *
  3. ServletContext.getInitParameter
  4. + *
  5. java:comp/env/cas/SHORTFILTERNAME/{propertyName}
  6. + *
  7. java:comp/env/cas/{propertyName}
  8. + *
  9. Default Value
  10. + *
+ * + * + * @param filterConfig the Filter Configuration. + * @param propertyName the property to retrieve. + * @param defaultValue the default value if the property is not found. + * @return the property value, following the above conventions. It will always return the more specific value (i.e. + * filter vs. context). + */ + protected final String getPropertyFromInitParams(final FilterConfig filterConfig, final String propertyName, final String defaultValue) { + final String value = filterConfig.getInitParameter(propertyName); + + if (CommonUtils.isNotBlank(value)) { + log.info("Property [" + propertyName + "] loaded from FilterConfig.getInitParameter with value [" + value + "]"); + return value; + } + + final String value2 = filterConfig.getServletContext().getInitParameter(propertyName); + + if (CommonUtils.isNotBlank(value2)) { + log.info("Property [" + propertyName + "] loaded from ServletContext.getInitParameter with value [" + value2 + "]"); + return value2; + } + InitialContext context; + try { + context = new InitialContext(); + } catch (final NamingException e) { + log.warn(e,e); + return defaultValue; + } + + + final String shortName = this.getClass().getName().substring(this.getClass().getName().lastIndexOf(".")+1); + final String value3 = loadFromContext(context, "java:comp/env/cas/" + shortName + "/" + propertyName); + + if (CommonUtils.isNotBlank(value3)) { + log.info("Property [" + propertyName + "] loaded from JNDI Filter Specific Property with value [" + value3 + "]"); + return value3; + } + + final String value4 = loadFromContext(context, "java:comp/env/cas/" + propertyName); + + if (CommonUtils.isNotBlank(value4)) { + log.info("Property [" + propertyName + "] loaded from JNDI with value [" + value4 + "]"); + return value4; + } + + log.info("Property [" + propertyName + "] not found. Using default value [" + defaultValue + "]"); + return defaultValue; + } + + protected final boolean parseBoolean(final String value) { + return ((value != null) && value.equalsIgnoreCase("true")); + } + + protected final String loadFromContext(final InitialContext context, final String path) { + try { + return (String) context.lookup(path); + } catch (final NamingException e) { + return null; + } + } + + public final void setIgnoreInitConfiguration(boolean ignoreInitConfiguration) { + this.ignoreInitConfiguration = ignoreInitConfiguration; + } + + protected final boolean isIgnoreInitConfiguration() { + return this.ignoreInitConfiguration; + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/util/AssertionHolder.java b/modules/cas/src/main/java/org/jasig/cas/client/util/AssertionHolder.java new file mode 100644 index 00000000..7959ca0e --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/util/AssertionHolder.java @@ -0,0 +1,63 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.util; + +import org.jasig.cas.client.validation.Assertion; + +/** + * Static holder that places Assertion in a ThreadLocal. + * + * @author Scott Battaglia + * @version $Revision: 11728 $ $Date: 2007-09-26 14:20:43 -0400 (Tue, 26 Sep 2007) $ + * @since 3.0 + */ +public class AssertionHolder { + + /** + * ThreadLocal to hold the Assertion for Threads to access. + */ + private static final ThreadLocal threadLocal = new ThreadLocal(); + + + /** + * Retrieve the assertion from the ThreadLocal. + * + * @return the Asssertion associated with this thread. + */ + public static Assertion getAssertion() { + return threadLocal.get(); + } + + /** + * Add the Assertion to the ThreadLocal. + * + * @param assertion the assertion to add. + */ + public static void setAssertion(final Assertion assertion) { + threadLocal.set(assertion); + } + + /** + * Clear the ThreadLocal. + */ + public static void clear() { + threadLocal.set(null); + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/util/AssertionThreadLocalFilter.java b/modules/cas/src/main/java/org/jasig/cas/client/util/AssertionThreadLocalFilter.java new file mode 100644 index 00000000..b0de8741 --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/util/AssertionThreadLocalFilter.java @@ -0,0 +1,63 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.util; + +import org.jasig.cas.client.validation.Assertion; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; +import java.io.IOException; + +/** + * Places the assertion in a ThreadLocal such that other resources can access it that do not have access to the web tier session. + * + * @author Scott Battaglia + * @version $Revision: 11728 $ $Date: 2007-09-26 14:20:43 -0400 (Tue, 26 Sep 2007) $ + * @since 3.0 + */ +public final class AssertionThreadLocalFilter implements Filter { + + public void init(final FilterConfig filterConfig) throws ServletException { + // nothing to do here + } + + public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException { + final HttpServletRequest request = (HttpServletRequest) servletRequest; + final HttpSession session = request.getSession(false); + final Assertion assertion = (Assertion) (session == null ? request.getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION) : session.getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION)); + + try { + AssertionHolder.setAssertion(assertion); + filterChain.doFilter(servletRequest, servletResponse); + } finally { + AssertionHolder.clear(); + } + } + + public void destroy() { + // nothing to do + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/util/CommonUtils.java b/modules/cas/src/main/java/org/jasig/cas/client/util/CommonUtils.java new file mode 100644 index 00000000..d74b211b --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/util/CommonUtils.java @@ -0,0 +1,388 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.jasig.cas.client.proxy.ProxyGrantingTicketStorage; +import org.jasig.cas.client.validation.ProxyList; +import org.jasig.cas.client.validation.ProxyListEditor; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.URLConnection; +import java.net.URLEncoder; +import java.net.URL; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Collection; +import java.util.Date; +import java.util.TimeZone; + +/** + * Common utilities so that we don't need to include Commons Lang. + * + * @author Scott Battaglia + * @version $Revision: 11729 $ $Date: 2007-09-26 14:22:30 -0400 (Tue, 26 Sep 2007) $ + * @since 3.0 + */ +public final class CommonUtils { + + /** Instance of Commons Logging. */ + private static final Log LOG = LogFactory.getLog(CommonUtils.class); + + /** + * Constant representing the ProxyGrantingTicket IOU Request Parameter. + */ + private static final String PARAM_PROXY_GRANTING_TICKET_IOU = "pgtIou"; + + /** + * Constant representing the ProxyGrantingTicket Request Parameter. + */ + private static final String PARAM_PROXY_GRANTING_TICKET = "pgtId"; + + private CommonUtils() { + // nothing to do + } + + public static String formatForUtcTime(final Date date) { + final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + return dateFormat.format(date); + } + + /** + * Check whether the object is null or not. If it is, throw an exception and + * display the message. + * + * @param object the object to check. + * @param message the message to display if the object is null. + */ + public static void assertNotNull(final Object object, final String message) { + if (object == null) { + throw new IllegalArgumentException(message); + } + } + + /** + * Check whether the collection is null or empty. If it is, throw an + * exception and display the message. + * + * @param c the collecion to check. + * @param message the message to display if the object is null. + */ + public static void assertNotEmpty(final Collection c, final String message) { + assertNotNull(c, message); + if (c.isEmpty()) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that the statement is true, otherwise throw an exception with the + * provided message. + * + * @param cond the codition to assert is true. + * @param message the message to display if the condition is not true. + */ + public static void assertTrue(final boolean cond, final String message) { + if (!cond) { + throw new IllegalArgumentException(message); + } + } + + /** + * Determines whether the String is null or of length 0. + * + * @param string the string to check + * @return true if its null or length of 0, false otherwise. + */ + public static boolean isEmpty(final String string) { + return string == null || string.length() == 0; + } + + /** + * Determines if the String is not empty. A string is not empty if it is not + * null and has a length > 0. + * + * @param string the string to check + * @return true if it is not empty, false otherwise. + */ + public static boolean isNotEmpty(final String string) { + return !isEmpty(string); + } + + /** + * Determines if a String is blank or not. A String is blank if its empty or + * if it only contains spaces. + * + * @param string the string to check + * @return true if its blank, false otherwise. + */ + public static boolean isBlank(final String string) { + return isEmpty(string) || string.trim().length() == 0; + } + + /** + * Determines if a string is not blank. A string is not blank if it contains + * at least one non-whitespace character. + * + * @param string the string to check. + * @return true if its not blank, false otherwise. + */ + public static boolean isNotBlank(final String string) { + return !isBlank(string); + } + + /** + * Constructs the URL to use to redirect to the CAS server. + * + * @param casServerLoginUrl the CAS Server login url. + * @param serviceParameterName the name of the parameter that defines the service. + * @param serviceUrl the actual service's url. + * @param renew whether we should send renew or not. + * @param gateway where we should send gateway or not. + * @return the fully constructed redirect url. + */ + public static String constructRedirectUrl(final String casServerLoginUrl, final String serviceParameterName, final String serviceUrl, final boolean renew, final boolean gateway) { + try { + return casServerLoginUrl + (casServerLoginUrl.indexOf("?") != -1 ? "&" : "?") + serviceParameterName + "=" + + URLEncoder.encode(serviceUrl, "UTF-8") + + (renew ? "&renew=true" : "") + + (gateway ? "&gateway=true" : ""); + } catch (final UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + public static void readAndRespondToProxyReceptorRequest(final HttpServletRequest request, final HttpServletResponse response, final ProxyGrantingTicketStorage proxyGrantingTicketStorage) throws IOException { + final String proxyGrantingTicketIou = request.getParameter(PARAM_PROXY_GRANTING_TICKET_IOU); + + final String proxyGrantingTicket = request.getParameter(PARAM_PROXY_GRANTING_TICKET); + + if (CommonUtils.isBlank(proxyGrantingTicket) || CommonUtils.isBlank(proxyGrantingTicketIou)) { + response.getWriter().write(""); + return; + } + + if (LOG.isDebugEnabled()) { + LOG.debug("Received proxyGrantingTicketId [" + + proxyGrantingTicket + "] for proxyGrantingTicketIou [" + + proxyGrantingTicketIou + "]"); + } + + proxyGrantingTicketStorage.save(proxyGrantingTicketIou, proxyGrantingTicket); + + if (LOG.isDebugEnabled()) { + LOG.debug("Successfully saved proxyGrantingTicketId [" + + proxyGrantingTicket + "] for proxyGrantingTicketIou [" + + proxyGrantingTicketIou + "]"); + } + + response.getWriter().write(""); + response.getWriter().write(""); + } + +/** + * Constructs a service url from the HttpServletRequest or from the given + * serviceUrl. Prefers the serviceUrl provided if both a serviceUrl and a + * serviceName. + * + * @param request the HttpServletRequest + * @param response the HttpServletResponse + * @param service the configured service url (this will be used if not null) + * @param serverName the server name to use to constuct the service url if the service param is empty + * @param artifactParameterName the artifact parameter name to remove (i.e. ticket) + * @param encode whether to encode the url or not (i.e. Jsession). + * @return the service url to use. + */ + public static String constructServiceUrl(final HttpServletRequest request, + final HttpServletResponse response, final String service, final String serverName, final String artifactParameterName, final boolean encode) { + if (CommonUtils.isNotBlank(service)) { + return encode ? response.encodeURL(service) : service; + } + + final StringBuilder buffer = new StringBuilder(); + + + if (!serverName.startsWith("https://") && !serverName.startsWith("http://")) { + buffer.append(request.isSecure() ? "https://" : "http://"); + } + + buffer.append(serverName); + buffer.append(request.getRequestURI()); + + if (CommonUtils.isNotBlank(request.getQueryString())) { + final int location = request.getQueryString().indexOf(artifactParameterName + "="); + + if (location == 0) { + final String returnValue = encode ? response.encodeURL(buffer.toString()): buffer.toString(); + if (LOG.isDebugEnabled()) { + LOG.debug("serviceUrl generated: " + returnValue); + } + return returnValue; + } + + buffer.append("?"); + + if (location == -1) { + buffer.append(request.getQueryString()); + } else if (location > 0) { + final int actualLocation = request.getQueryString() + .indexOf("&" + artifactParameterName + "="); + + if (actualLocation == -1) { + buffer.append(request.getQueryString()); + } else if (actualLocation > 0) { + buffer.append(request.getQueryString().substring(0, + actualLocation)); + } + } + } + + final String returnValue = encode ? response.encodeURL(buffer.toString()) : buffer.toString(); + if (LOG.isDebugEnabled()) { + LOG.debug("serviceUrl generated: " + returnValue); + } + return returnValue; + } + + /** + * Safe method for retrieving a parameter from the request without disrupting the reader UNLESS the parameter + * actually exists in the query string. + *

+ * Note, this does not work for POST Requests for "logoutRequest". It works for all other CAS POST requests because the + * parameter is ALWAYS in the GET request. + *

+ * If we see the "logoutRequest" parameter we MUST treat it as if calling the standard request.getParameter. + * + * @param request the request to check. + * @param parameter the parameter to look for. + * @return the value of the parameter. + */ + public static String safeGetParameter(final HttpServletRequest request, final String parameter) { + if ("POST".equals(request.getMethod()) && "logoutRequest".equals(parameter)) { + LOG.debug("safeGetParameter called on a POST HttpServletRequest for LogoutRequest. Cannot complete check safely. Reverting to standard behavior for this Parameter"); + return request.getParameter(parameter); + } + return request.getQueryString() == null || request.getQueryString().indexOf(parameter) == -1 ? null : request.getParameter(parameter); + } + + /** + * Contacts the remote URL and returns the response. + * + * @param constructedUrl the url to contact. + * @param encoding the encoding to use. + * @return the response. + */ + public static String getResponseFromServer(final URL constructedUrl, final String encoding) { + return getResponseFromServer(constructedUrl, HttpsURLConnection.getDefaultHostnameVerifier(), encoding); + } + + /** + * Contacts the remote URL and returns the response. + * + * @param constructedUrl the url to contact. + * @param hostnameVerifier Host name verifier to use for HTTPS connections. + * @param encoding the encoding to use. + * @return the response. + */ + public static String getResponseFromServer(final URL constructedUrl, final HostnameVerifier hostnameVerifier, final String encoding) { + URLConnection conn = null; + try { + conn = constructedUrl.openConnection(); + if (conn instanceof HttpsURLConnection) { + ((HttpsURLConnection)conn).setHostnameVerifier(hostnameVerifier); + } + final BufferedReader in; + + if (CommonUtils.isEmpty(encoding)) { + in = new BufferedReader(new InputStreamReader(conn.getInputStream())); + } else { + in = new BufferedReader(new InputStreamReader(conn.getInputStream(), encoding)); + } + + String line; + final StringBuilder stringBuffer = new StringBuilder(255); + + while ((line = in.readLine()) != null) { + stringBuffer.append(line); + stringBuffer.append("\n"); + } + return stringBuffer.toString(); + } catch (final Exception e) { + LOG.error(e.getMessage(), e); + throw new RuntimeException(e); + } finally { + if (conn != null && conn instanceof HttpURLConnection) { + ((HttpURLConnection)conn).disconnect(); + } + } + + } + /** + * Contacts the remote URL and returns the response. + * + * @param url the url to contact. + * @param encoding the encoding to use. + * @return the response. + */ + public static String getResponseFromServer(final String url, String encoding) { + try { + return getResponseFromServer(new URL(url), encoding); + } catch (final MalformedURLException e) { + throw new IllegalArgumentException(e); + } + } + + public static ProxyList createProxyList(final String proxies) { + if (CommonUtils.isBlank(proxies)) { + return new ProxyList(); + } + + final ProxyListEditor editor = new ProxyListEditor(); + editor.setAsText(proxies); + return (ProxyList) editor.getValue(); + } + + /** + * Sends the redirect message and captures the exceptions that we can't possibly do anything with. + * + * @param response the HttpServletResponse. CANNOT be NULL. + * @param url the url to redirect to. + */ + public static void sendRedirect(final HttpServletResponse response, final String url) { + try { + response.sendRedirect(url); + } catch (final Exception e) { + LOG.warn(e.getMessage(), e); + } + + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/util/DelegatingFilter.java b/modules/cas/src/main/java/org/jasig/cas/client/util/DelegatingFilter.java new file mode 100644 index 00000000..da123ea6 --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/util/DelegatingFilter.java @@ -0,0 +1,122 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.Map; + +/** + * A Delegating Filter looks up a parameter in the request object and matches + * (either exact or using Regular Expressions) the value. If there is a match, + * the associated filter is executed. Otherwise, the normal chain is executed. + * + * @author Scott Battaglia + * @version $Revision: 11729 $ $Date: 2006-09-26 14:22:30 -0400 (Tue, 26 Sep 2006) $ + * @since 3.0 + */ +public final class DelegatingFilter implements Filter { + + /** + * Instance of Commons Logging. + */ + private final Log log = LogFactory.getLog(this.getClass()); + + /** + * The request parameter to look for in the Request object. + */ + private final String requestParameterName; + + /** + * The map of filters to delegate to and the criteria (as key). + */ + private final Map delegators; + + /** + * The default filter to use if there is no match. + */ + private final Filter defaultFilter; + + /** + * Whether the key in the delegators map is an exact match or a regular + * expression. + */ + private final boolean exactMatch; + + public DelegatingFilter(final String requestParameterName, final Map delegators, final boolean exactMatch) { + this(requestParameterName, delegators, exactMatch, null); + } + + public DelegatingFilter(final String requestParameterName, final Map delegators, final boolean exactMatch, final Filter defaultFilter) { + CommonUtils.assertNotNull(requestParameterName, "requestParameterName cannot be null."); + CommonUtils.assertTrue(!delegators.isEmpty(), "delegators cannot be empty."); + + this.requestParameterName = requestParameterName; + this.delegators = delegators; + this.defaultFilter = defaultFilter; + this.exactMatch = exactMatch; + } + + public void destroy() { + // nothing to do here + } + + public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain filterChain) throws IOException, ServletException { + + final String parameter = CommonUtils.safeGetParameter((HttpServletRequest) request, this.requestParameterName); + + if (CommonUtils.isNotEmpty(parameter)) { + for (final String key : this.delegators.keySet()) { + if ((parameter.equals(key) && this.exactMatch) || (parameter.matches(key) && !this.exactMatch)) { + final Filter filter = this.delegators.get(key); + if (log.isDebugEnabled()) { + log.debug("Match found for parameter [" + + this.requestParameterName + "] with value [" + + parameter + "]. Delegating to filter [" + + filter.getClass().getName() + "]"); + } + filter.doFilter(request, response, filterChain); + return; + } + } + } + + log.debug("No match found for parameter [" + this.requestParameterName + "] with value [" + parameter + "]"); + + if (this.defaultFilter != null) { + this.defaultFilter.doFilter(request, response, filterChain); + } else { + filterChain.doFilter(request, response); + } + } + + public void init(final FilterConfig filterConfig) throws ServletException { + // nothing to do here. + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/util/ErrorRedirectFilter.java b/modules/cas/src/main/java/org/jasig/cas/client/util/ErrorRedirectFilter.java new file mode 100644 index 00000000..715552e7 --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/util/ErrorRedirectFilter.java @@ -0,0 +1,128 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.util; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Filters that redirects to the supplied url based on an exception. Exceptions and the urls are configured via + * init filter name/param values. + *

+ * If there is an exact match the filter uses that value. If there's a non-exact match (i.e. inheritance), then the filter + * uses the last value that matched. + *

+ * If there is no match it will redirect to a default error page. The default exception is configured via the "defaultErrorRedirectPage" property. + * + * @author Scott Battaglia + * @version $Revision$ $Date$ + * @since 3.1.4 + * + */ +public final class ErrorRedirectFilter implements Filter { + + private final Log log = LogFactory.getLog(getClass()); + + private final List errors = new ArrayList(); + + private String defaultErrorRedirectPage; + + public void destroy() { + // nothing to do here + } + + public void doFilter(final ServletRequest request, final ServletResponse response, + final FilterChain filterChain) throws IOException, ServletException { + final HttpServletResponse httpResponse = (HttpServletResponse) response; + try { + filterChain.doFilter(request, response); + } catch (final ServletException e) { + final Throwable t = e.getCause(); + ErrorHolder currentMatch = null; + for (final ErrorHolder errorHolder : this.errors) { + if (errorHolder.exactMatch(t)) { + currentMatch = errorHolder; + break; + } else if (errorHolder.inheritanceMatch(t)) { + currentMatch = errorHolder; + } + } + + if (currentMatch != null) { + httpResponse.sendRedirect(currentMatch.getUrl()); + } else { + httpResponse.sendRedirect(defaultErrorRedirectPage); + } + } + } + + public void init(final FilterConfig filterConfig) throws ServletException { + this.defaultErrorRedirectPage = filterConfig.getInitParameter("defaultErrorRedirectPage"); + + final Enumeration enumeration = filterConfig.getInitParameterNames(); + while (enumeration.hasMoreElements()) { + final String className = (String) enumeration.nextElement(); + try { + if (!className.equals("defaultErrorRedirectPage")) { + this.errors.add(new ErrorHolder(className, filterConfig.getInitParameter(className))); + } + } catch (final ClassNotFoundException e) { + log.warn("Class [" + className + "] cannot be found in ClassLoader. Ignoring."); + } + } + } + + protected final class ErrorHolder { + + private Class className; + + private String url; + + protected ErrorHolder(final String className, final String url) throws ClassNotFoundException { + this.className = Class.forName(className); + this.url = url; + } + + public boolean exactMatch(final Throwable e) { + return this.className.equals(e.getClass()); + } + + public boolean inheritanceMatch(final Throwable e) { + return className.isAssignableFrom(e.getClass()); + } + + public String getUrl() { + return this.url; + } + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/util/HttpServletRequestWrapperFilter.java b/modules/cas/src/main/java/org/jasig/cas/client/util/HttpServletRequestWrapperFilter.java new file mode 100644 index 00000000..6a9b4c87 --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/util/HttpServletRequestWrapperFilter.java @@ -0,0 +1,153 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.util; + +import org.jasig.cas.client.authentication.AttributePrincipal; +import org.jasig.cas.client.validation.Assertion; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import jakarta.servlet.http.HttpSession; +import java.io.IOException; +import java.security.Principal; +import java.util.Collection; + +/** + * Implementation of a filter that wraps the normal HttpServletRequest with a + * wrapper that overrides the following methods to provide data from the + * CAS Assertion: + *

    + *
  • {@link HttpServletRequest#getUserPrincipal()}
  • + *
  • {@link HttpServletRequest#getRemoteUser()}
  • + *
  • {@link HttpServletRequest#isUserInRole(String)}
  • + *
+ *

+ * This filter needs to be configured in the chain so that it executes after + * both the authentication and the validation filters. + * + * @author Scott Battaglia + * @author Marvin S. Addison + * @version $Revision: 11729 $ $Date: 2007-09-26 14:22:30 -0400 (Tue, 26 Sep 2007) $ + * @since 3.0 + */ +public final class HttpServletRequestWrapperFilter extends AbstractConfigurationFilter { + + /** Name of the attribute used to answer role membership queries */ + private String roleAttribute; + + /** Whether or not to ignore case in role membership queries */ + private boolean ignoreCase; + + public void destroy() { + // nothing to do + } + + /** + * Wraps the HttpServletRequest in a wrapper class that delegates + * request.getRemoteUser to the underlying Assertion object + * stored in the user session. + */ + public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException { + final AttributePrincipal principal = retrievePrincipalFromSessionOrRequest(servletRequest); + + filterChain.doFilter(new CasHttpServletRequestWrapper((HttpServletRequest) servletRequest, principal), servletResponse); + } + + protected AttributePrincipal retrievePrincipalFromSessionOrRequest(final ServletRequest servletRequest) { + final HttpServletRequest request = (HttpServletRequest) servletRequest; + final HttpSession session = request.getSession(false); + final Assertion assertion = (Assertion) (session == null ? request.getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION) : session.getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION)); + + return assertion == null ? null : assertion.getPrincipal(); + } + + public void init(final FilterConfig filterConfig) throws ServletException { + this.roleAttribute = getPropertyFromInitParams(filterConfig, "roleAttribute", null); + this.ignoreCase = Boolean.parseBoolean(getPropertyFromInitParams(filterConfig, "ignoreCase", "false")); + } + + final class CasHttpServletRequestWrapper extends HttpServletRequestWrapper { + + private final AttributePrincipal principal; + + CasHttpServletRequestWrapper(final HttpServletRequest request, final AttributePrincipal principal) { + super(request); + this.principal = principal; + } + + public Principal getUserPrincipal() { + return this.principal; + } + + public String getRemoteUser() { + return principal != null ? this.principal.getName() : null; + } + + public boolean isUserInRole(final String role) { + if (CommonUtils.isBlank(role)) { + log.debug("No valid role provided. Returning false."); + return false; + } + + if (this.principal == null) { + log.debug("No Principal in Request. Returning false."); + return false; + } + + if (CommonUtils.isBlank(roleAttribute)) { + log.debug("No Role Attribute Configured. Returning false."); + return false; + } + + final Object value = this.principal.getAttributes().get(roleAttribute); + + if (value instanceof Collection) { + for (final Object o : (Collection) value) { + if (rolesEqual(role, o)) { + log.debug("User [" + getRemoteUser() + "] is in role [" + role + "]: " + true); + return true; + } + } + } + + final boolean isMember = rolesEqual(role, value); + log.debug("User [" + getRemoteUser() + "] is in role [" + role + "]: " + isMember); + return isMember; + } + + /** + * Determines whether the given role is equal to the candidate + * role attribute taking into account case sensitivity. + * + * @param given Role under consideration. + * @param candidate Role that the current user possesses. + * + * @return True if roles are equal, false otherwise. + */ + private boolean rolesEqual(final String given, final Object candidate) { + return ignoreCase ? given.equalsIgnoreCase(candidate.toString()) : given.equals(candidate); + } + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/util/ReflectUtils.java b/modules/cas/src/main/java/org/jasig/cas/client/util/ReflectUtils.java new file mode 100644 index 00000000..0b1f963e --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/util/ReflectUtils.java @@ -0,0 +1,152 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.util; + +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.InvocationTargetException; + +/** + * Helper class with reflection utility methods. + * + * @author Marvin S. Addison + * @version $Revision$ + * @since 3.1.11 + * + */ +public final class ReflectUtils { + + private ReflectUtils() { + // private constructor to prevent instanciation. + } + + /** + * Attempts to create a class from a String. + * @param className the name of the class to create. + * @return the class. CANNOT be NULL. + * @throws IllegalArgumentException if the className does not exist. + */ + @SuppressWarnings("unchecked") + public static Class loadClass(final String className) throws IllegalArgumentException { + try { + return (Class) Class.forName(className); + } catch (final ClassNotFoundException e) { + throw new IllegalArgumentException(className + " class not found."); + } + } + + + /** + * Creates a new instance of the given class by passing the given arguments + * to the constructor. + * @param className Name of class to be created. + * @param args Constructor arguments. + * @return New instance of given class. + */ + public static T newInstance(final String className, final Object ... args) { + return newInstance(ReflectUtils.loadClass(className), args); + } + + /** + * Creates a new instance of the given class by passing the given arguments + * to the constructor. + * @param clazz Class of instance to be created. + * @param args Constructor arguments. + * @return New instance of given class. + */ + public static T newInstance(final Class clazz, final Object ... args) { + final Class[] argClasses = new Class[args.length]; + for (int i = 0; i < args.length; i++) { + argClasses[i] = args[i].getClass(); + } + try { + return clazz.getConstructor(argClasses).newInstance(args); + } catch (final Exception e) { + throw new IllegalArgumentException("Error creating new instance of " + clazz, e); + } + } + + /** + * Gets the property descriptor for the named property on the given class. + * @param clazz Class to which property belongs. + * @param propertyName Name of property. + * @return Property descriptor for given property or null if no property with given + * name exists in given class. + */ + public static PropertyDescriptor getPropertyDescriptor(final Class clazz, final String propertyName) { + try { + return getPropertyDescriptor(Introspector.getBeanInfo(clazz), propertyName); + } catch (final IntrospectionException e) { + throw new RuntimeException("Failed getting bean info for " + clazz, e); + } + } + + /** + * Gets the property descriptor for the named property from the bean info describing + * a particular class to which property belongs. + * @param info Bean info describing class to which property belongs. + * @param propertyName Name of property. + * @return Property descriptor for given property or null if no property with given + * name exists. + */ + public static PropertyDescriptor getPropertyDescriptor(final BeanInfo info, final String propertyName) { + for (int i = 0; i < info.getPropertyDescriptors().length; i++) { + final PropertyDescriptor pd = info.getPropertyDescriptors()[i]; + if (pd.getName().equals(propertyName)) { + return pd; + } + } + return null; + } + + /** + * Sets the given property on the target JavaBean using bean instrospection. + * @param propertyName Property to set. + * @param value Property value to set. + * @param target Target java bean on which to set property. + */ + public static void setProperty(final String propertyName, final Object value, final Object target) { + try { + setProperty(propertyName, value, target, Introspector.getBeanInfo(target.getClass())); + } catch (final IntrospectionException e) { + throw new RuntimeException("Failed getting bean info on target JavaBean " + target, e); + } + } + + /** + * Sets the given property on the target JavaBean using bean instrospection. + * @param propertyName Property to set. + * @param value Property value to set. + * @param target Target JavaBean on which to set property. + * @param info BeanInfo describing the target JavaBean. + */ + public static void setProperty(final String propertyName, final Object value, final Object target, final BeanInfo info) { + try { + final PropertyDescriptor pd = getPropertyDescriptor(info, propertyName); + pd.getWriteMethod().invoke(target, value); + } catch (final InvocationTargetException e) { + throw new RuntimeException("Error setting property " + propertyName, e.getCause()); + } catch (final Exception e) { + throw new RuntimeException("Error setting property " + propertyName, e); + } + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/util/XmlUtils.java b/modules/cas/src/main/java/org/jasig/cas/client/util/XmlUtils.java new file mode 100644 index 00000000..5496001a --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/util/XmlUtils.java @@ -0,0 +1,172 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.DefaultHandler; +import org.xml.sax.helpers.XMLReaderFactory; + +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; + +/** + * Common utilities for easily parsing XML without duplicating logic. + * + * @author Scott Battaglia + * @version $Revision: 11729 $ $Date: 2007-09-26 14:22:30 -0400 (Tue, 26 Sep 2007) $ + * @since 3.0 + */ +public final class XmlUtils { + + /** + * Static instance of Commons Logging. + */ + private final static Log LOG = LogFactory.getLog(XmlUtils.class); + + /** + * Get an instance of an XML reader from the XMLReaderFactory. + * + * @return the XMLReader. + */ + public static XMLReader getXmlReader() { + try { + return XMLReaderFactory.createXMLReader(); + } catch (final SAXException e) { + throw new RuntimeException("Unable to create XMLReader", e); + } + } + + /** + * Retrieve the text for a group of elements. Each text element is an entry + * in a list. + *

This method is currently optimized for the use case of two elements in a list. + * + * @param xmlAsString the xml response + * @param element the element to look for + * @return the list of text from the elements. + */ + public static List getTextForElements(final String xmlAsString, + final String element) { + final List elements = new ArrayList(2); + final XMLReader reader = getXmlReader(); + + final DefaultHandler handler = new DefaultHandler() { + + private boolean foundElement = false; + + private StringBuilder buffer = new StringBuilder(); + + public void startElement(final String uri, final String localName, + final String qName, final Attributes attributes) + throws SAXException { + if (localName.equals(element)) { + this.foundElement = true; + } + } + + public void endElement(final String uri, final String localName, + final String qName) throws SAXException { + if (localName.equals(element)) { + this.foundElement = false; + elements.add(this.buffer.toString()); + this.buffer = new StringBuilder(); + } + } + + public void characters(char[] ch, int start, int length) + throws SAXException { + if (this.foundElement) { + this.buffer.append(ch, start, length); + } + } + }; + + reader.setContentHandler(handler); + reader.setErrorHandler(handler); + + try { + reader.parse(new InputSource(new StringReader(xmlAsString))); + } catch (final Exception e) { + LOG.error(e, e); + return null; + } + + return elements; + } + + /** + * Retrieve the text for a specific element (when we know there is only + * one). + * + * @param xmlAsString the xml response + * @param element the element to look for + * @return the text value of the element. + */ + public static String getTextForElement(final String xmlAsString, + final String element) { + final XMLReader reader = getXmlReader(); + final StringBuilder builder = new StringBuilder(); + + final DefaultHandler handler = new DefaultHandler() { + + private boolean foundElement = false; + + public void startElement(final String uri, final String localName, + final String qName, final Attributes attributes) + throws SAXException { + if (localName.equals(element)) { + this.foundElement = true; + } + } + + public void endElement(final String uri, final String localName, + final String qName) throws SAXException { + if (localName.equals(element)) { + this.foundElement = false; + } + } + + public void characters(char[] ch, int start, int length) + throws SAXException { + if (this.foundElement) { + builder.append(ch, start, length); + } + } + }; + + reader.setContentHandler(handler); + reader.setErrorHandler(handler); + + try { + reader.parse(new InputSource(new StringReader(xmlAsString))); + } catch (final Exception e) { + LOG.error(e, e); + return null; + } + + return builder.toString(); + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/validation/AbstractCasProtocolUrlBasedTicketValidator.java b/modules/cas/src/main/java/org/jasig/cas/client/validation/AbstractCasProtocolUrlBasedTicketValidator.java new file mode 100644 index 00000000..84d9e674 --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/validation/AbstractCasProtocolUrlBasedTicketValidator.java @@ -0,0 +1,53 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.validation; + +import org.jasig.cas.client.util.CommonUtils; + +import java.net.URL; + +/** + * Abstract class that knows the protocol for validating a CAS ticket. + * + * @author Scott Battaglia + * @version $Revision$ $Date$ + * @since 3.1 + */ +public abstract class AbstractCasProtocolUrlBasedTicketValidator extends AbstractUrlBasedTicketValidator { + + protected AbstractCasProtocolUrlBasedTicketValidator(final String casServerUrlPrefix) { + super(casServerUrlPrefix); + } + + protected final void setDisableXmlSchemaValidation(final boolean disable) { + // nothing to do + } + + /** + * Retrieves the response from the server by opening a connection and merely reading the response. + */ + protected final String retrieveResponseFromServer(final URL validationUrl, final String ticket) { + if (this.hostnameVerifier != null) { + return CommonUtils.getResponseFromServer(validationUrl, this.hostnameVerifier, getEncoding()); + } else { + return CommonUtils.getResponseFromServer(validationUrl, getEncoding()); + } + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/validation/AbstractTicketValidationFilter.java b/modules/cas/src/main/java/org/jasig/cas/client/validation/AbstractTicketValidationFilter.java new file mode 100644 index 00000000..815f0d4a --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/validation/AbstractTicketValidationFilter.java @@ -0,0 +1,224 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.validation; + +import org.jasig.cas.client.util.AbstractCasFilter; +import org.jasig.cas.client.util.CommonUtils; +import org.jasig.cas.client.util.ReflectUtils; + +import javax.net.ssl.HostnameVerifier; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * The filter that handles all the work of validating ticket requests. + *

+ * This filter can be configured with the following values: + *

    + *
  • redirectAfterValidation - redirect the CAS client to the same URL without the ticket.
  • + *
  • exceptionOnValidationFailure - throw an exception if the validation fails. Otherwise, continue + * processing.
  • + *
  • useSession - store any of the useful information in a session attribute.
  • + *
+ * + * @author Scott Battaglia + * @version $Revision$ $Date$ + * @since 3.1 + */ +public abstract class AbstractTicketValidationFilter extends AbstractCasFilter { + + /** The TicketValidator we will use to validate tickets. */ + private TicketValidator ticketValidator; + + /** + * Specify whether the filter should redirect the user agent after a + * successful validation to remove the ticket parameter from the query + * string. + */ + private boolean redirectAfterValidation = false; + + /** Determines whether an exception is thrown when there is a ticket validation failure. */ + private boolean exceptionOnValidationFailure = true; + + private boolean useSession = true; + + /** + * Template method to return the appropriate validator. + * + * @param filterConfig the FilterConfiguration that may be needed to construct a validator. + * @return the ticket validator. + */ + protected TicketValidator getTicketValidator(final FilterConfig filterConfig) { + return this.ticketValidator; + } + + /** + * Gets the configured {@link HostnameVerifier} to use for HTTPS connections + * if one is configured for this filter. + * @param filterConfig Servlet filter configuration. + * @return Instance of specified host name verifier or null if none specified. + */ + protected HostnameVerifier getHostnameVerifier(final FilterConfig filterConfig) { + final String className = getPropertyFromInitParams(filterConfig, "hostnameVerifier", null); + log.trace("Using hostnameVerifier parameter: " + className); + final String config = getPropertyFromInitParams(filterConfig, "hostnameVerifierConfig", null); + log.trace("Using hostnameVerifierConfig parameter: " + config); + if (className != null) { + if (config != null) { + return ReflectUtils.newInstance(className, config); + } else { + return ReflectUtils.newInstance(className); + } + } + return null; + } + + protected void initInternal(final FilterConfig filterConfig) throws ServletException { + setExceptionOnValidationFailure(parseBoolean(getPropertyFromInitParams(filterConfig, "exceptionOnValidationFailure", "true"))); + log.trace("Setting exceptionOnValidationFailure parameter: " + this.exceptionOnValidationFailure); + setRedirectAfterValidation(parseBoolean(getPropertyFromInitParams(filterConfig, "redirectAfterValidation", "true"))); + log.trace("Setting redirectAfterValidation parameter: " + this.redirectAfterValidation); + setUseSession(parseBoolean(getPropertyFromInitParams(filterConfig, "useSession", "true"))); + log.trace("Setting useSession parameter: " + this.useSession); + setTicketValidator(getTicketValidator(filterConfig)); + super.initInternal(filterConfig); + } + + public void init() { + super.init(); + CommonUtils.assertNotNull(this.ticketValidator, "ticketValidator cannot be null."); + } + + /** + * Pre-process the request before the normal filter process starts. This could be useful for pre-empting code. + * + * @param servletRequest The servlet request. + * @param servletResponse The servlet response. + * @param filterChain the filter chain. + * @return true if processing should continue, false otherwise. + * @throws IOException if there is an I/O problem + * @throws ServletException if there is a servlet problem. + */ + protected boolean preFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException { + return true; + } + + /** + * Template method that gets executed if ticket validation succeeds. Override if you want additional behavior to occur + * if ticket validation succeeds. This method is called after all ValidationFilter processing required for a successful authentication + * occurs. + * + * @param request the HttpServletRequest. + * @param response the HttpServletResponse. + * @param assertion the successful Assertion from the server. + */ + protected void onSuccessfulValidation(final HttpServletRequest request, final HttpServletResponse response, final Assertion assertion) { + // nothing to do here. + } + + /** + * Template method that gets executed if validation fails. This method is called right after the exception is caught from the ticket validator + * but before any of the processing of the exception occurs. + * + * @param request the HttpServletRequest. + * @param response the HttpServletResponse. + */ + protected void onFailedValidation(final HttpServletRequest request, final HttpServletResponse response) { + // nothing to do here. + } + + public final void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException { + + if (!preFilter(servletRequest, servletResponse, filterChain)) { + return; + } + + final HttpServletRequest request = (HttpServletRequest) servletRequest; + final HttpServletResponse response = (HttpServletResponse) servletResponse; + final String ticket = CommonUtils.safeGetParameter(request, getArtifactParameterName()); + + if (CommonUtils.isNotBlank(ticket)) { + if (log.isDebugEnabled()) { + log.debug("Attempting to validate ticket: " + ticket); + } + + try { + final Assertion assertion = this.ticketValidator.validate(ticket, constructServiceUrl(request, response)); + + if (log.isDebugEnabled()) { + log.debug("Successfully authenticated user: " + assertion.getPrincipal().getName()); + } + + request.setAttribute(CONST_CAS_ASSERTION, assertion); + + if (this.useSession) { + request.getSession().setAttribute(CONST_CAS_ASSERTION, assertion); + } + onSuccessfulValidation(request, response, assertion); + + if (this.redirectAfterValidation) { + log. debug("Redirecting after successful ticket validation."); + response.sendRedirect(constructServiceUrl(request, response)); + return; + } + } catch (final TicketValidationException e) { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + log.warn(e, e); + + onFailedValidation(request, response); + + if (this.exceptionOnValidationFailure) { + throw new ServletException(e); + } + + return; + } + } + + filterChain.doFilter(request, response); + + } + + public final void setTicketValidator(final TicketValidator ticketValidator) { + this.ticketValidator = ticketValidator; +} + + public final void setRedirectAfterValidation(final boolean redirectAfterValidation) { + this.redirectAfterValidation = redirectAfterValidation; + } + + public final void setExceptionOnValidationFailure(final boolean exceptionOnValidationFailure) { + this.exceptionOnValidationFailure = exceptionOnValidationFailure; + } + + public final void setUseSession(final boolean useSession) { + this.useSession = useSession; + } + + + + +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/validation/AbstractUrlBasedTicketValidator.java b/modules/cas/src/main/java/org/jasig/cas/client/validation/AbstractUrlBasedTicketValidator.java new file mode 100644 index 00000000..803d2c21 --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/validation/AbstractUrlBasedTicketValidator.java @@ -0,0 +1,243 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.validation; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.jasig.cas.client.util.CommonUtils; + +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLEncoder; +import java.util.HashMap; +import java.util.Map; + +import javax.net.ssl.HostnameVerifier; + +/** + * Abstract validator implementation for tickets that must be validated against a server. + * + * @author Scott Battaglia + * @version $Revision$ $Date$ + * @since 3.1 + */ +public abstract class AbstractUrlBasedTicketValidator implements TicketValidator { + + /** + * Commons Logging instance. + */ + protected final Log log = LogFactory.getLog(getClass()); + + /** + * Hostname verifier used when making an SSL request to the CAS server. + */ + protected HostnameVerifier hostnameVerifier; + + /** + * Prefix for the CAS server. Should be everything up to the url endpoint, including the /. + * + * i.e. https://cas.rutgers.edu/ + */ + private final String casServerUrlPrefix; + + /** + * Whether the request include a renew or not. + */ + private boolean renew; + + /** + * A map containing custom parameters to pass to the validation url. + */ + private Map customParameters; + + private String encoding; + + /** + * Constructs a new TicketValidator with the casServerUrlPrefix. + * + * @param casServerUrlPrefix the location of the CAS server. + */ + protected AbstractUrlBasedTicketValidator(final String casServerUrlPrefix) { + this.casServerUrlPrefix = casServerUrlPrefix; + CommonUtils.assertNotNull(this.casServerUrlPrefix, "casServerUrlPrefix cannot be null."); + } + + /** + * Template method for ticket validators that need to provide additional parameters to the validation url. + * + * @param urlParameters the map containing the parameters. + */ + protected void populateUrlAttributeMap(final Map urlParameters) { + // nothing to do + } + + /** + * The endpoint of the validation URL. Should be relative (i.e. not start with a "/"). I.e. validate or serviceValidate. + * @return the endpoint of the validation URL. + */ + protected abstract String getUrlSuffix(); + + /** + * Disable XML Schema validation. Note, setting this to true may not be reversable. Defaults to false. Setting it to false + * after setting it to true may not have any affect. + * + * @param disabled whether to disable or not. + */ + protected abstract void setDisableXmlSchemaValidation(boolean disabled); + + /** + * Constructs the URL to send the validation request to. + * + * @param ticket the ticket to be validated. + * @param serviceUrl the service identifier. + * @return the fully constructed URL. + */ + protected final String constructValidationUrl(final String ticket, final String serviceUrl) { + final Map urlParameters = new HashMap(); + + log.debug("Placing URL parameters in map."); + urlParameters.put("ticket", ticket); + urlParameters.put("service", serviceUrl); + + if (this.renew) { + urlParameters.put("renew", "true"); + } + + log.debug("Calling template URL attribute map."); + populateUrlAttributeMap(urlParameters); + + log.debug("Loading custom parameters from configuration."); + if (this.customParameters != null) { + urlParameters.putAll(this.customParameters); + } + + final String suffix = getUrlSuffix(); + final StringBuilder buffer = new StringBuilder(urlParameters.size()*10 + this.casServerUrlPrefix.length() + suffix.length() +1); + + int i = 0; + + buffer.append(this.casServerUrlPrefix); + if (!this.casServerUrlPrefix.endsWith("/")) { + buffer.append("/"); + } + buffer.append(suffix); + + for (Map.Entry entry : urlParameters.entrySet()) { + final String key = entry.getKey(); + final String value = entry.getValue(); + + if (value != null) { + buffer.append(i++ == 0 ? "?" : "&"); + buffer.append(key); + buffer.append("="); + final String encodedValue = encodeUrl(value); + buffer.append(encodedValue); + } + } + + return buffer.toString(); + + } + + /** + * Encodes a URL using the URLEncoder format. + * + * @param url the url to encode. + * @return the encoded url, or the original url if "UTF-8" character encoding could not be found. + */ + protected final String encodeUrl(final String url) { + if (url == null) { + return null; + } + + try { + return URLEncoder.encode(url, "UTF-8"); + } catch (final UnsupportedEncodingException e) { + return url; + } + } + + /** + * Parses the response from the server into a CAS Assertion. + * + * @param response the response from the server, in any format. + * @return the CAS assertion if one could be parsed from the response. + * @throws TicketValidationException if an Assertion could not be created. + * + */ + protected abstract Assertion parseResponseFromServer(final String response) throws TicketValidationException; + + /** + * Contacts the CAS Server to retrieve the response for the ticket validation. + * + * @param validationUrl the url to send the validation request to. + * @param ticket the ticket to validate. + * @return the response from the CAS server. + */ + + protected abstract String retrieveResponseFromServer(URL validationUrl, String ticket); + + public Assertion validate(final String ticket, final String service) throws TicketValidationException { + + + final String validationUrl = constructValidationUrl(ticket, service); + if (log.isDebugEnabled()) { + log.debug("Constructing validation url: " + validationUrl); + } + + try { + log.debug("Retrieving response from server."); + final String serverResponse = retrieveResponseFromServer(new URL(validationUrl), ticket); + + if (serverResponse == null) { + throw new TicketValidationException("The CAS server returned no response."); + } + + if (log.isDebugEnabled()) { + log.debug("Server response: " + serverResponse); + } + + return parseResponseFromServer(serverResponse); + } catch (final MalformedURLException e) { + throw new TicketValidationException(e); + } + } + + public final void setRenew(final boolean renew) { + this.renew = renew; + } + + public final void setCustomParameters(final Map customParameters) { + this.customParameters = customParameters; + } + + public final void setHostnameVerifier(final HostnameVerifier verifier) { + this.hostnameVerifier = verifier; + } + + public final void setEncoding(final String encoding) { + this.encoding = encoding; + } + + protected final String getEncoding() { + return this.encoding; + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/validation/Assertion.java b/modules/cas/src/main/java/org/jasig/cas/client/validation/Assertion.java new file mode 100644 index 00000000..75ac70d4 --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/validation/Assertion.java @@ -0,0 +1,64 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.validation; + +import org.jasig.cas.client.authentication.AttributePrincipal; + +import java.io.Serializable; +import java.util.Date; +import java.util.Map; + +/** + * Represents a response to a validation request. + * + * @author Scott Battaglia + * @version $Revision$ $Date$ + * @since 3.1 + */ +public interface Assertion extends Serializable { + + /** + * The date from which the assertion is valid from. + * + * @return the valid from date. + */ + Date getValidFromDate(); + + /** + * The date which the assertion is valid until. + * + * @return the valid until date. + */ + Date getValidUntilDate(); + + /** + * The key/value pairs associated with this assertion. + * + * @return the map of attributes. + */ + Map getAttributes(); + + /** + * The principal for which this assertion is valid. + * + * @return the principal. + */ + AttributePrincipal getPrincipal(); +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/validation/AssertionImpl.java b/modules/cas/src/main/java/org/jasig/cas/client/validation/AssertionImpl.java new file mode 100644 index 00000000..a845fe87 --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/validation/AssertionImpl.java @@ -0,0 +1,116 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.validation; + +import org.jasig.cas.client.authentication.AttributePrincipal; +import org.jasig.cas.client.authentication.AttributePrincipalImpl; +import org.jasig.cas.client.util.CommonUtils; + +import java.util.Collections; +import java.util.Date; +import java.util.Map; + +/** + * Concrete Implementation of the {@link Assertion}. + * + * @author Scott Battaglia + * @version $Revision$ $Date$ + * @since 3.1 + * + */ +public final class AssertionImpl implements Assertion { + + /** Unique Id for serialization. */ + private static final long serialVersionUID = -7767943925833639221L; + + /** The date from which the assertion is valid. */ + private final Date validFromDate; + + /** The date the assertion is valid until. */ + private final Date validUntilDate; + + /** Map of key/value pairs associated with this assertion. I.e. authentication type. */ + private final Map attributes; + + /** The principal for which this assertion is valid for. */ + private final AttributePrincipal principal; + + /** + * Constructs a new Assertion with a Principal of the supplied name, a valid from date of now, no valid until date, and no attributes. + * + * @param name the name of the principal for which this assertion is valid. + */ + public AssertionImpl(final String name) { + this(new AttributePrincipalImpl(name)); + } + + /** + * Creates a new Assertion with the supplied Principal. + * + * @param principal the Principal to associate with the Assertion. + */ + public AssertionImpl(final AttributePrincipal principal) { + this(principal, Collections.emptyMap()); + } + + /** + * Create a new Assertion with the supplied principal and Assertion attributes. + * + * @param principal the Principal to associate with the Assertion. + * @param attributes the key/value pairs for this attribute. + */ + public AssertionImpl(final AttributePrincipal principal, final Map attributes) { + this(principal, new Date(), null, attributes); + } + + /** + * Creates a new Assertion with the supplied principal, Assertion attributes, and start and valid until dates. + * + * @param principal the Principal to associate with the Assertion. + * @param validFromDate when the assertion is valid from. + * @param validUntilDate when the assertion is valid to. + * @param attributes the key/value pairs for this attribute. + */ + public AssertionImpl(final AttributePrincipal principal, final Date validFromDate, final Date validUntilDate, final Map attributes) { + this.principal = principal; + this.validFromDate = validFromDate; + this.validUntilDate = validUntilDate; + this.attributes = attributes; + + CommonUtils.assertNotNull(this.principal, "principal cannot be null."); + CommonUtils.assertNotNull(this.validFromDate, "validFromDate cannot be null."); + CommonUtils.assertNotNull(this.attributes, "attributes cannot be null."); + } + public Date getValidFromDate() { + return this.validFromDate; + } + + public Date getValidUntilDate() { + return this.validUntilDate; + } + + public Map getAttributes() { + return this.attributes; + } + + public AttributePrincipal getPrincipal() { + return this.principal; + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/validation/Cas10TicketValidationFilter.java b/modules/cas/src/main/java/org/jasig/cas/client/validation/Cas10TicketValidationFilter.java new file mode 100644 index 00000000..400b2c42 --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/validation/Cas10TicketValidationFilter.java @@ -0,0 +1,44 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.validation; + +import jakarta.servlet.FilterConfig; + +/** + * Implementation of AbstractTicketValidatorFilter that instanciates a Cas10TicketValidator. + *

Deployers can provide the "casServerPrefix" and the "renew" attributes via the standard context or filter init + * parameters. + * + * @author Scott Battaglia + * @version $Revision$ $Date$ + * @since 3.1 + */ +public class Cas10TicketValidationFilter extends AbstractTicketValidationFilter { + + protected final TicketValidator getTicketValidator(final FilterConfig filterConfig) { + final String casServerUrlPrefix = getPropertyFromInitParams(filterConfig, "casServerUrlPrefix", null); + final Cas10TicketValidator validator = new Cas10TicketValidator(casServerUrlPrefix); + validator.setRenew(parseBoolean(getPropertyFromInitParams(filterConfig, "renew", "false"))); + validator.setHostnameVerifier(getHostnameVerifier(filterConfig)); + validator.setEncoding(getPropertyFromInitParams(filterConfig, "encoding", null)); + + return validator; + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/validation/Cas10TicketValidator.java b/modules/cas/src/main/java/org/jasig/cas/client/validation/Cas10TicketValidator.java new file mode 100644 index 00000000..7bdfd044 --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/validation/Cas10TicketValidator.java @@ -0,0 +1,58 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.validation; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; + +/** + * Implementation of a Ticket Validator that can validate tickets conforming to the CAS 1.0 specification. + * + * @author Scott Battaglia + * @version $Revision$ $Date$ + * @since 3.1 + */ +public final class Cas10TicketValidator extends AbstractCasProtocolUrlBasedTicketValidator { + + public Cas10TicketValidator(final String casServerUrlPrefix) { + super(casServerUrlPrefix); + } + + protected String getUrlSuffix() { + return "validate"; + } + + protected Assertion parseResponseFromServer(final String response) throws TicketValidationException { + if (!response.startsWith("yes")) { + throw new TicketValidationException("CAS Server could not validate ticket."); + } + + try { + final BufferedReader reader = new BufferedReader(new StringReader(response)); + reader.readLine(); + final String name = reader.readLine(); + + return new AssertionImpl(name); + } catch (final IOException e) { + throw new TicketValidationException("Unable to parse response.", e); + } + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/validation/Cas20ProxyReceivingTicketValidationFilter.java b/modules/cas/src/main/java/org/jasig/cas/client/validation/Cas20ProxyReceivingTicketValidationFilter.java new file mode 100644 index 00000000..94177150 --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/validation/Cas20ProxyReceivingTicketValidationFilter.java @@ -0,0 +1,205 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.validation; + +import java.io.IOException; +import java.util.*; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.jasig.cas.client.proxy.*; +import org.jasig.cas.client.util.CommonUtils; +import org.jasig.cas.client.util.ReflectUtils; + +/** + * Creates either a CAS20ProxyTicketValidator or a CAS20ServiceTicketValidator depending on whether any of the + * proxy parameters are set. + *

+ * This filter can also pass additional parameters to the ticket validator. Any init parameter not included in the + * reserved list {@link org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter#RESERVED_INIT_PARAMS}. + * + * @author Scott Battaglia + * @author Brad Cupit (brad [at] lsu {dot} edu) + * @version $Revision$ $Date$ + * @since 3.1 + * + */ +public class Cas20ProxyReceivingTicketValidationFilter extends AbstractTicketValidationFilter { + + private static final String[] RESERVED_INIT_PARAMS = new String[] {"proxyGrantingTicketStorageClass", "proxyReceptorUrl", "acceptAnyProxy", "allowedProxyChains", "casServerUrlPrefix", "proxyCallbackUrl", "renew", "exceptionOnValidationFailure", "redirectAfterValidation", "useSession", "serverName", "service", "artifactParameterName", "serviceParameterName", "encodeServiceUrl", "millisBetweenCleanUps", "hostnameVerifier", "encoding", "config"}; + + private static final int DEFAULT_MILLIS_BETWEEN_CLEANUPS = 60 * 1000; + + /** + * The URL to send to the CAS server as the URL that will process proxying requests on the CAS client. + */ + private String proxyReceptorUrl; + + private Timer timer; + + private TimerTask timerTask; + + private int millisBetweenCleanUps; + + /** + * Storage location of ProxyGrantingTickets and Proxy Ticket IOUs. + */ + private ProxyGrantingTicketStorage proxyGrantingTicketStorage = new ProxyGrantingTicketStorageImpl(); + + protected void initInternal(final FilterConfig filterConfig) throws ServletException { + setProxyReceptorUrl(getPropertyFromInitParams(filterConfig, "proxyReceptorUrl", null)); + + final String proxyGrantingTicketStorageClass = getPropertyFromInitParams(filterConfig, "proxyGrantingTicketStorageClass", null); + + if (proxyGrantingTicketStorageClass != null) { + this.proxyGrantingTicketStorage = ReflectUtils.newInstance(proxyGrantingTicketStorageClass); + + if (this.proxyGrantingTicketStorage instanceof AbstractEncryptedProxyGrantingTicketStorageImpl) { + final AbstractEncryptedProxyGrantingTicketStorageImpl p = (AbstractEncryptedProxyGrantingTicketStorageImpl) this.proxyGrantingTicketStorage; + final String cipherAlgorithm = getPropertyFromInitParams(filterConfig, "cipherAlgorithm", AbstractEncryptedProxyGrantingTicketStorageImpl.DEFAULT_ENCRYPTION_ALGORITHM); + final String secretKey = getPropertyFromInitParams(filterConfig, "secretKey", null); + + p.setCipherAlgorithm(cipherAlgorithm); + + try { + if (secretKey != null) { + p.setSecretKey(secretKey); + } + } catch (final Exception e) { + throw new RuntimeException(e); + } + } + } + + log.trace("Setting proxyReceptorUrl parameter: " + this.proxyReceptorUrl); + this.millisBetweenCleanUps = Integer.parseInt(getPropertyFromInitParams(filterConfig, "millisBetweenCleanUps", Integer.toString(DEFAULT_MILLIS_BETWEEN_CLEANUPS))); + super.initInternal(filterConfig); + } + + public void init() { + super.init(); + CommonUtils.assertNotNull(this.proxyGrantingTicketStorage, "proxyGrantingTicketStorage cannot be null."); + + if (this.timer == null) { + this.timer = new Timer(true); + } + + if (this.timerTask == null) { + this.timerTask = new CleanUpTimerTask(this.proxyGrantingTicketStorage); + } + this.timer.schedule(this.timerTask, this.millisBetweenCleanUps, this.millisBetweenCleanUps); + } + + /** + * Constructs a Cas20ServiceTicketValidator or a Cas20ProxyTicketValidator based on supplied parameters. + * + * @param filterConfig the Filter Configuration object. + * @return a fully constructed TicketValidator. + */ + protected final TicketValidator getTicketValidator(final FilterConfig filterConfig) { + final String allowAnyProxy = getPropertyFromInitParams(filterConfig, "acceptAnyProxy", null); + final String allowedProxyChains = getPropertyFromInitParams(filterConfig, "allowedProxyChains", null); + final String casServerUrlPrefix = getPropertyFromInitParams(filterConfig, "casServerUrlPrefix", null); + final Cas20ServiceTicketValidator validator; + + if (CommonUtils.isNotBlank(allowAnyProxy) || CommonUtils.isNotBlank(allowedProxyChains)) { + final Cas20ProxyTicketValidator v = new Cas20ProxyTicketValidator(casServerUrlPrefix); + v.setAcceptAnyProxy(parseBoolean(allowAnyProxy)); + v.setAllowedProxyChains(CommonUtils.createProxyList(allowedProxyChains)); + validator = v; + } else { + validator = new Cas20ServiceTicketValidator(casServerUrlPrefix); + } + validator.setProxyCallbackUrl(getPropertyFromInitParams(filterConfig, "proxyCallbackUrl", null)); + validator.setProxyGrantingTicketStorage(this.proxyGrantingTicketStorage); + validator.setProxyRetriever(new Cas20ProxyRetriever(casServerUrlPrefix, getPropertyFromInitParams(filterConfig, "encoding", null))); + validator.setRenew(parseBoolean(getPropertyFromInitParams(filterConfig, "renew", "false"))); + validator.setEncoding(getPropertyFromInitParams(filterConfig, "encoding", null)); + + final Map additionalParameters = new HashMap(); + final List params = Arrays.asList(RESERVED_INIT_PARAMS); + + for (final Enumeration e = filterConfig.getInitParameterNames(); e.hasMoreElements();) { + final String s = (String) e.nextElement(); + + if (!params.contains(s)) { + additionalParameters.put(s, filterConfig.getInitParameter(s)); + } + } + + validator.setCustomParameters(additionalParameters); + validator.setHostnameVerifier(getHostnameVerifier(filterConfig)); + + return validator; + } + + public void destroy() { + super.destroy(); + this.timer.cancel(); + } + + /** + * This processes the ProxyReceptor request before the ticket validation code executes. + */ + protected final boolean preFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException { + final HttpServletRequest request = (HttpServletRequest) servletRequest; + final HttpServletResponse response = (HttpServletResponse) servletResponse; + final String requestUri = request.getRequestURI(); + + if (CommonUtils.isEmpty(this.proxyReceptorUrl) || !requestUri.endsWith(this.proxyReceptorUrl)) { + return true; + } + + try { + CommonUtils.readAndRespondToProxyReceptorRequest(request, response, this.proxyGrantingTicketStorage); + } catch (final RuntimeException e) { + log.error(e.getMessage(), e); + throw e; + } + + return false; + } + + public final void setProxyReceptorUrl(final String proxyReceptorUrl) { + this.proxyReceptorUrl = proxyReceptorUrl; + } + + public void setProxyGrantingTicketStorage(final ProxyGrantingTicketStorage storage) { + this.proxyGrantingTicketStorage = storage; + } + + public void setTimer(final Timer timer) { + this.timer = timer; + } + + public void setTimerTask(final TimerTask timerTask) { + this.timerTask = timerTask; + } + + public void setMillisBetweenCleanUps(final int millisBetweenCleanUps) { + this.millisBetweenCleanUps = millisBetweenCleanUps; + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/validation/Cas20ProxyTicketValidator.java b/modules/cas/src/main/java/org/jasig/cas/client/validation/Cas20ProxyTicketValidator.java new file mode 100644 index 00000000..2b4d8d1d --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/validation/Cas20ProxyTicketValidator.java @@ -0,0 +1,75 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.validation; + +import org.jasig.cas.client.util.XmlUtils; + +import java.util.List; + +/** + * Extension to the traditional Service Ticket validation that will validate service tickets and proxy tickets. + * + * @author Scott Battaglia + * @version $Revision$ $Date$ + * @since 3.1 + */ +public class Cas20ProxyTicketValidator extends Cas20ServiceTicketValidator { + + private boolean acceptAnyProxy; + + /** This should be a list of an array of Strings */ + private ProxyList allowedProxyChains = new ProxyList(); + + public Cas20ProxyTicketValidator(final String casServerUrlPrefix) { + super(casServerUrlPrefix); + } + + public ProxyList getAllowedProxyChains() { + return this.allowedProxyChains; + } + + protected String getUrlSuffix() { + return "proxyValidate"; + } + + protected void customParseResponse(final String response, final Assertion assertion) throws TicketValidationException { + final List proxies = XmlUtils.getTextForElements(response, "proxy"); + final String[] proxiedList = proxies.toArray(new String[proxies.size()]); + + // this means there was nothing in the proxy chain, which is okay + if (proxies.isEmpty() || this.acceptAnyProxy) { + return; + } + + if (this.allowedProxyChains.contains(proxiedList)) { + return; + } + + throw new InvalidProxyChainTicketValidationException("Invalid proxy chain: " + proxies.toString()); + } + + public void setAcceptAnyProxy(final boolean acceptAnyProxy) { + this.acceptAnyProxy = acceptAnyProxy; + } + + public void setAllowedProxyChains(final ProxyList allowedProxyChains) { + this.allowedProxyChains = allowedProxyChains; + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/validation/Cas20ServiceTicketValidator.java b/modules/cas/src/main/java/org/jasig/cas/client/validation/Cas20ServiceTicketValidator.java new file mode 100644 index 00000000..b013485d --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/validation/Cas20ServiceTicketValidator.java @@ -0,0 +1,188 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.validation; + +import org.jasig.cas.client.authentication.AttributePrincipal; +import org.jasig.cas.client.authentication.AttributePrincipalImpl; +import org.jasig.cas.client.proxy.Cas20ProxyRetriever; +import org.jasig.cas.client.proxy.ProxyGrantingTicketStorage; +import org.jasig.cas.client.proxy.ProxyRetriever; +import org.jasig.cas.client.util.CommonUtils; +import org.jasig.cas.client.util.XmlUtils; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Implementation of the TicketValidator that will validate Service Tickets in compliance with the CAS 2. + * + * @author Scott Battaglia + * @version $Revision$ $Date$ + * @since 3.1 + */ +public class Cas20ServiceTicketValidator extends AbstractCasProtocolUrlBasedTicketValidator { + + /** The CAS 2.0 protocol proxy callback url. */ + private String proxyCallbackUrl; + + /** The storage location of the proxy granting tickets. */ + private ProxyGrantingTicketStorage proxyGrantingTicketStorage; + + /** Implementation of the proxy retriever. */ + private ProxyRetriever proxyRetriever; + + /** + * Constructs an instance of the CAS 2.0 Service Ticket Validator with the supplied + * CAS server url prefix. + * + * @param casServerUrlPrefix the CAS Server URL prefix. + */ + public Cas20ServiceTicketValidator(final String casServerUrlPrefix) { + super(casServerUrlPrefix); + this.proxyRetriever = new Cas20ProxyRetriever(casServerUrlPrefix, getEncoding()); + } + + /** + * Adds the pgtUrl to the list of parameters to pass to the CAS server. + * + * @param urlParameters the Map containing the existing parameters to send to the server. + */ + protected final void populateUrlAttributeMap(final Map urlParameters) { + urlParameters.put("pgtUrl", this.proxyCallbackUrl); + } + + protected String getUrlSuffix() { + return "serviceValidate"; + } + + protected final Assertion parseResponseFromServer(final String response) throws TicketValidationException { + final String error = XmlUtils.getTextForElement(response, + "authenticationFailure"); + + if (CommonUtils.isNotBlank(error)) { + throw new TicketValidationException(error); + } + + final String principal = XmlUtils.getTextForElement(response, "user"); + final String proxyGrantingTicketIou = XmlUtils.getTextForElement(response, "proxyGrantingTicket"); + final String proxyGrantingTicket = this.proxyGrantingTicketStorage != null ? this.proxyGrantingTicketStorage.retrieve(proxyGrantingTicketIou) : null; + + if (CommonUtils.isEmpty(principal)) { + throw new TicketValidationException("No principal was found in the response from the CAS server."); + } + + final Assertion assertion; + final Map attributes = extractCustomAttributes(response); + if (CommonUtils.isNotBlank(proxyGrantingTicket)) { + final AttributePrincipal attributePrincipal = new AttributePrincipalImpl(principal, attributes, proxyGrantingTicket, this.proxyRetriever); + assertion = new AssertionImpl(attributePrincipal); + } else { + assertion = new AssertionImpl(new AttributePrincipalImpl(principal, attributes)); + } + + customParseResponse(response, assertion); + + return assertion; + } + + /** + * Default attribute parsing of attributes that look like the following: + * <cas:attributes> + * <cas:attribute1>value</cas:attribute1> + * <cas:attribute2>value</cas:attribute2> + * </cas:attributes> + *

+ * This code is here merely for sample/demonstration purposes for those wishing to modify the CAS2 protocol. You'll + * probably want a more robust implementation or to use SAML 1.1 + * + * @param xml the XML to parse. + * @return the map of attributes. + */ + protected Map extractCustomAttributes(final String xml) { + final int pos1 = xml.indexOf(""); + final int pos2 = xml.indexOf(""); + + if (pos1 == -1) { + return Collections.emptyMap(); + } + + final String attributesText = xml.substring(pos1+16, pos2); + + final Map attributes = new HashMap(); + final BufferedReader br = new BufferedReader(new StringReader(attributesText)); + + String line; + final List attributeNames = new ArrayList(); + try { + while ((line = br.readLine()) != null) { + final String trimmedLine = line.trim(); + if (trimmedLine.length() > 0) { + final int leftPos = trimmedLine.indexOf(":"); + final int rightPos = trimmedLine.indexOf(">"); + attributeNames.add(trimmedLine.substring(leftPos+1, rightPos)); + } + } + br.close(); + } catch (final IOException e) { + //ignore + } + + for (final String name : attributeNames) { + final List values = XmlUtils.getTextForElements(xml, name); + + if (values.size() == 1) { + attributes.put(name, values.get(0)); + } else { + attributes.put(name, values); + } + } + + return attributes; + } + + /** + * Template method if additional custom parsing (such as Proxying) needs to be done. + * + * @param response the original response from the CAS server. + * @param assertion the partially constructed assertion. + * @throws TicketValidationException if there is a problem constructing the Assertion. + */ + protected void customParseResponse(final String response, final Assertion assertion) throws TicketValidationException { + // nothing to do + } + + public final void setProxyCallbackUrl(final String proxyCallbackUrl) { + this.proxyCallbackUrl = proxyCallbackUrl; + } + + public final void setProxyGrantingTicketStorage(final ProxyGrantingTicketStorage proxyGrantingTicketStorage) { + this.proxyGrantingTicketStorage = proxyGrantingTicketStorage; + } + + public final void setProxyRetriever(final ProxyRetriever proxyRetriever) { + this.proxyRetriever = proxyRetriever; + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/validation/InvalidProxyChainTicketValidationException.java b/modules/cas/src/main/java/org/jasig/cas/client/validation/InvalidProxyChainTicketValidationException.java new file mode 100644 index 00000000..d0f9c002 --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/validation/InvalidProxyChainTicketValidationException.java @@ -0,0 +1,61 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.validation; + +/** + * Exception denotes that an invalid proxy chain was sent from the CAS server to the local application. + * + * @author Scott Battaglia + * @version $Revision$ $Date$ + * @since 3.1 + */ +public final class InvalidProxyChainTicketValidationException extends TicketValidationException { + + /** + * Unique Id for Serialization + */ + private static final long serialVersionUID = -7736653266370691534L; + + /** + * Constructs an exception with the supplied message. + * @param string the supplied message. + */ + + public InvalidProxyChainTicketValidationException(final String string) { + super(string); + } + + /** + * Constructs an exception with the supplied message and chained throwable. + * @param string the message. + * @param throwable the root exception. + */ + public InvalidProxyChainTicketValidationException(final String string, final Throwable throwable) { + super(string, throwable); + } + + /** + * Constructs an exception with the chained throwable. + * @param throwable the root exception. + */ + public InvalidProxyChainTicketValidationException(final Throwable throwable) { + super(throwable); + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/validation/ProxyList.java b/modules/cas/src/main/java/org/jasig/cas/client/validation/ProxyList.java new file mode 100644 index 00000000..812e9706 --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/validation/ProxyList.java @@ -0,0 +1,61 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.validation; + +import org.jasig.cas.client.util.CommonUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Arrays; + +/** + * Holding class for the proxy list to make Spring configuration easier. + * + * @author Scott Battaglia + * @version $Revision$ $Date$ + * @since 3.1.3 + */ +public final class ProxyList { + + private final List proxyChains; + + public ProxyList(final List proxyChains) { + CommonUtils.assertNotNull(proxyChains, "List of proxy chains cannot be null."); + this.proxyChains = proxyChains; + } + + public ProxyList() { + this(new ArrayList()); + } + + public boolean contains(String[] proxiedList) { + for (final String[] list : this.proxyChains) { + if (Arrays.equals(proxiedList, list)) { + return true; + } + } + + return false; + } + + public String toString() { + return this.proxyChains.toString(); + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/validation/ProxyListEditor.java b/modules/cas/src/main/java/org/jasig/cas/client/validation/ProxyListEditor.java new file mode 100644 index 00000000..ed5e7849 --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/validation/ProxyListEditor.java @@ -0,0 +1,64 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.validation; + +import org.jasig.cas.client.util.CommonUtils; + +import java.beans.PropertyEditorSupport; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; + +/** + * Convert a String-formatted list of acceptable proxies to an array. + * + * @author Scott Battaglia + * @version $Revision$ $Date$ + * @since 3.1 + * + */ +public final class ProxyListEditor extends PropertyEditorSupport { + + public void setAsText(final String text) throws IllegalArgumentException { + final BufferedReader reader = new BufferedReader(new StringReader(text)); + final List proxyChains = new ArrayList(); + + try { + String line; + while ((line = reader.readLine()) != null) { + if (CommonUtils.isNotBlank(line)) { + proxyChains.add(line.trim().split(" ")); + } + } + } catch (final IOException e) { + // ignore this + } finally { + try { + reader.close(); + } catch (final IOException e) { + // nothing to do + } + } + + setValue(new ProxyList(proxyChains)); + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/validation/Saml11TicketValidationFilter.java b/modules/cas/src/main/java/org/jasig/cas/client/validation/Saml11TicketValidationFilter.java new file mode 100644 index 00000000..0d981d51 --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/validation/Saml11TicketValidationFilter.java @@ -0,0 +1,64 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.validation; + +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; + +/** + * Implementation of TicketValidationFilter that can instanciate a SAML 1.1 Ticket Validator. + *

+ * Deployers can provide the "casServerUrlPrefix" and "tolerance" properties of the Saml11TicketValidator via the + * context or filter init parameters. + *

+ * Note, the "final" on this class helps ensure the compliance required in the initInternal method. + * + * @author Scott Battaglia + * @version $Revision$ $Date$ + * @since 3.1 + */ +public final class Saml11TicketValidationFilter extends AbstractTicketValidationFilter { + + public Saml11TicketValidationFilter() { + setArtifactParameterName("SAMLart"); + setServiceParameterName("TARGET"); + } + + protected void initInternal(final FilterConfig filterConfig) throws ServletException { + super.initInternal(filterConfig); + + log.warn("SAML1.1 compliance requires the [artifactParameterName] and [serviceParameterName] to be set to specified values."); + log.warn("This filter will overwrite any user-provided values (if any are provided)"); + + setArtifactParameterName("SAMLart"); + setServiceParameterName("TARGET"); + } + + protected final TicketValidator getTicketValidator(final FilterConfig filterConfig) { + final Saml11TicketValidator validator = new Saml11TicketValidator(getPropertyFromInitParams(filterConfig, "casServerUrlPrefix", null)); + final String tolerance = getPropertyFromInitParams(filterConfig, "tolerance", "1000"); + validator.setTolerance(Long.parseLong(tolerance)); + validator.setRenew(parseBoolean(getPropertyFromInitParams(filterConfig, "renew", "false"))); + validator.setHostnameVerifier(getHostnameVerifier(filterConfig)); + validator.setEncoding(getPropertyFromInitParams(filterConfig, "encoding", null)); + validator.setDisableXmlSchemaValidation(parseBoolean(getPropertyFromInitParams(filterConfig, "disableXmlSchemaValidation", "false"))); + return validator; + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/validation/Saml11TicketValidator.java b/modules/cas/src/main/java/org/jasig/cas/client/validation/Saml11TicketValidator.java new file mode 100644 index 00000000..69025d62 --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/validation/Saml11TicketValidator.java @@ -0,0 +1,242 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.validation; + +import org.jasig.cas.client.authentication.AttributePrincipal; +import org.jasig.cas.client.authentication.AttributePrincipalImpl; +import org.jasig.cas.client.util.CommonUtils; +import org.opensaml.*; + +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.*; + +import javax.net.ssl.HttpsURLConnection; + +/** + * TicketValidator that can understand validating a SAML artifact. This includes the SOAP request/response. + * + * @author Scott Battaglia + * @version $Revision$ $Date$ + * @since 3.1 + */ +public final class Saml11TicketValidator extends AbstractUrlBasedTicketValidator { + + /** Time tolerance to allow for time drifting. */ + private long tolerance = 1000L; + + public Saml11TicketValidator(final String casServerUrlPrefix) { + super(casServerUrlPrefix); + } + + protected String getUrlSuffix() { + return "samlValidate"; + } + + protected void populateUrlAttributeMap(final Map urlParameters) { + final String service = urlParameters.get("service"); + urlParameters.remove("service"); + urlParameters.remove("ticket"); + urlParameters.put("TARGET", service); + } + + @Override + protected void setDisableXmlSchemaValidation(final boolean disabled) { + if (disabled) { + // according to our reading of the SAML 1.1 code, this should disable the schema checking. However, there may be a couple + // of error messages that slip through on start up! + XML.parserPool.setDefaultSchemas(null, null); + } + } + + protected Assertion parseResponseFromServer(final String response) throws TicketValidationException { + try { + final String removeStartOfSoapBody = response.substring(response.indexOf("") + 15); + final String removeEndOfSoapBody = removeStartOfSoapBody.substring(0, removeStartOfSoapBody.indexOf("")); + final SAMLResponse samlResponse = new SAMLResponse(new ByteArrayInputStream(CommonUtils.isNotBlank(getEncoding()) ? removeEndOfSoapBody.getBytes(Charset.forName(getEncoding())) : removeEndOfSoapBody.getBytes())); + + if (!samlResponse.getAssertions().hasNext()) { + throw new TicketValidationException("No assertions found."); + } + + for (final Iterator iter = samlResponse.getAssertions(); iter.hasNext();) { + final SAMLAssertion assertion = (SAMLAssertion) iter.next(); + + if (!isValidAssertion(assertion)) { + continue; + } + + final SAMLAuthenticationStatement authenticationStatement = getSAMLAuthenticationStatement(assertion); + + if (authenticationStatement == null) { + throw new TicketValidationException("No AuthentiationStatement found in SAML Assertion."); + } + final SAMLSubject subject = authenticationStatement.getSubject(); + + if (subject == null) { + throw new TicketValidationException("No Subject found in SAML Assertion."); + } + + final SAMLAttribute[] attributes = getAttributesFor(assertion, subject); + final Map personAttributes = new HashMap(); + for (final SAMLAttribute samlAttribute : attributes) { + final List values = getValuesFrom(samlAttribute); + + personAttributes.put(samlAttribute.getName(), values.size() == 1 ? values.get(0) : values); + } + + final AttributePrincipal principal = new AttributePrincipalImpl(subject.getNameIdentifier().getName(), personAttributes); + + final Map authenticationAttributes = new HashMap(); + authenticationAttributes.put("samlAuthenticationStatement::authMethod", authenticationStatement.getAuthMethod()); + + return new AssertionImpl(principal, authenticationAttributes); + } + } catch (final SAMLException e) { + throw new TicketValidationException(e); + } + + throw new TicketValidationException("No Assertion found within valid time range. Either there's a replay of the ticket or there's clock drift. Check tolerance range, or server/client synchronization."); + } + + private boolean isValidAssertion(final SAMLAssertion assertion) { + final Date notBefore = assertion.getNotBefore(); + final Date notOnOrAfter = assertion.getNotOnOrAfter(); + + if (assertion.getNotBefore() == null || assertion.getNotOnOrAfter() == null) { + log.debug("Assertion has no bounding dates. Will not process."); + return false; + } + + final long currentTime = getCurrentTimeInUtc().getTime(); + + if (currentTime + tolerance < notBefore.getTime()) { + log.debug("skipping assertion that's not yet valid..."); + return false; + } + + if (notOnOrAfter.getTime() <= currentTime - tolerance) { + log.debug("skipping expired assertion..."); + return false; + } + + return true; + } + + private SAMLAuthenticationStatement getSAMLAuthenticationStatement(final SAMLAssertion assertion) { + for (final Iterator iter = assertion.getStatements(); iter.hasNext();) { + final SAMLStatement statement = (SAMLStatement) iter.next(); + + if (statement instanceof SAMLAuthenticationStatement) { + return (SAMLAuthenticationStatement) statement; + } + } + + return null; + } + + private SAMLAttribute[] getAttributesFor(final SAMLAssertion assertion, final SAMLSubject subject) { + final List attributes = new ArrayList(); + for (final Iterator iter = assertion.getStatements(); iter.hasNext();) { + final SAMLStatement statement = (SAMLStatement) iter.next(); + + if (statement instanceof SAMLAttributeStatement) { + final SAMLAttributeStatement attributeStatement = (SAMLAttributeStatement) statement; + // used because SAMLSubject does not implement equals + if (subject.getNameIdentifier().getName().equals(attributeStatement.getSubject().getNameIdentifier().getName())) { + for (final Iterator iter2 = attributeStatement.getAttributes(); iter2.hasNext();) + attributes.add((SAMLAttribute) iter2.next()); + } + } + } + + return attributes.toArray(new SAMLAttribute[attributes.size()]); + } + + private List getValuesFrom(final SAMLAttribute attribute) { + final List list = new ArrayList(); + for (final Iterator iter = attribute.getValues(); iter.hasNext();) { + list.add(iter.next()); + } + return list; + } + + private Date getCurrentTimeInUtc() { + final Calendar c = Calendar.getInstance(); + c.setTimeZone(TimeZone.getTimeZone("UTC")); + return c.getTime(); + } + + protected String retrieveResponseFromServer(final URL validationUrl, final String ticket) { + + String MESSAGE_TO_SEND; + + try { + MESSAGE_TO_SEND = "" + + "" + ticket + + ""; + } catch (final SAMLException e) { + throw new RuntimeException(e); + } + + HttpURLConnection conn = null; + + try { + conn = (HttpURLConnection) validationUrl.openConnection(); + if (this.hostnameVerifier != null && conn instanceof HttpsURLConnection) { + ((HttpsURLConnection)conn).setHostnameVerifier(this.hostnameVerifier); + } + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "text/xml"); + conn.setRequestProperty("Content-Length", Integer.toString(MESSAGE_TO_SEND.length())); + conn.setRequestProperty("SOAPAction", "http://www.oasis-open.org/committees/security"); + conn.setUseCaches(false); + conn.setDoInput(true); + conn.setDoOutput(true); + + final DataOutputStream out = new DataOutputStream(conn.getOutputStream()); + out.writeBytes(MESSAGE_TO_SEND); + out.flush(); + out.close(); + + final BufferedReader in = new BufferedReader(CommonUtils.isNotBlank(getEncoding()) ? new InputStreamReader(conn.getInputStream(), Charset.forName(getEncoding())) : new InputStreamReader(conn.getInputStream())); + final StringBuilder buffer = new StringBuilder(256); + + String line; + + while ((line = in.readLine()) != null) { + buffer.append(line); + } + return buffer.toString(); + } catch (final IOException e) { + throw new RuntimeException(e); + } finally { + if (conn != null) { + conn.disconnect(); + } + } + } + + public void setTolerance(final long tolerance) { + this.tolerance = tolerance; + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/validation/TicketValidationException.java b/modules/cas/src/main/java/org/jasig/cas/client/validation/TicketValidationException.java new file mode 100644 index 00000000..e383fedb --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/validation/TicketValidationException.java @@ -0,0 +1,62 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.validation; + +/** + * Generic exception to be thrown when ticket validation fails. + * + * @author Scott Battaglia + * @version $Revision$ $Date$ + * @since 3.1 + */ +public class TicketValidationException extends Exception { + + /** + * Unique Id for Serialization + */ + private static final long serialVersionUID = -7036248720402711806L; + + /** + * Constructs an exception with the supplied message. + * + * @param string the message + */ + public TicketValidationException(final String string) { + super(string); + } + + /** + * Constructs an exception with the supplied message and chained throwable. + * + * @param string the message + * @param throwable the original exception + */ + public TicketValidationException(final String string, final Throwable throwable) { + super(string, throwable); + } + + /** + * Constructs an exception with the chained throwable. + * @param throwable the original exception. + */ + public TicketValidationException(final Throwable throwable) { + super(throwable); + } +} diff --git a/modules/cas/src/main/java/org/jasig/cas/client/validation/TicketValidator.java b/modules/cas/src/main/java/org/jasig/cas/client/validation/TicketValidator.java new file mode 100644 index 00000000..2efa2b9c --- /dev/null +++ b/modules/cas/src/main/java/org/jasig/cas/client/validation/TicketValidator.java @@ -0,0 +1,43 @@ +/** + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.jasig.cas.client.validation; + +/** + * Contract for a validator that will confirm the validity of a supplied ticket. + *

+ * Validator makes no statement about how to validate the ticket or the format of the ticket (other than that it must be a String). + * + * @author Scott Battaglia + * @version $Revision$ $Date$ + * @since 3.1 + */ +public interface TicketValidator { + + /** + * Attempts to validate a ticket for the provided service. + * + * @param ticket the ticket to attempt to validate. + * @param service the service this ticket is valid for. + * @return an assertion from the ticket. + * @throws TicketValidationException if the ticket cannot be validated. + * + */ + Assertion validate(String ticket, String service) throws TicketValidationException; +}