001/*
002 *  Copyright 2017 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.plugins.extrausermgt.authentication.cas;
017
018import java.util.ArrayList;
019import java.util.HashMap;
020import java.util.List;
021import java.util.Map;
022
023import javax.servlet.ServletContext;
024
025import org.apache.avalon.framework.context.Context;
026import org.apache.avalon.framework.context.ContextException;
027import org.apache.avalon.framework.context.Contextualizable;
028import org.apache.cocoon.components.ContextHelper;
029import org.apache.cocoon.environment.ObjectModelHelper;
030import org.apache.cocoon.environment.Redirector;
031import org.apache.cocoon.environment.Request;
032import org.apache.cocoon.environment.Session;
033import org.apache.cocoon.environment.http.HttpEnvironment;
034import org.apache.commons.lang3.StringUtils;
035import org.apache.commons.lang3.Strings;
036import org.jasig.cas.client.authentication.AuthenticationFilter;
037import org.jasig.cas.client.util.AbstractCasFilter;
038import org.jasig.cas.client.util.HttpServletRequestWrapperFilter;
039import org.jasig.cas.client.validation.Assertion;
040
041import org.ametys.core.authentication.AbstractCredentialProvider;
042import org.ametys.core.authentication.AuthenticateAction;
043import org.ametys.core.authentication.BlockingCredentialProvider;
044import org.ametys.core.authentication.NonBlockingCredentialProvider;
045import org.ametys.core.servletwrapper.filter.ServletFilterWrapper;
046import org.ametys.core.user.UserIdentity;
047import org.ametys.core.util.URIUtils;
048
049/**
050 * This manager gets the credentials given by an authentication CAS filter.
051 * <br>
052 * The filter must set the 'remote user' header into the request. <br>
053 * <br>
054 * This manager can not get the password of the connected user: the user is
055 * already authenticated. This manager should not be associated with a
056 * <code>UsersManagerAuthentication</code>
057 */
058public class CASCredentialProvider extends AbstractCredentialProvider implements NonBlockingCredentialProvider, BlockingCredentialProvider, Contextualizable
059{
060    /** Parameter name for server url  */
061    public static final String PARAM_SERVER_URL = "authentication.cas.serverUrl";
062    
063    /** Parameter name for "request proxy tickets" */
064    private static final String __PARAM_REQUEST_PROXY_TICKETS = "authentication.cas.requestProxyTickets";
065    
066    /** Parameter name for "accept any proxy" */
067    private static final String __PARAM_ACCEPT_ANY_PROXY = "authentication.cas.acceptAnyProxy";
068    
069    /** Parameter name for authorized proxy chains */
070    private static final String __PARAM_AUTHORIZED_PROXY_CHAINS = "authentication.cas.authorizedProxyChain";
071    
072    /** Parameter name for the gateway mode */
073    private static final String __PARAM_GATEWAY_ENABLED = "authentication.cas.enableGateway";
074    
075    /** Cas server URL with context (https://cas-server ou https://cas-server/cas) */
076    protected String _serverUrl;
077    
078    private Context _context;
079
080    /** Should the application request proxy tickets */
081    private boolean _requestProxyTickets;
082    /** Should the application accept any proxy */
083    private boolean _acceptAnyProxy;
084    /**
085     * Authorized proxy chains, which is
086     *  a newline-delimited list of acceptable proxy chains.
087     *  A proxy chain includes a whitespace-delimited list of valid proxy URLs.
088     *  Only one proxy chain needs to match for the login to be successful.
089     */
090    private String _authorizedProxyChains;
091    /** Should the cas gateway mode be used */
092    private boolean _gatewayModeEnabled;
093
094    
095    @Override
096    public void contextualize(Context context) throws ContextException
097    {
098        _context = context;
099    }
100    
101    @Override
102    public void init(String id, String cpModelId, Map<String, Object> paramValues, String label) throws Exception
103    {
104        super.init(id, cpModelId, paramValues, label);
105        _serverUrl = (String) paramValues.get(PARAM_SERVER_URL);
106        _requestProxyTickets = (boolean) paramValues.get(__PARAM_REQUEST_PROXY_TICKETS);
107        _acceptAnyProxy = (boolean) paramValues.get(__PARAM_ACCEPT_ANY_PROXY);
108        _authorizedProxyChains = (String) paramValues.get(__PARAM_AUTHORIZED_PROXY_CHAINS);
109        _gatewayModeEnabled = (boolean) paramValues.get(__PARAM_GATEWAY_ENABLED);
110    }
111
112    @Override
113    public boolean blockingIsStillConnected(UserIdentity userIdentity, Redirector redirector) throws Exception
114    {
115        return Strings.CS.equals(userIdentity.getLogin(), _getLoginFromFilter(false, redirector));
116    }
117    
118    @Override
119    public boolean nonBlockingIsStillConnected(UserIdentity userIdentity, Redirector redirector) throws Exception
120    {
121        return blockingIsStillConnected(userIdentity, redirector);
122    }
123    
124    private String _getLoginFromFilter(boolean gateway, Redirector redirector) throws Exception
125    {
126        Map objectModel = ContextHelper.getObjectModel(_context);
127        Request request = ObjectModelHelper.getRequest(objectModel);
128        
129        if (request.getRequestURI().startsWith(request.getContextPath() + "/plugins/core/authenticate/") && "true".equals(request.getParameter("proxy")))
130        {
131            String redirectUrl = "cocoon://_plugins/extra-user-management/ametysCasProxy";
132            getLogger().debug("Redirecting to '{}'", redirector);
133            redirector.redirect(true, redirectUrl);
134            return null;
135        }
136        
137        StringBuffer serverName = new StringBuffer(request.getServerName());
138        
139        // Build an URI without :80 (http) and without :443 (https)
140        if (request.isSecure())
141        {
142            if (request.getServerPort() != 443)
143            {
144                serverName.append(":");
145                serverName.append(request.getServerPort());
146            }
147        }
148        else
149        {
150            if (request.getServerPort() != 80)
151            {
152                serverName.append(":");
153                serverName.append(request.getServerPort());
154            }
155        }
156        
157        String name = serverName.toString();
158        
159        // Create the filter chain.
160        List<ServletFilterWrapper> runtimeFilters = new ArrayList<>();
161        
162        ServletContext servletContext = (ServletContext) objectModel.get(HttpEnvironment.HTTP_SERVLET_CONTEXT);
163        Map<String, String> parameters = new HashMap<>();
164        
165        try
166        {
167            // Authentication filter.
168            parameters.put("casServerLoginUrl", _serverUrl + "/login");
169            parameters.put("serverName", name);
170            parameters.put("gateway", String.valueOf(gateway));
171            ServletFilterWrapper runtimeFilter = new ServletFilterWrapper(new AuthenticationFilter());
172            runtimeFilter.init(parameters, servletContext);
173            runtimeFilters.add(runtimeFilter);
174            
175            // Ticket validation filter.
176            parameters.clear();
177            parameters.put("casServerUrlPrefix", _serverUrl);
178            parameters.put("serverName", name);
179            if (_acceptAnyProxy)
180            {
181                parameters.put("acceptAnyProxy", "true");
182            }
183            else
184            {
185                parameters.put("allowedProxyChains", _authorizedProxyChains);
186            }
187            
188            if (_requestProxyTickets && StringUtils.isNotEmpty(request.getParameter("ticket")))
189            {
190                String proxyCallbackUrl = "https://" + name + _getProxyCallbackRelativeUrl(request);
191                getLogger().debug("The computed proxy callback url is: {}", proxyCallbackUrl);
192                parameters.put("proxyCallbackUrl", proxyCallbackUrl);
193                parameters.put("proxyGrantingTicketStorageClass", CasProxyGrantingTicketManager.class.getName());
194                parameters.put("ticketValidatorClass", AmetysCas20ProxyTicketValidator.class.getName());
195            }
196            
197            runtimeFilter = new ServletFilterWrapper(new AmetysCas20ProxyReceivingTicketValidationFilter());
198            runtimeFilter.init(parameters, servletContext);
199            runtimeFilters.add(runtimeFilter);
200            
201            // Ticket validation filter.
202            parameters.clear();
203            runtimeFilter = new ServletFilterWrapper(new HttpServletRequestWrapperFilter());
204            runtimeFilter.init(parameters, servletContext);
205            runtimeFilters.add(runtimeFilter);
206            
207            getLogger().debug("Executing CAS filter chain...");
208            
209            // Execute the filter chain.
210            for (ServletFilterWrapper filter : runtimeFilters)
211            {
212                filter.doFilter(objectModel, redirector);
213            }
214        }
215        finally
216        {
217            getLogger().debug("Destroying CAS filter chain...");
218            
219            // Release filters
220            for (ServletFilterWrapper filter : runtimeFilters)
221            {
222                filter.destroy();
223            }
224        }
225        
226        // If a redirect was sent, the getSession call won't work.
227        if (!redirector.hasRedirected())
228        {
229            return _getLogin(request);
230        }
231        
232        return null;
233    }
234    
235    private String _getProxyCallbackRelativeUrl(Request request)
236    {
237        Map<String, String> params = new HashMap<>();
238        params.put("proxy", "true");
239        
240        String userPopulationId = (String) request.getAttribute(AuthenticateAction.REQUEST_ATTRIBUTE_USER_POPULATION_ID);
241        if (StringUtils.isNotEmpty(userPopulationId))
242        {
243            params.put(AuthenticateAction.REQUEST_PARAMETER_POPULATION_NAME, userPopulationId);
244        }
245        
246        Integer cpIndex = _getRunningCpIndex(request);
247        if (cpIndex.equals(-1))
248        {
249            // Force to authenticate over the first credential provider
250            cpIndex = 0;
251        }
252        
253        @SuppressWarnings("unchecked")
254        List<String> contexts = (List<String>) request.getAttribute("Runtime:Contexts");
255        
256        String contextAsString = contexts != null ? StringUtils.join(contexts.toArray(), ',') : "";
257        params.put("contexts", contextAsString);
258        
259        return URIUtils.buildURI(request.getContextPath() + "/plugins/core/authenticate/" + cpIndex.toString(), params);
260    }
261    
262    private Integer _getRunningCpIndex(Request request)
263    {
264        Integer cpIndex = (Integer) request.getAttribute("Runtime:RequestCredentialProviderIndex");
265        if (cpIndex != null)
266        {
267            return cpIndex;
268        }
269        
270        Session session = request.getSession(false);
271        if (session != null)
272        {
273            Integer formerRunningCredentialProviderIndex = (Integer) session.getAttribute("Runtime:ConnectingCredentialProviderIndexLastKnown");
274            if (formerRunningCredentialProviderIndex != null)
275            {
276                return formerRunningCredentialProviderIndex;
277            }
278        }
279        
280        return -1;
281    }
282
283    @Override
284    public boolean blockingGrantAnonymousRequest()
285    {       
286        return false;
287    }
288    
289    @Override
290    public boolean nonBlockingGrantAnonymousRequest()
291    {
292        return false;
293    }
294
295    @Override
296    public UserIdentity blockingGetUserIdentity(Redirector redirector) throws Exception
297    {
298        String userLogin = _getLoginFromFilter(false, redirector);
299        
300        if (redirector.hasRedirected())
301        {
302            return null;
303        }
304
305        if (userLogin == null)
306        {
307            throw new IllegalStateException("CAS authentication needs a CAS filter.");
308        }
309        
310        return new UserIdentity(userLogin, null);
311    }
312    
313    @Override
314    public UserIdentity nonBlockingGetUserIdentity(Redirector redirector) throws Exception
315    {
316        if (!_gatewayModeEnabled)
317        {
318            return null;
319        }
320        
321        String userLogin = _getLoginFromFilter(true, redirector);
322        if (userLogin == null)
323        {
324            return null;
325        }
326        
327        return new UserIdentity(userLogin, null);
328    }
329
330    @Override
331    public void blockingUserNotAllowed(Redirector redirector) throws Exception
332    {
333        // Nothing to do.
334    }
335    
336    @Override
337    public void nonBlockingUserNotAllowed(Redirector redirector) throws Exception
338    {
339        // Nothing to do.
340    }
341
342    @Override
343    public void blockingUserAllowed(UserIdentity userIdentity, Redirector redirector)
344    {
345        // Empty method, nothing more to do.
346    }
347    
348    @Override
349    public void nonBlockingUserAllowed(UserIdentity userIdentity, Redirector redirector)
350    {
351        // Empty method, nothing more to do.
352    }
353
354    public boolean requiresNewWindow()
355    {
356        return true;
357    }
358    
359    /**
360     * Get the connected user login from the request or session.
361     * @param request the request object.
362     * @return the connected user login or null.
363     */
364    protected String _getLogin(Request request)
365    {
366        String userLogin = null;
367        
368        Session session = request.getSession(false);
369        
370        final Assertion assertion = (Assertion) (session == null ? request.getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION) : session.getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION));
371        
372        if (assertion != null)
373        {
374            userLogin = assertion.getPrincipal().getName();
375        }
376        return userLogin;
377    }
378}