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