View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.shiro.cas;
20  
21  import org.apache.shiro.authc.AuthenticationException;
22  import org.apache.shiro.authc.AuthenticationInfo;
23  import org.apache.shiro.authc.AuthenticationToken;
24  import org.apache.shiro.authc.SimpleAuthenticationInfo;
25  import org.apache.shiro.authz.AuthorizationInfo;
26  import org.apache.shiro.authz.SimpleAuthorizationInfo;
27  import org.apache.shiro.realm.AuthorizingRealm;
28  import org.apache.shiro.subject.PrincipalCollection;
29  import org.apache.shiro.subject.SimplePrincipalCollection;
30  import org.apache.shiro.util.CollectionUtils;
31  import org.apache.shiro.util.StringUtils;
32  import org.jasig.cas.client.authentication.AttributePrincipal;
33  import org.jasig.cas.client.validation.*;
34  import org.slf4j.Logger;
35  import org.slf4j.LoggerFactory;
36  
37  import java.util.ArrayList;
38  import java.util.List;
39  import java.util.Map;
40  
41  /**
42   * This realm implementation acts as a CAS client to a CAS server for authentication and basic authorization.
43   * <p/>
44   * This realm functions by inspecting a submitted {@link org.apache.shiro.cas.CasToken CasToken} (which essentially 
45   * wraps a CAS service ticket) and validates it against the CAS server using a configured CAS
46   * {@link org.jasig.cas.client.validation.TicketValidator TicketValidator}.
47   * <p/>
48   * The {@link #getValidationProtocol() validationProtocol} is {@code CAS} by default, which indicates that a
49   * a {@link org.jasig.cas.client.validation.Cas20ServiceTicketValidator Cas20ServiceTicketValidator}
50   * will be used for ticket validation.  You can alternatively set
51   * or {@link org.jasig.cas.client.validation.Saml11TicketValidator Saml11TicketValidator} of CAS client. It is based on
52   * {@link AuthorizingRealm AuthorizingRealm} for both authentication and authorization. User id and attributes are retrieved from the CAS
53   * service ticket validation response during authentication phase. Roles and permissions are computed during authorization phase (according
54   * to the attributes previously retrieved).
55   *
56   * @since 1.2
57   */
58  public class CasRealm extends AuthorizingRealm {
59  
60      // default name of the CAS attribute for remember me authentication (CAS 3.4.10+)
61      public static final String DEFAULT_REMEMBER_ME_ATTRIBUTE_NAME = "longTermAuthenticationRequestTokenUsed";
62      public static final String DEFAULT_VALIDATION_PROTOCOL = "CAS";
63      
64      private static Logger log = LoggerFactory.getLogger(CasRealm.class);
65      
66      // this is the url of the CAS server (example : http://host:port/cas)
67      private String casServerUrlPrefix;
68      
69      // this is the CAS service url of the application (example : http://host:port/mycontextpath/shiro-cas)
70      private String casService;
71      
72      /* CAS protocol to use for ticket validation : CAS (default) or SAML :
73         - 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)
74         - 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
75      */
76      private String validationProtocol = DEFAULT_VALIDATION_PROTOCOL;
77      
78      // default name of the CAS attribute for remember me authentication (CAS 3.4.10+)
79      private String rememberMeAttributeName = DEFAULT_REMEMBER_ME_ATTRIBUTE_NAME;
80      
81      // this class from the CAS client is used to validate a service ticket on CAS server
82      private TicketValidator ticketValidator;
83      
84      // default roles to applied to authenticated user
85      private String defaultRoles;
86      
87      // default permissions to applied to authenticated user
88      private String defaultPermissions;
89      
90      // names of attributes containing roles
91      private String roleAttributeNames;
92      
93      // names of attributes containing permissions
94      private String permissionAttributeNames;
95      
96      public CasRealm() {
97          setAuthenticationTokenClass(CasToken.class);
98      }
99  
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 }