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}