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}