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 * @see <a href="https://github.com/bujiio/buji-pac4j">buji-pac4j</a>
058 * @deprecated replaced with Shiro integration in <a href="https://github.com/bujiio/buji-pac4j">buji-pac4j</a>.
059 */
060@Deprecated
061public class CasRealm extends AuthorizingRealm {
062
063    // default name of the CAS attribute for remember me authentication (CAS 3.4.10+)
064    public static final String DEFAULT_REMEMBER_ME_ATTRIBUTE_NAME = "longTermAuthenticationRequestTokenUsed";
065    public static final String DEFAULT_VALIDATION_PROTOCOL = "CAS";
066    
067    private static Logger log = LoggerFactory.getLogger(CasRealm.class);
068    
069    // this is the url of the CAS server (example : http://host:port/cas)
070    private String casServerUrlPrefix;
071    
072    // this is the CAS service url of the application (example : http://host:port/mycontextpath/shiro-cas)
073    private String casService;
074    
075    /* CAS protocol to use for ticket validation : CAS (default) or SAML :
076       - 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)
077       - 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
078    */
079    private String validationProtocol = DEFAULT_VALIDATION_PROTOCOL;
080    
081    // default name of the CAS attribute for remember me authentication (CAS 3.4.10+)
082    private String rememberMeAttributeName = DEFAULT_REMEMBER_ME_ATTRIBUTE_NAME;
083    
084    // this class from the CAS client is used to validate a service ticket on CAS server
085    private TicketValidator ticketValidator;
086    
087    // default roles to applied to authenticated user
088    private String defaultRoles;
089    
090    // default permissions to applied to authenticated user
091    private String defaultPermissions;
092    
093    // names of attributes containing roles
094    private String roleAttributeNames;
095    
096    // names of attributes containing permissions
097    private String permissionAttributeNames;
098    
099    public CasRealm() {
100        setAuthenticationTokenClass(CasToken.class);
101    }
102
103    @Override
104    protected void onInit() {
105        super.onInit();
106        ensureTicketValidator();
107    }
108
109    protected TicketValidator ensureTicketValidator() {
110        if (this.ticketValidator == null) {
111            this.ticketValidator = createTicketValidator();
112        }
113        return this.ticketValidator;
114    }
115    
116    protected TicketValidator createTicketValidator() {
117        String urlPrefix = getCasServerUrlPrefix();
118        if ("saml".equalsIgnoreCase(getValidationProtocol())) {
119            return new Saml11TicketValidator(urlPrefix);
120        }
121        return new Cas20ServiceTicketValidator(urlPrefix);
122    }
123    
124    /**
125     * Authenticates a user and retrieves its information.
126     * 
127     * @param token the authentication token
128     * @throws AuthenticationException if there is an error during authentication.
129     */
130    @Override
131    @SuppressWarnings("unchecked")
132    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
133        CasToken casToken = (CasToken) token;
134        if (token == null) {
135            return null;
136        }
137        
138        String ticket = (String)casToken.getCredentials();
139        if (!StringUtils.hasText(ticket)) {
140            return null;
141        }
142        
143        TicketValidator ticketValidator = ensureTicketValidator();
144
145        try {
146            // contact CAS server to validate service ticket
147            Assertion casAssertion = ticketValidator.validate(ticket, getCasService());
148            // get principal, user id and attributes
149            AttributePrincipal casPrincipal = casAssertion.getPrincipal();
150            String userId = casPrincipal.getName();
151            log.debug("Validate ticket : {} in CAS server : {} to retrieve user : {}", new Object[]{
152                    ticket, getCasServerUrlPrefix(), userId
153            });
154
155            Map<String, Object> attributes = casPrincipal.getAttributes();
156            // refresh authentication token (user id + remember me)
157            casToken.setUserId(userId);
158            String rememberMeAttributeName = getRememberMeAttributeName();
159            String rememberMeStringValue = (String)attributes.get(rememberMeAttributeName);
160            boolean isRemembered = rememberMeStringValue != null && Boolean.parseBoolean(rememberMeStringValue);
161            if (isRemembered) {
162                casToken.setRememberMe(true);
163            }
164            // create simple authentication info
165            List<Object> principals = CollectionUtils.asList(userId, attributes);
166            PrincipalCollection principalCollection = new SimplePrincipalCollection(principals, getName());
167            return new SimpleAuthenticationInfo(principalCollection, ticket);
168        } catch (TicketValidationException e) { 
169            throw new CasAuthenticationException("Unable to validate ticket [" + ticket + "]", e);
170        }
171    }
172    
173    /**
174     * Retrieves the AuthorizationInfo for the given principals (the CAS previously authenticated user : id + attributes).
175     * 
176     * @param principals the primary identifying principals of the AuthorizationInfo that should be retrieved.
177     * @return the AuthorizationInfo associated with this principals.
178     */
179    @Override
180    @SuppressWarnings("unchecked")
181    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
182        // retrieve user information
183        SimplePrincipalCollection principalCollection = (SimplePrincipalCollection) principals;
184        List<Object> listPrincipals = principalCollection.asList();
185        Map<String, String> attributes = (Map<String, String>) listPrincipals.get(1);
186        // create simple authorization info
187        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
188        // add default roles
189        addRoles(simpleAuthorizationInfo, split(defaultRoles));
190        // add default permissions
191        addPermissions(simpleAuthorizationInfo, split(defaultPermissions));
192        // get roles from attributes
193        List<String> attributeNames = split(roleAttributeNames);
194        for (String attributeName : attributeNames) {
195            String value = attributes.get(attributeName);
196            addRoles(simpleAuthorizationInfo, split(value));
197        }
198        // get permissions from attributes
199        attributeNames = split(permissionAttributeNames);
200        for (String attributeName : attributeNames) {
201            String value = attributes.get(attributeName);
202            addPermissions(simpleAuthorizationInfo, split(value));
203        }
204        return simpleAuthorizationInfo;
205    }
206    
207    /**
208     * Split a string into a list of not empty and trimmed strings, delimiter is a comma.
209     * 
210     * @param s the input string
211     * @return the list of not empty and trimmed strings
212     */
213    private List<String> split(String s) {
214        List<String> list = new ArrayList<String>();
215        String[] elements = StringUtils.split(s, ',');
216        if (elements != null && elements.length > 0) {
217            for (String element : elements) {
218                if (StringUtils.hasText(element)) {
219                    list.add(element.trim());
220                }
221            }
222        }
223        return list;
224    }
225    
226    /**
227     * Add roles to the simple authorization info.
228     * 
229     * @param simpleAuthorizationInfo
230     * @param roles the list of roles to add
231     */
232    private void addRoles(SimpleAuthorizationInfo simpleAuthorizationInfo, List<String> roles) {
233        for (String role : roles) {
234            simpleAuthorizationInfo.addRole(role);
235        }
236    }
237    
238    /**
239     * Add permissions to the simple authorization info.
240     * 
241     * @param simpleAuthorizationInfo
242     * @param permissions the list of permissions to add
243     */
244    private void addPermissions(SimpleAuthorizationInfo simpleAuthorizationInfo, List<String> permissions) {
245        for (String permission : permissions) {
246            simpleAuthorizationInfo.addStringPermission(permission);
247        }
248    }
249
250    public String getCasServerUrlPrefix() {
251        return casServerUrlPrefix;
252    }
253
254    public void setCasServerUrlPrefix(String casServerUrlPrefix) {
255        this.casServerUrlPrefix = casServerUrlPrefix;
256    }
257
258    public String getCasService() {
259        return casService;
260    }
261
262    public void setCasService(String casService) {
263        this.casService = casService;
264    }
265
266    public String getValidationProtocol() {
267        return validationProtocol;
268    }
269
270    public void setValidationProtocol(String validationProtocol) {
271        this.validationProtocol = validationProtocol;
272    }
273
274    public String getRememberMeAttributeName() {
275        return rememberMeAttributeName;
276    }
277
278    public void setRememberMeAttributeName(String rememberMeAttributeName) {
279        this.rememberMeAttributeName = rememberMeAttributeName;
280    }
281
282    public String getDefaultRoles() {
283        return defaultRoles;
284    }
285
286    public void setDefaultRoles(String defaultRoles) {
287        this.defaultRoles = defaultRoles;
288    }
289
290    public String getDefaultPermissions() {
291        return defaultPermissions;
292    }
293
294    public void setDefaultPermissions(String defaultPermissions) {
295        this.defaultPermissions = defaultPermissions;
296    }
297
298    public String getRoleAttributeNames() {
299        return roleAttributeNames;
300    }
301
302    public void setRoleAttributeNames(String roleAttributeNames) {
303        this.roleAttributeNames = roleAttributeNames;
304    }
305
306    public String getPermissionAttributeNames() {
307        return permissionAttributeNames;
308    }
309
310    public void setPermissionAttributeNames(String permissionAttributeNames) {
311        this.permissionAttributeNames = permissionAttributeNames;
312    }
313}