Adding shiro-cas back to master Still deprecated, but we cannot remove it until 2.0
Project: http://git-wip-us.apache.org/repos/asf/shiro/repo Commit: http://git-wip-us.apache.org/repos/asf/shiro/commit/056d7cc2 Tree: http://git-wip-us.apache.org/repos/asf/shiro/tree/056d7cc2 Diff: http://git-wip-us.apache.org/repos/asf/shiro/diff/056d7cc2 Branch: refs/heads/master Commit: 056d7cc27b462d94f8dc91490154dbca784d857c Parents: 3fccc75 Author: Brian Demers <[email protected]> Authored: Mon Nov 7 15:16:10 2016 -0500 Committer: Brian Demers <[email protected]> Committed: Mon Nov 7 15:16:10 2016 -0500 ---------------------------------------------------------------------- support/cas/pom.xml | 89 ++++++ .../shiro/cas/CasAuthenticationException.java | 46 +++ .../java/org/apache/shiro/cas/CasFilter.java | 156 +++++++++ .../java/org/apache/shiro/cas/CasRealm.java | 313 +++++++++++++++++++ .../org/apache/shiro/cas/CasSubjectFactory.java | 59 ++++ .../java/org/apache/shiro/cas/CasToken.java | 67 ++++ .../org/apache/shiro/cas/CasRealmTest.groovy | 176 +++++++++++ .../org/apache/shiro/cas/CasTokenTest.groovy | 49 +++ .../shiro/cas/MockServiceTicketValidator.groovy | 60 ++++ support/pom.xml | 1 + test-coverage/pom.xml | 4 + 11 files changed, 1020 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/shiro/blob/056d7cc2/support/cas/pom.xml ---------------------------------------------------------------------- diff --git a/support/cas/pom.xml b/support/cas/pom.xml new file mode 100644 index 0000000..387f018 --- /dev/null +++ b/support/cas/pom.xml @@ -0,0 +1,89 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ 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. + --> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + + <parent> + <groupId>org.apache.shiro</groupId> + <artifactId>shiro-root</artifactId> + <version>1.4.0-SNAPSHOT</version> + <relativePath>../../pom.xml</relativePath> + </parent> + + <modelVersion>4.0.0</modelVersion> + <artifactId>shiro-cas</artifactId> + <name>Apache Shiro :: Support :: CAS</name> + <packaging>bundle</packaging> + + <dependencies> + <dependency> + <groupId>org.apache.shiro</groupId> + <artifactId>shiro-web</artifactId> + </dependency> + <dependency> + <groupId>org.jasig.cas.client</groupId> + <artifactId>cas-client-core</artifactId> + <version>3.2.2</version> + </dependency> + <dependency> + <!-- for Optional SAML ticket validation: --> + <groupId>commons-codec</groupId> + <artifactId>commons-codec</artifactId> + <optional>true</optional> + </dependency> + <dependency> + <!-- for Optional SAML ticket validation: --> + <groupId>org.opensaml</groupId> + <artifactId>opensaml</artifactId> + <version>1.1</version> + <scope>runtime</scope> + <optional>true</optional> + </dependency> + <dependency> + <!-- for Optional SAML ticket validation: --> + <groupId>org.apache.santuario</groupId> + <artifactId>xmlsec</artifactId> + <version>1.4.3</version> + <scope>runtime</scope> + <optional>true</optional> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <extensions>true</extensions> + <configuration> + <instructions> + <Bundle-SymbolicName>org.apache.shiro.cas</Bundle-SymbolicName> + <Export-Package>org.apache.shiro.cas*;version=${project.version}</Export-Package> + <Import-Package> + org.apache.shiro*;version="${shiro.osgi.importRange}", + org.jasig.cas.client*;version="[3.2, 4)", + * + </Import-Package> + </instructions> + </configuration> + </plugin> + </plugins> + </build> + +</project> http://git-wip-us.apache.org/repos/asf/shiro/blob/056d7cc2/support/cas/src/main/java/org/apache/shiro/cas/CasAuthenticationException.java ---------------------------------------------------------------------- diff --git a/support/cas/src/main/java/org/apache/shiro/cas/CasAuthenticationException.java b/support/cas/src/main/java/org/apache/shiro/cas/CasAuthenticationException.java new file mode 100644 index 0000000..e3add40 --- /dev/null +++ b/support/cas/src/main/java/org/apache/shiro/cas/CasAuthenticationException.java @@ -0,0 +1,46 @@ +/* + * 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 org.apache.shiro.cas; + +import org.apache.shiro.authc.AuthenticationException; + +/** + * @since 1.2 + * @see <a href="https://github.com/bujiio/buji-pac4j">buji-pac4j</a> + * @deprecated replaced with Shiro integration in <a href="https://github.com/bujiio/buji-pac4j">buji-pac4j</a>. + */ +@Deprecated +public class CasAuthenticationException extends AuthenticationException { + + public CasAuthenticationException() { + super(); + } + + public CasAuthenticationException(String message) { + super(message); + } + + public CasAuthenticationException(Throwable cause) { + super(cause); + } + + public CasAuthenticationException(String message, Throwable cause) { + super(message, cause); + } +} http://git-wip-us.apache.org/repos/asf/shiro/blob/056d7cc2/support/cas/src/main/java/org/apache/shiro/cas/CasFilter.java ---------------------------------------------------------------------- diff --git a/support/cas/src/main/java/org/apache/shiro/cas/CasFilter.java b/support/cas/src/main/java/org/apache/shiro/cas/CasFilter.java new file mode 100644 index 0000000..88262a8 --- /dev/null +++ b/support/cas/src/main/java/org/apache/shiro/cas/CasFilter.java @@ -0,0 +1,156 @@ +/* + * 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 org.apache.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 javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.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}: + * <pre> + * [main] + * casFilter = org.apache.shiro.cas.CasFilter + * ... + * + * [urls] + * /shiro-cas = casFilter + * ... + * </pre> + * (example : http://host:port/mycontextpath/shiro-cas) + * + * @since 1.2 + * @see <a href="https://github.com/bujiio/buji-pac4j">buji-pac4j</a> + * @deprecated replaced with Shiro integration in <a href="https://github.com/bujiio/buji-pac4j">buji-pac4j</a>. + */ +@Deprecated +public class CasFilter extends AuthenticatingFilter { + + private static Logger logger = LoggerFactory.getLogger(CasFilter.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 <code>false</code> 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 <code>false</code> + */ + @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; + } +} http://git-wip-us.apache.org/repos/asf/shiro/blob/056d7cc2/support/cas/src/main/java/org/apache/shiro/cas/CasRealm.java ---------------------------------------------------------------------- diff --git a/support/cas/src/main/java/org/apache/shiro/cas/CasRealm.java b/support/cas/src/main/java/org/apache/shiro/cas/CasRealm.java new file mode 100644 index 0000000..791674a --- /dev/null +++ b/support/cas/src/main/java/org/apache/shiro/cas/CasRealm.java @@ -0,0 +1,313 @@ +/* + * 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 org.apache.shiro.cas; + +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationInfo; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authc.SimpleAuthenticationInfo; +import org.apache.shiro.authz.AuthorizationInfo; +import org.apache.shiro.authz.SimpleAuthorizationInfo; +import org.apache.shiro.realm.AuthorizingRealm; +import org.apache.shiro.subject.PrincipalCollection; +import org.apache.shiro.subject.SimplePrincipalCollection; +import org.apache.shiro.util.CollectionUtils; +import org.apache.shiro.util.StringUtils; +import org.jasig.cas.client.authentication.AttributePrincipal; +import org.jasig.cas.client.validation.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * This realm implementation acts as a CAS client to a CAS server for authentication and basic authorization. + * <p/> + * This realm functions by inspecting a submitted {@link org.apache.shiro.cas.CasToken CasToken} (which essentially + * wraps a CAS service ticket) and validates it against the CAS server using a configured CAS + * {@link org.jasig.cas.client.validation.TicketValidator TicketValidator}. + * <p/> + * The {@link #getValidationProtocol() validationProtocol} is {@code CAS} by default, which indicates that a + * a {@link org.jasig.cas.client.validation.Cas20ServiceTicketValidator Cas20ServiceTicketValidator} + * will be used for ticket validation. You can alternatively set + * or {@link org.jasig.cas.client.validation.Saml11TicketValidator Saml11TicketValidator} of CAS client. It is based on + * {@link AuthorizingRealm AuthorizingRealm} for both authentication and authorization. User id and attributes are retrieved from the CAS + * service ticket validation response during authentication phase. Roles and permissions are computed during authorization phase (according + * to the attributes previously retrieved). + * + * @since 1.2 + * @see <a href="https://github.com/bujiio/buji-pac4j">buji-pac4j</a> + * @deprecated replaced with Shiro integration in <a href="https://github.com/bujiio/buji-pac4j">buji-pac4j</a>. + */ +@Deprecated +public class CasRealm extends AuthorizingRealm { + + // default name of the CAS attribute for remember me authentication (CAS 3.4.10+) + public static final String DEFAULT_REMEMBER_ME_ATTRIBUTE_NAME = "longTermAuthenticationRequestTokenUsed"; + public static final String DEFAULT_VALIDATION_PROTOCOL = "CAS"; + + private static Logger log = LoggerFactory.getLogger(CasRealm.class); + + // this is the url of the CAS server (example : http://host:port/cas) + private String casServerUrlPrefix; + + // this is the CAS service url of the application (example : http://host:port/mycontextpath/shiro-cas) + private String casService; + + /* CAS protocol to use for ticket validation : CAS (default) or SAML : + - CAS protocol can be used with CAS server version < 3.1 : in this case, no user attributes can be retrieved from the CAS ticket validation response (except if there are some customizations on CAS server side) + - SAML protocol can be used with CAS server version >= 3.1 : in this case, user attributes can be extracted from the CAS ticket validation response + */ + private String validationProtocol = DEFAULT_VALIDATION_PROTOCOL; + + // default name of the CAS attribute for remember me authentication (CAS 3.4.10+) + private String rememberMeAttributeName = DEFAULT_REMEMBER_ME_ATTRIBUTE_NAME; + + // this class from the CAS client is used to validate a service ticket on CAS server + private TicketValidator ticketValidator; + + // default roles to applied to authenticated user + private String defaultRoles; + + // default permissions to applied to authenticated user + private String defaultPermissions; + + // names of attributes containing roles + private String roleAttributeNames; + + // names of attributes containing permissions + private String permissionAttributeNames; + + public CasRealm() { + setAuthenticationTokenClass(CasToken.class); + } + + @Override + protected void onInit() { + super.onInit(); + ensureTicketValidator(); + } + + protected TicketValidator ensureTicketValidator() { + if (this.ticketValidator == null) { + this.ticketValidator = createTicketValidator(); + } + return this.ticketValidator; + } + + protected TicketValidator createTicketValidator() { + String urlPrefix = getCasServerUrlPrefix(); + if ("saml".equalsIgnoreCase(getValidationProtocol())) { + return new Saml11TicketValidator(urlPrefix); + } + return new Cas20ServiceTicketValidator(urlPrefix); + } + + /** + * Authenticates a user and retrieves its information. + * + * @param token the authentication token + * @throws AuthenticationException if there is an error during authentication. + */ + @Override + @SuppressWarnings("unchecked") + protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { + CasToken casToken = (CasToken) token; + if (token == null) { + return null; + } + + String ticket = (String)casToken.getCredentials(); + if (!StringUtils.hasText(ticket)) { + return null; + } + + TicketValidator ticketValidator = ensureTicketValidator(); + + try { + // contact CAS server to validate service ticket + Assertion casAssertion = ticketValidator.validate(ticket, getCasService()); + // get principal, user id and attributes + AttributePrincipal casPrincipal = casAssertion.getPrincipal(); + String userId = casPrincipal.getName(); + log.debug("Validate ticket : {} in CAS server : {} to retrieve user : {}", new Object[]{ + ticket, getCasServerUrlPrefix(), userId + }); + + Map<String, Object> attributes = casPrincipal.getAttributes(); + // refresh authentication token (user id + remember me) + casToken.setUserId(userId); + String rememberMeAttributeName = getRememberMeAttributeName(); + String rememberMeStringValue = (String)attributes.get(rememberMeAttributeName); + boolean isRemembered = rememberMeStringValue != null && Boolean.parseBoolean(rememberMeStringValue); + if (isRemembered) { + casToken.setRememberMe(true); + } + // create simple authentication info + List<Object> principals = CollectionUtils.asList(userId, attributes); + PrincipalCollection principalCollection = new SimplePrincipalCollection(principals, getName()); + return new SimpleAuthenticationInfo(principalCollection, ticket); + } catch (TicketValidationException e) { + throw new CasAuthenticationException("Unable to validate ticket [" + ticket + "]", e); + } + } + + /** + * Retrieves the AuthorizationInfo for the given principals (the CAS previously authenticated user : id + attributes). + * + * @param principals the primary identifying principals of the AuthorizationInfo that should be retrieved. + * @return the AuthorizationInfo associated with this principals. + */ + @Override + @SuppressWarnings("unchecked") + protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { + // retrieve user information + SimplePrincipalCollection principalCollection = (SimplePrincipalCollection) principals; + List<Object> listPrincipals = principalCollection.asList(); + Map<String, String> attributes = (Map<String, String>) listPrincipals.get(1); + // create simple authorization info + SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); + // add default roles + addRoles(simpleAuthorizationInfo, split(defaultRoles)); + // add default permissions + addPermissions(simpleAuthorizationInfo, split(defaultPermissions)); + // get roles from attributes + List<String> attributeNames = split(roleAttributeNames); + for (String attributeName : attributeNames) { + String value = attributes.get(attributeName); + addRoles(simpleAuthorizationInfo, split(value)); + } + // get permissions from attributes + attributeNames = split(permissionAttributeNames); + for (String attributeName : attributeNames) { + String value = attributes.get(attributeName); + addPermissions(simpleAuthorizationInfo, split(value)); + } + return simpleAuthorizationInfo; + } + + /** + * Split a string into a list of not empty and trimmed strings, delimiter is a comma. + * + * @param s the input string + * @return the list of not empty and trimmed strings + */ + private List<String> split(String s) { + List<String> list = new ArrayList<String>(); + String[] elements = StringUtils.split(s, ','); + if (elements != null && elements.length > 0) { + for (String element : elements) { + if (StringUtils.hasText(element)) { + list.add(element.trim()); + } + } + } + return list; + } + + /** + * Add roles to the simple authorization info. + * + * @param simpleAuthorizationInfo + * @param roles the list of roles to add + */ + private void addRoles(SimpleAuthorizationInfo simpleAuthorizationInfo, List<String> roles) { + for (String role : roles) { + simpleAuthorizationInfo.addRole(role); + } + } + + /** + * Add permissions to the simple authorization info. + * + * @param simpleAuthorizationInfo + * @param permissions the list of permissions to add + */ + private void addPermissions(SimpleAuthorizationInfo simpleAuthorizationInfo, List<String> permissions) { + for (String permission : permissions) { + simpleAuthorizationInfo.addStringPermission(permission); + } + } + + public String getCasServerUrlPrefix() { + return casServerUrlPrefix; + } + + public void setCasServerUrlPrefix(String casServerUrlPrefix) { + this.casServerUrlPrefix = casServerUrlPrefix; + } + + public String getCasService() { + return casService; + } + + public void setCasService(String casService) { + this.casService = casService; + } + + public String getValidationProtocol() { + return validationProtocol; + } + + public void setValidationProtocol(String validationProtocol) { + this.validationProtocol = validationProtocol; + } + + public String getRememberMeAttributeName() { + return rememberMeAttributeName; + } + + public void setRememberMeAttributeName(String rememberMeAttributeName) { + this.rememberMeAttributeName = rememberMeAttributeName; + } + + public String getDefaultRoles() { + return defaultRoles; + } + + public void setDefaultRoles(String defaultRoles) { + this.defaultRoles = defaultRoles; + } + + public String getDefaultPermissions() { + return defaultPermissions; + } + + public void setDefaultPermissions(String defaultPermissions) { + this.defaultPermissions = defaultPermissions; + } + + public String getRoleAttributeNames() { + return roleAttributeNames; + } + + public void setRoleAttributeNames(String roleAttributeNames) { + this.roleAttributeNames = roleAttributeNames; + } + + public String getPermissionAttributeNames() { + return permissionAttributeNames; + } + + public void setPermissionAttributeNames(String permissionAttributeNames) { + this.permissionAttributeNames = permissionAttributeNames; + } +} http://git-wip-us.apache.org/repos/asf/shiro/blob/056d7cc2/support/cas/src/main/java/org/apache/shiro/cas/CasSubjectFactory.java ---------------------------------------------------------------------- diff --git a/support/cas/src/main/java/org/apache/shiro/cas/CasSubjectFactory.java b/support/cas/src/main/java/org/apache/shiro/cas/CasSubjectFactory.java new file mode 100644 index 0000000..51a774e --- /dev/null +++ b/support/cas/src/main/java/org/apache/shiro/cas/CasSubjectFactory.java @@ -0,0 +1,59 @@ +/* + * 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 org.apache.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 <a href="https://github.com/bujiio/buji-pac4j">buji-pac4j</a> + * @deprecated replaced with Shiro integration in <a href="https://github.com/bujiio/buji-pac4j">buji-pac4j</a>. + */ +@Deprecated +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); + } +} http://git-wip-us.apache.org/repos/asf/shiro/blob/056d7cc2/support/cas/src/main/java/org/apache/shiro/cas/CasToken.java ---------------------------------------------------------------------- diff --git a/support/cas/src/main/java/org/apache/shiro/cas/CasToken.java b/support/cas/src/main/java/org/apache/shiro/cas/CasToken.java new file mode 100644 index 0000000..221d1cb --- /dev/null +++ b/support/cas/src/main/java/org/apache/shiro/cas/CasToken.java @@ -0,0 +1,67 @@ +/* + * 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 org.apache.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 <a href="https://github.com/bujiio/buji-pac4j">buji-pac4j</a> + * @deprecated replaced with Shiro integration in <a href="https://github.com/bujiio/buji-pac4j">buji-pac4j</a>. + */ +@Deprecated +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; + } +} http://git-wip-us.apache.org/repos/asf/shiro/blob/056d7cc2/support/cas/src/test/groovy/org/apache/shiro/cas/CasRealmTest.groovy ---------------------------------------------------------------------- diff --git a/support/cas/src/test/groovy/org/apache/shiro/cas/CasRealmTest.groovy b/support/cas/src/test/groovy/org/apache/shiro/cas/CasRealmTest.groovy new file mode 100644 index 0000000..baf2a57 --- /dev/null +++ b/support/cas/src/test/groovy/org/apache/shiro/cas/CasRealmTest.groovy @@ -0,0 +1,176 @@ +/* + * 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 org.apache.shiro.cas + +import org.apache.shiro.authc.AuthenticationInfo +import org.apache.shiro.authz.AuthorizationInfo + +/** + * Unit tests for the {@link CasRealm} implementation. + * + * @since 1.2 + * @see <a href="https://github.com/bujiio/buji-pac4j">buji-pac4j</a> + * @deprecated replaced with Shiro integration in <a href="https://github.com/bujiio/buji-pac4j">buji-pac4j</a>. + */ +@Deprecated +class CasRealmTest extends GroovyTestCase { + + /** + * Creates a CAS realm with a ticket validator mock. + * + * @return CasRealm The CAS realm for testing. + */ + private CasRealm createCasRealm() { + new CasRealm(ticketValidator: new MockServiceTicketValidator()); + } + + void testNoAttribute() { + CasRealm casRealm = createCasRealm(); + CasToken casToken = new CasToken('$=defaultId'); + AuthenticationInfo authenticationInfo = casRealm.doGetAuthenticationInfo(casToken); + def principals = authenticationInfo.principals + assertEquals "defaultId", principals.primaryPrincipal + def attributes = principals.asList()[1] //returns a map + assertEquals 0, attributes.size() + AuthorizationInfo authorizationInfo = casRealm.doGetAuthorizationInfo(principals); + assertNull authorizationInfo.stringPermissions + assertNull authorizationInfo.roles + } + + void testNoAttributeDefaultRoleAndPermission() { + CasRealm casRealm = createCasRealm(); + casRealm.defaultRoles = "defaultRole" + casRealm.defaultPermissions = "defaultPermission" + CasToken casToken = new CasToken('$=defaultId'); + AuthenticationInfo authenticationInfo = casRealm.doGetAuthenticationInfo(casToken); + def principals = authenticationInfo.principals + assertEquals "defaultId", principals.primaryPrincipal + def attributes = principals.oneByType(Map) + assertEquals 0, attributes.size() + AuthorizationInfo authorizationInfo = casRealm.doGetAuthorizationInfo(principals); + assertTrue authorizationInfo.roles.contains("defaultRole") + assertTrue authorizationInfo.stringPermissions.contains("defaultPermission") + } + + void testNoAttributeDefaultRolesAndPermissions() { + CasRealm casRealm = createCasRealm(); + casRealm.defaultRoles = "defaultRole1, defaultRole2" + casRealm.defaultPermissions = "defaultPermission1,defaultPermission2" + CasToken casToken = new CasToken('$=defaultId'); + AuthenticationInfo authcInfo = casRealm.doGetAuthenticationInfo(casToken); + def principals = authcInfo.principals + assertEquals "defaultId", principals.primaryPrincipal + def attributes = principals.oneByType(Map) + assertEquals 0, attributes.size() + AuthorizationInfo authzInfo = casRealm.doGetAuthorizationInfo(principals) + assertEquals 2, authzInfo.roles.size() + assertTrue authzInfo.roles.contains("defaultRole1") + assertTrue authzInfo.roles.contains("defaultRole2") + assertEquals 2, authzInfo.stringPermissions.size() + assertTrue authzInfo.stringPermissions.contains("defaultPermission1") + assertTrue authzInfo.stringPermissions.contains("defaultPermission2") + } + + void testRoleAndPermission() { + CasRealm casRealm = createCasRealm(); + casRealm.roleAttributeNames = "role" + casRealm.permissionAttributeNames = "permission" + CasToken casToken = new CasToken('$=defaultId|role=aRole|permission=aPermission'); + AuthenticationInfo authcInfo = casRealm.doGetAuthenticationInfo(casToken); + def principals = authcInfo.principals + assertEquals "defaultId", principals.primaryPrincipal + def attributes = principals.oneByType(Map) + assertEquals 2, attributes.size() + assertEquals "aRole", attributes['role'] + assertEquals "aPermission", attributes['permission'] + AuthorizationInfo authzInfo = casRealm.doGetAuthorizationInfo(principals); + assertTrue authzInfo.roles.contains("aRole") + assertTrue authzInfo.stringPermissions.contains("aPermission") + } + + void testRolesAndPermissions() { + CasRealm casRealm = createCasRealm(); + casRealm.setRoleAttributeNames("role1 , role2"); + casRealm.setPermissionAttributeNames("permission1,permission2"); + CasToken casToken = new CasToken( + '$=defaultId|role1=role11 , role12|role2=role21,role22|permission1=permission11, permission12|permission2=permission21 ,permission22'); + AuthenticationInfo authcInfo = casRealm.doGetAuthenticationInfo(casToken); + def principals = authcInfo.principals + assertEquals "defaultId", principals.primaryPrincipal + def attributes = principals.oneByType(Map) + assertEquals "role11 , role12", attributes['role1'] + assertEquals "role21,role22", attributes['role2'] + assertEquals "permission11, permission12", attributes['permission1'] + assertEquals "permission21 ,permission22", attributes['permission2'] + AuthorizationInfo authzInfo = casRealm.doGetAuthorizationInfo(principals); + assertEquals 4, authzInfo.roles.size() + assertTrue authzInfo.roles.contains("role11") + assertTrue authzInfo.roles.contains("role12") + assertTrue authzInfo.roles.contains("role21") + assertTrue authzInfo.roles.contains("role22") + assertTrue authzInfo.stringPermissions.contains("permission11") + assertTrue authzInfo.stringPermissions.contains("permission12") + assertTrue authzInfo.stringPermissions.contains("permission21") + assertTrue authzInfo.stringPermissions.contains("permission22") + } + + void testNotRememberMe() { + CasRealm casRealm = createCasRealm(); + CasToken casToken = new CasToken("\$=defaultId|$CasRealm.DEFAULT_REMEMBER_ME_ATTRIBUTE_NAME=false"); + AuthenticationInfo authcInfo = casRealm.doGetAuthenticationInfo(casToken); + def principals = authcInfo.principals + assertEquals "defaultId", principals.primaryPrincipal + def attributes = principals.oneByType(Map) + assertEquals "false", attributes[CasRealm.DEFAULT_REMEMBER_ME_ATTRIBUTE_NAME] + assertFalse casToken.rememberMe + AuthorizationInfo authzInfo = casRealm.doGetAuthorizationInfo(principals); + assertNull authzInfo.stringPermissions + assertNull authzInfo.roles + } + + void testRememberMe() { + CasRealm casRealm = createCasRealm(); + CasToken casToken = new CasToken("\$=defaultId|$CasRealm.DEFAULT_REMEMBER_ME_ATTRIBUTE_NAME=true"); + AuthenticationInfo authcInfo = casRealm.doGetAuthenticationInfo(casToken); + def principals = authcInfo.principals + assertEquals "defaultId", principals.primaryPrincipal + def attributes = principals.oneByType(Map) + assertEquals "true", attributes[CasRealm.DEFAULT_REMEMBER_ME_ATTRIBUTE_NAME] + assertTrue casToken.rememberMe + AuthorizationInfo authzInfo = casRealm.doGetAuthorizationInfo(principals); + assertNull authzInfo.stringPermissions + assertNull authzInfo.roles + } + + void testRememberMeNewAttributeName() { + CasRealm casRealm = createCasRealm(); + casRealm.rememberMeAttributeName = "rme" + CasToken casToken = new CasToken('$=defaultId|rme=true'); + AuthenticationInfo authcInfo = casRealm.doGetAuthenticationInfo(casToken); + def principals = authcInfo.principals + assertEquals "defaultId", principals.primaryPrincipal + def attributes = principals.oneByType(Map) + assertEquals "true", attributes[casRealm.rememberMeAttributeName] + assertTrue casToken.rememberMe + AuthorizationInfo authzInfo = casRealm.doGetAuthorizationInfo(principals); + assertNull authzInfo.stringPermissions + assertNull authzInfo.roles + } + +} http://git-wip-us.apache.org/repos/asf/shiro/blob/056d7cc2/support/cas/src/test/groovy/org/apache/shiro/cas/CasTokenTest.groovy ---------------------------------------------------------------------- diff --git a/support/cas/src/test/groovy/org/apache/shiro/cas/CasTokenTest.groovy b/support/cas/src/test/groovy/org/apache/shiro/cas/CasTokenTest.groovy new file mode 100644 index 0000000..ae86eee --- /dev/null +++ b/support/cas/src/test/groovy/org/apache/shiro/cas/CasTokenTest.groovy @@ -0,0 +1,49 @@ +/* + * 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 org.apache.shiro.cas + +/** + * Unit tests for the {@link CasToken} implementation. + * + * @since 1.2 + * @see <a href="https://github.com/bujiio/buji-pac4j">buji-pac4j</a> + * @deprecated replaced with Shiro integration in <a href="https://github.com/bujiio/buji-pac4j">buji-pac4j</a>. + */ +@Deprecated +class CasTokenTest extends GroovyTestCase { + + void testPrincipal() { + CasToken casToken = new CasToken("fakeTicket") + assertNull casToken.principal + casToken.userId = "myUserId" + assertEquals "myUserId", casToken.principal + } + + void testCredentials() { + CasToken casToken = new CasToken("fakeTicket") + assertEquals "fakeTicket", casToken.credentials + } + + void testRememberMe() { + CasToken casToken = new CasToken("fakeTicket") + assertFalse casToken.rememberMe + casToken.rememberMe = true + assertTrue casToken.rememberMe + } +} http://git-wip-us.apache.org/repos/asf/shiro/blob/056d7cc2/support/cas/src/test/groovy/org/apache/shiro/cas/MockServiceTicketValidator.groovy ---------------------------------------------------------------------- diff --git a/support/cas/src/test/groovy/org/apache/shiro/cas/MockServiceTicketValidator.groovy b/support/cas/src/test/groovy/org/apache/shiro/cas/MockServiceTicketValidator.groovy new file mode 100644 index 0000000..fc46ab2 --- /dev/null +++ b/support/cas/src/test/groovy/org/apache/shiro/cas/MockServiceTicketValidator.groovy @@ -0,0 +1,60 @@ +/* + * 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 org.apache.shiro.cas + +import org.apache.shiro.util.StringUtils +import org.jasig.cas.client.authentication.AttributePrincipalImpl +import org.jasig.cas.client.validation.Assertion +import org.jasig.cas.client.validation.AssertionImpl +import org.jasig.cas.client.validation.TicketValidationException +import org.jasig.cas.client.validation.TicketValidator + +/** + * @since 1.2 + * @see <a href="https://github.com/bujiio/buji-pac4j">buji-pac4j</a> + * @deprecated replaced with Shiro integration in <a href="https://github.com/bujiio/buji-pac4j">buji-pac4j</a>. + */ +@Deprecated +class MockServiceTicketValidator implements TicketValidator { + + /** + * Returns different assertions according to the ticket input. The format of the mock ticket must be : + * key1=value1,key2=value2,...,keyN=valueN. If keyX is $, valueX is considered to be the name of the principal, otherwise (keyX, valueX) + * is considered to be an attribute of the principal. + */ + public Assertion validate(String ticket, String service) throws TicketValidationException { + String name = null; + def attributes = [:] + String[] elements = StringUtils.split(ticket, '|' as char); + int length = elements.length; + for (int i = 0; i < length; i++) { + String[] pair = StringUtils.split(elements[i], '=' as char); + String key = pair[0].trim(); + String value = pair[1].trim(); + if ('$'.equals(key)) { + name = value; + } else { + attributes.put(key, value); + } + } + AttributePrincipalImpl attributePrincipalImpl = new AttributePrincipalImpl(name, attributes); + return new AssertionImpl(attributePrincipalImpl, [:]); + + } +} http://git-wip-us.apache.org/repos/asf/shiro/blob/056d7cc2/support/pom.xml ---------------------------------------------------------------------- diff --git a/support/pom.xml b/support/pom.xml index e753d60..fd7f801 100644 --- a/support/pom.xml +++ b/support/pom.xml @@ -40,6 +40,7 @@ <module>guice</module> <module>openid4j</module> <module>features</module> + <module>cas</module> <module>spring-boot</module> <module>servlet-plugin</module> <module>jaxrs</module> http://git-wip-us.apache.org/repos/asf/shiro/blob/056d7cc2/test-coverage/pom.xml ---------------------------------------------------------------------- diff --git a/test-coverage/pom.xml b/test-coverage/pom.xml index 8705d61..f088f32 100644 --- a/test-coverage/pom.xml +++ b/test-coverage/pom.xml @@ -46,6 +46,10 @@ </dependency> <dependency> <groupId>org.apache.shiro</groupId> + <artifactId>shiro-cas</artifactId> + </dependency> + <dependency> + <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> </dependency> <dependency>
