001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     *
010     *     http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing,
013     * software distributed under the License is distributed on an
014     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015     * KIND, either express or implied.  See the License for the
016     * specific language governing permissions and limitations
017     * under the License.
018     */
019    package org.apache.shiro.cas;
020    
021    import org.apache.shiro.authc.AuthenticationException;
022    import org.apache.shiro.authc.AuthenticationInfo;
023    import org.apache.shiro.authc.AuthenticationToken;
024    import org.apache.shiro.authc.SimpleAuthenticationInfo;
025    import org.apache.shiro.authz.AuthorizationInfo;
026    import org.apache.shiro.authz.SimpleAuthorizationInfo;
027    import org.apache.shiro.realm.AuthorizingRealm;
028    import org.apache.shiro.subject.PrincipalCollection;
029    import org.apache.shiro.subject.SimplePrincipalCollection;
030    import org.apache.shiro.util.CollectionUtils;
031    import org.apache.shiro.util.StringUtils;
032    import org.jasig.cas.client.authentication.AttributePrincipal;
033    import org.jasig.cas.client.validation.*;
034    import org.slf4j.Logger;
035    import org.slf4j.LoggerFactory;
036    
037    import java.util.ArrayList;
038    import java.util.List;
039    import java.util.Map;
040    
041    /**
042     * This realm implementation acts as a CAS client to a CAS server for authentication and basic authorization.
043     * <p/>
044     * This realm functions by inspecting a submitted {@link org.apache.shiro.cas.CasToken CasToken} (which essentially 
045     * wraps a CAS service ticket) and validates it against the CAS server using a configured CAS
046     * {@link org.jasig.cas.client.validation.TicketValidator TicketValidator}.
047     * <p/>
048     * The {@link #getValidationProtocol() validationProtocol} is {@code CAS} by default, which indicates that a
049     * a {@link org.jasig.cas.client.validation.Cas20ServiceTicketValidator Cas20ServiceTicketValidator}
050     * will be used for ticket validation.  You can alternatively set
051     * or {@link org.jasig.cas.client.validation.Saml11TicketValidator Saml11TicketValidator} of CAS client. It is based on
052     * {@link AuthorizingRealm AuthorizingRealm} for both authentication and authorization. User id and attributes are retrieved from the CAS
053     * service ticket validation response during authentication phase. Roles and permissions are computed during authorization phase (according
054     * to the attributes previously retrieved).
055     *
056     * @since 1.2
057     */
058    public class CasRealm extends AuthorizingRealm {
059    
060        // default name of the CAS attribute for remember me authentication (CAS 3.4.10+)
061        public static final String DEFAULT_REMEMBER_ME_ATTRIBUTE_NAME = "longTermAuthenticationRequestTokenUsed";
062        public static final String DEFAULT_VALIDATION_PROTOCOL = "CAS";
063        
064        private static Logger log = LoggerFactory.getLogger(CasRealm.class);
065        
066        // this is the url of the CAS server (example : http://host:port/cas)
067        private String casServerUrlPrefix;
068        
069        // this is the CAS service url of the application (example : http://host:port/mycontextpath/shiro-cas)
070        private String casService;
071        
072        /* CAS protocol to use for ticket validation : CAS (default) or SAML :
073           - 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)
074           - 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
075        */
076        private String validationProtocol = DEFAULT_VALIDATION_PROTOCOL;
077        
078        // default name of the CAS attribute for remember me authentication (CAS 3.4.10+)
079        private String rememberMeAttributeName = DEFAULT_REMEMBER_ME_ATTRIBUTE_NAME;
080        
081        // this class from the CAS client is used to validate a service ticket on CAS server
082        private TicketValidator ticketValidator;
083        
084        // default roles to applied to authenticated user
085        private String defaultRoles;
086        
087        // default permissions to applied to authenticated user
088        private String defaultPermissions;
089        
090        // names of attributes containing roles
091        private String roleAttributeNames;
092        
093        // names of attributes containing permissions
094        private String permissionAttributeNames;
095        
096        public CasRealm() {
097            setAuthenticationTokenClass(CasToken.class);
098        }
099    
100        @Override
101        protected void onInit() {
102            super.onInit();
103            ensureTicketValidator();
104        }
105    
106        protected TicketValidator ensureTicketValidator() {
107            if (this.ticketValidator == null) {
108                this.ticketValidator = createTicketValidator();
109            }
110            return this.ticketValidator;
111        }
112        
113        protected TicketValidator createTicketValidator() {
114            String urlPrefix = getCasServerUrlPrefix();
115            if ("saml".equalsIgnoreCase(getValidationProtocol())) {
116                return new Saml11TicketValidator(urlPrefix);
117            }
118            return new Cas20ServiceTicketValidator(urlPrefix);
119        }
120        
121        /**
122         * Authenticates a user and retrieves its information.
123         * 
124         * @param token the authentication token
125         * @throws AuthenticationException if there is an error during authentication.
126         */
127        @Override
128        @SuppressWarnings("unchecked")
129        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
130            CasToken casToken = (CasToken) token;
131            if (token == null) {
132                return null;
133            }
134            
135            String ticket = (String)casToken.getCredentials();
136            if (!StringUtils.hasText(ticket)) {
137                return null;
138            }
139            
140            TicketValidator ticketValidator = ensureTicketValidator();
141    
142            try {
143                // contact CAS server to validate service ticket
144                Assertion casAssertion = ticketValidator.validate(ticket, getCasService());
145                // get principal, user id and attributes
146                AttributePrincipal casPrincipal = casAssertion.getPrincipal();
147                String userId = casPrincipal.getName();
148                log.debug("Validate ticket : {} in CAS server : {} to retrieve user : {}", new Object[]{
149                        ticket, getCasServerUrlPrefix(), userId
150                });
151    
152                Map<String, Object> attributes = casPrincipal.getAttributes();
153                // refresh authentication token (user id + remember me)
154                casToken.setUserId(userId);
155                String rememberMeAttributeName = getRememberMeAttributeName();
156                String rememberMeStringValue = (String)attributes.get(rememberMeAttributeName);
157                boolean isRemembered = rememberMeStringValue != null && Boolean.parseBoolean(rememberMeStringValue);
158                if (isRemembered) {
159                    casToken.setRememberMe(true);
160                }
161                // create simple authentication info
162                List<Object> principals = CollectionUtils.asList(userId, attributes);
163                PrincipalCollection principalCollection = new SimplePrincipalCollection(principals, getName());
164                return new SimpleAuthenticationInfo(principalCollection, ticket);
165            } catch (TicketValidationException e) { 
166                throw new CasAuthenticationException("Unable to validate ticket [" + ticket + "]", e);
167            }
168        }
169        
170        /**
171         * Retrieves the AuthorizationInfo for the given principals (the CAS previously authenticated user : id + attributes).
172         * 
173         * @param principals the primary identifying principals of the AuthorizationInfo that should be retrieved.
174         * @return the AuthorizationInfo associated with this principals.
175         */
176        @Override
177        @SuppressWarnings("unchecked")
178        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
179            // retrieve user information
180            SimplePrincipalCollection principalCollection = (SimplePrincipalCollection) principals;
181            List<Object> listPrincipals = principalCollection.asList();
182            Map<String, String> attributes = (Map<String, String>) listPrincipals.get(1);
183            // create simple authorization info
184            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
185            // add default roles
186            addRoles(simpleAuthorizationInfo, split(defaultRoles));
187            // add default permissions
188            addPermissions(simpleAuthorizationInfo, split(defaultPermissions));
189            // get roles from attributes
190            List<String> attributeNames = split(roleAttributeNames);
191            for (String attributeName : attributeNames) {
192                String value = attributes.get(attributeName);
193                addRoles(simpleAuthorizationInfo, split(value));
194            }
195            // get permissions from attributes
196            attributeNames = split(permissionAttributeNames);
197            for (String attributeName : attributeNames) {
198                String value = attributes.get(attributeName);
199                addPermissions(simpleAuthorizationInfo, split(value));
200            }
201            return simpleAuthorizationInfo;
202        }
203        
204        /**
205         * Split a string into a list of not empty and trimmed strings, delimiter is a comma.
206         * 
207         * @param s the input string
208         * @return the list of not empty and trimmed strings
209         */
210        private List<String> split(String s) {
211            List<String> list = new ArrayList<String>();
212            String[] elements = StringUtils.split(s, ',');
213            if (elements != null && elements.length > 0) {
214                for (String element : elements) {
215                    if (StringUtils.hasText(element)) {
216                        list.add(element.trim());
217                    }
218                }
219            }
220            return list;
221        }
222        
223        /**
224         * Add roles to the simple authorization info.
225         * 
226         * @param simpleAuthorizationInfo
227         * @param roles the list of roles to add
228         */
229        private void addRoles(SimpleAuthorizationInfo simpleAuthorizationInfo, List<String> roles) {
230            for (String role : roles) {
231                simpleAuthorizationInfo.addRole(role);
232            }
233        }
234        
235        /**
236         * Add permissions to the simple authorization info.
237         * 
238         * @param simpleAuthorizationInfo
239         * @param permissions the list of permissions to add
240         */
241        private void addPermissions(SimpleAuthorizationInfo simpleAuthorizationInfo, List<String> permissions) {
242            for (String permission : permissions) {
243                simpleAuthorizationInfo.addStringPermission(permission);
244            }
245        }
246    
247        public String getCasServerUrlPrefix() {
248            return casServerUrlPrefix;
249        }
250    
251        public void setCasServerUrlPrefix(String casServerUrlPrefix) {
252            this.casServerUrlPrefix = casServerUrlPrefix;
253        }
254    
255        public String getCasService() {
256            return casService;
257        }
258    
259        public void setCasService(String casService) {
260            this.casService = casService;
261        }
262    
263        public String getValidationProtocol() {
264            return validationProtocol;
265        }
266    
267        public void setValidationProtocol(String validationProtocol) {
268            this.validationProtocol = validationProtocol;
269        }
270    
271        public String getRememberMeAttributeName() {
272            return rememberMeAttributeName;
273        }
274    
275        public void setRememberMeAttributeName(String rememberMeAttributeName) {
276            this.rememberMeAttributeName = rememberMeAttributeName;
277        }
278    
279        public String getDefaultRoles() {
280            return defaultRoles;
281        }
282    
283        public void setDefaultRoles(String defaultRoles) {
284            this.defaultRoles = defaultRoles;
285        }
286    
287        public String getDefaultPermissions() {
288            return defaultPermissions;
289        }
290    
291        public void setDefaultPermissions(String defaultPermissions) {
292            this.defaultPermissions = defaultPermissions;
293        }
294    
295        public String getRoleAttributeNames() {
296            return roleAttributeNames;
297        }
298    
299        public void setRoleAttributeNames(String roleAttributeNames) {
300            this.roleAttributeNames = roleAttributeNames;
301        }
302    
303        public String getPermissionAttributeNames() {
304            return permissionAttributeNames;
305        }
306    
307        public void setPermissionAttributeNames(String permissionAttributeNames) {
308            this.permissionAttributeNames = permissionAttributeNames;
309        }
310    }