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}