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