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 */
019package org.apache.shiro.cas;
020
021import org.apache.shiro.authc.AuthenticationException;
022import org.apache.shiro.authc.AuthenticationInfo;
023import org.apache.shiro.authc.AuthenticationToken;
024import org.apache.shiro.authc.SimpleAuthenticationInfo;
025import org.apache.shiro.authz.AuthorizationInfo;
026import org.apache.shiro.authz.SimpleAuthorizationInfo;
027import org.apache.shiro.realm.AuthorizingRealm;
028import org.apache.shiro.subject.PrincipalCollection;
029import org.apache.shiro.subject.SimplePrincipalCollection;
030import org.apache.shiro.util.CollectionUtils;
031import org.apache.shiro.util.StringUtils;
032import org.jasig.cas.client.authentication.AttributePrincipal;
033import org.jasig.cas.client.validation.*;
034import org.slf4j.Logger;
035import org.slf4j.LoggerFactory;
036
037import java.util.ArrayList;
038import java.util.List;
039import 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 */
058public 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}