/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.sling.auth.core.impl; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Collection; import java.util.Dictionary; import java.util.HashMap; import java.util.Hashtable; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import javax.jcr.SimpleCredentials; import javax.security.auth.login.AccountLockedException; import javax.security.auth.login.AccountNotFoundException; import javax.security.auth.login.CredentialExpiredException; import javax.servlet.ServletRequest; import javax.servlet.ServletRequestEvent; import javax.servlet.ServletRequestListener; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.felix.scr.annotations.Activate; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.Deactivate; import org.apache.felix.scr.annotations.Modified; import org.apache.felix.scr.annotations.Properties; import org.apache.felix.scr.annotations.Property; import org.apache.felix.scr.annotations.PropertyOption; import org.apache.felix.scr.annotations.PropertyUnbounded; import org.apache.felix.scr.annotations.Reference; import org.apache.felix.scr.annotations.ReferencePolicy; import org.apache.felix.scr.annotations.Service; import org.apache.sling.api.SlingConstants; import org.apache.sling.api.auth.Authenticator; import org.apache.sling.api.auth.NoAuthenticationHandlerException; import org.apache.sling.api.resource.LoginException; import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.api.resource.ResourceResolverFactory; import org.apache.sling.auth.core.AuthConstants; import org.apache.sling.auth.core.AuthUtil; import org.apache.sling.auth.core.AuthenticationSupport; import org.apache.sling.auth.core.impl.engine.EngineAuthenticationHandlerHolder; import org.apache.sling.auth.core.spi.AbstractAuthenticationHandler; import org.apache.sling.auth.core.spi.AuthenticationFeedbackHandler; import org.apache.sling.auth.core.spi.AuthenticationHandler; import org.apache.sling.auth.core.spi.AuthenticationInfo; import org.apache.sling.auth.core.spi.AuthenticationInfoPostProcessor; import org.apache.sling.auth.core.spi.DefaultAuthenticationFeedbackHandler; import org.apache.sling.commons.osgi.PropertiesUtil; import org.osgi.framework.BundleContext; import org.osgi.framework.Constants; import org.osgi.framework.ServiceReference; import org.osgi.framework.ServiceRegistration; import org.osgi.service.event.Event; import org.osgi.service.event.EventAdmin; import org.osgi.service.http.context.ServletContextHelper; import org.osgi.service.http.whiteboard.HttpWhiteboardConstants; import org.osgi.util.tracker.ServiceTracker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The <code>SlingAuthenticator</code> class is the default implementation for * handling authentication. This class supports : * <ul> * <li>Support for login sessions where session ids are exchanged with cookies * <li>Support for multiple authentication handlers, which must implement the * {@link AuthenticationHandler} interface. * <li> * </ul> * <p> * Currently this class does not support multiple handlers for any one request * URL. */ @Component(name = "org.apache.sling.engine.impl.auth.SlingAuthenticator", label = "%auth.name", description = "%auth.description", metatype = true) @Service(value = { Authenticator.class, AuthenticationSupport.class, ServletRequestListener.class }) @Properties({ @Property(name = HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_SELECT, value = "(" + HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_NAME + "=*)"), @Property(name = HttpWhiteboardConstants.HTTP_WHITEBOARD_LISTENER, value = "true"), @Property(name = Constants.SERVICE_VENDOR, value = "The Apache Software Foundation") }) public class SlingAuthenticator implements Authenticator, AuthenticationSupport, ServletRequestListener { /** default log */ private final Logger log = LoggerFactory.getLogger(SlingAuthenticator.class); @Property(name = Constants.SERVICE_DESCRIPTION) static final String DESCRIPTION = "Apache Sling Request Authenticator"; /** The default impersonation cookie name */ private static final String DEFAULT_IMPERSONATION_COOKIE = "sling.sudo"; @Property(value = DEFAULT_IMPERSONATION_COOKIE) public static final String PAR_IMPERSONATION_COOKIE_NAME = "auth.sudo.cookie"; /** The default impersonation parameter name */ private static final String DEFAULT_IMPERSONATION_PARAMETER = "sudo"; @Property(value = DEFAULT_IMPERSONATION_PARAMETER) public static final String PAR_IMPERSONATION_PAR_NAME = "auth.sudo.parameter"; /** The default value for allowing anonymous access */ private static final boolean DEFAULT_ANONYMOUS_ALLOWED = true; @Property(boolValue = DEFAULT_ANONYMOUS_ALLOWED) public static final String PAR_ANONYMOUS_ALLOWED = "auth.annonymous"; @Property(cardinality = 2147483647) private static final String PAR_AUTH_REQ = AuthConstants.AUTH_REQUIREMENTS; @Property() private static final String PAR_ANONYMOUS_USER = "sling.auth.anonymous.user"; @Property() // TODO: This should be a PASSWORD type private static final String PAR_ANONYMOUS_PASSWORD = "sling.auth.anonymous.password"; /** * Value of the {@link #PAR_HTTP_AUTH} property to fully enable the built-in * HTTP Authentication Handler (value is "enabled"). */ private static final String HTTP_AUTH_ENABLED = "enabled"; /** * Value of the {@link #PAR_HTTP_AUTH} property to completely disable the * built-in HTTP Authentication Handler (value is "disabled"). */ private static final String HTTP_AUTH_DISABLED = "disabled"; /** * Value of the {@link #PAR_HTTP_AUTH} property to enable extracting the * credentials if the HTTP Basic authentication header is present (value is * "preemptive"). In <i>preemptive</i> mode, though, the * <code>requestCredentials</code> and <code>dropCredentials</code> methods * will not send back a 401 response. */ private static final String HTTP_AUTH_PREEMPTIVE = "preemptive"; @Property(value = HTTP_AUTH_PREEMPTIVE, options = { @PropertyOption(name = HTTP_AUTH_ENABLED, value = "Enabled"), @PropertyOption(name = HTTP_AUTH_PREEMPTIVE, value = "Enabled (Preemptive)"), @PropertyOption(name = HTTP_AUTH_DISABLED, value = "Disabled") }) private static final String PAR_HTTP_AUTH = "auth.http"; /** * The default realm for the built-in HTTP Basic authentication handler. */ private static final String DEFAULT_REALM = "Sling (Development)"; /** * The name of the configuration property used to set the Realm of the * built-in HTTP Basic authentication handler. */ @Property(value = DEFAULT_REALM) public static final String PAR_REALM_NAME = "auth.http.realm"; /** * Default request URI suffix to expect to be handled by authentication * handlers and not expecting to cause * {@link #handleSecurity(HttpServletRequest, HttpServletResponse)} to * return <code>true</code>. */ private static final String DEFAULT_AUTH_URI_SUFFIX = "/j_security_check"; /** * The name of the form submission parameter providing the new password of * the user (value is "j_newpassword"). */ private static final String PAR_NEW_PASSWORD = "j_newpassword"; /** * The name of the configuration property used to set a (potentially * empty) list of request URI suffixes intended to be handled by * authentication handlers. */ @Property(value = DEFAULT_AUTH_URI_SUFFIX, unbounded = PropertyUnbounded.ARRAY) public static final String PAR_AUTH_URI_SUFFIX = "auth.uri.suffix"; /** * The name of the {@link AuthenticationInfo} property providing the option * {@link org.apache.sling.auth.core.spi.AuthenticationFeedbackHandler} * handler to be called back on login failure or success. */ private static final String AUTH_INFO_PROP_FEEDBACK_HANDLER = "$$sling.auth.AuthenticationFeedbackHandler$$"; @Reference private ResourceResolverFactory resourceResolverFactory; private PathBasedHolderCache<AbstractAuthenticationHandlerHolder> authHandlerCache = new PathBasedHolderCache<AbstractAuthenticationHandlerHolder>(); // package protected for access in inner class ... private final PathBasedHolderCache<AuthenticationRequirementHolder> authRequiredCache = new PathBasedHolderCache<AuthenticationRequirementHolder>(); /** The name of the impersonation parameter */ private String sudoParameterName; /** The name of the impersonation cookie */ private String sudoCookieName; /** Cache control flag */ private boolean cacheControl; /** * The configured URI suffices indicating a authentication requests and * requiring redirects and thus returning <code>false</code> from the * #handleSecurity method. * <p> * This will be <code>null</code> if there are no suffices to consider. */ private String[] authUriSuffices; /** * The name of the user to assume for anonymous access. By default this is * <code>null</code> to use <code>null</code> credentials and thus use the * system provided identification. * * @see #getAnonymousCredentials() */ private String anonUser; /** * The password to use for anonymous access. This property is only used if * the {@link #anonUser} field is not <code>null</code>. * * @see #getAnonymousCredentials() */ private char[] anonPassword; /** HTTP Basic authentication handler */ private HttpBasicAuthenticationHandler httpBasicHandler; /** Web Console Plugin service registration */ private ServiceRegistration webConsolePlugin; /** * The listener for services registered with "sling.auth.requirements" to * update the internal authentication requirements */ private SlingAuthenticatorServiceListener serviceListener; /** * ServiceTracker tracking AuthenticationHandler services */ private ServiceTracker authHandlerTracker; /** * ServiceTracker tracking old Sling Engine AuthenticationHandler services */ private ServiceTracker engineAuthHandlerTracker; /** * ServiceTracker tracking AuthenticationInfoPostProcessor services */ private ServiceTracker authInfoPostProcessorTracker; /** * The event admin service. */ @Reference(policy=ReferencePolicy.DYNAMIC) private volatile EventAdmin eventAdmin; // ---------- SCR integration @SuppressWarnings("unused") @Activate private void activate(final BundleContext bundleContext, final Map<String, Object> properties) { modified(properties); AuthenticatorWebConsolePlugin plugin = new AuthenticatorWebConsolePlugin( this); Hashtable<String, Object> props = new Hashtable<String, Object>(); props.put("felix.webconsole.label", plugin.getLabel()); props.put("felix.webconsole.title", plugin.getTitle()); props.put("felix.webconsole.category", "Sling"); props.put(Constants.SERVICE_DESCRIPTION, "Sling Request Authenticator WebConsole Plugin"); props.put(Constants.SERVICE_VENDOR, properties.get(Constants.SERVICE_VENDOR)); webConsolePlugin = bundleContext.registerService( "javax.servlet.Servlet", plugin, props); serviceListener = SlingAuthenticatorServiceListener.createListener( bundleContext, this.authRequiredCache); authHandlerTracker = new AuthenticationHandlerTracker(bundleContext, authHandlerCache); engineAuthHandlerTracker = new EngineAuthenticationHandlerTracker( bundleContext, authHandlerCache); authInfoPostProcessorTracker = new ServiceTracker(bundleContext, AuthenticationInfoPostProcessor.SERVICE_NAME, null); authInfoPostProcessorTracker.open(); } @Modified private void modified(Map<String, Object> properties) { if (properties == null) { properties = new HashMap<String, Object>(); } String newCookie = (String) properties.get(PAR_IMPERSONATION_COOKIE_NAME); if (newCookie == null || newCookie.length() == 0) { newCookie = DEFAULT_IMPERSONATION_COOKIE; } if (!newCookie.equals(this.sudoCookieName)) { log.info( "modified: Setting new cookie name for impersonation {} (was {})", newCookie, this.sudoCookieName); this.sudoCookieName = newCookie; } String newPar = (String) properties.get(PAR_IMPERSONATION_PAR_NAME); if (newPar == null || newPar.length() == 0) { newPar = DEFAULT_IMPERSONATION_PARAMETER; } if (!newPar.equals(this.sudoParameterName)) { log.info( "modified: Setting new parameter name for impersonation {} (was {})", newPar, this.sudoParameterName); this.sudoParameterName = newPar; } authRequiredCache.clear(); final boolean anonAllowed = PropertiesUtil.toBoolean(properties.get(PAR_ANONYMOUS_ALLOWED), DEFAULT_ANONYMOUS_ALLOWED); authRequiredCache.addHolder(new AuthenticationRequirementHolder("/", !anonAllowed, null)); String[] authReqs = PropertiesUtil.toStringArray(properties.get(PAR_AUTH_REQ)); if (authReqs != null) { for (String authReq : authReqs) { if (authReq != null && authReq.length() > 0) { authRequiredCache.addHolder(AuthenticationRequirementHolder.fromConfig( authReq, null)); } } } final String anonUser = PropertiesUtil.toString(properties.get(PAR_ANONYMOUS_USER), ""); if (anonUser.length() > 0) { this.anonUser = anonUser; this.anonPassword = PropertiesUtil.toString(properties.get(PAR_ANONYMOUS_PASSWORD), "").toCharArray(); } else { this.anonUser = null; this.anonPassword = null; } authUriSuffices = PropertiesUtil.toStringArray(properties.get(PAR_AUTH_URI_SUFFIX), new String[] { DEFAULT_AUTH_URI_SUFFIX }); // don't require authentication for login/logout servlets authRequiredCache.addHolder(new AuthenticationRequirementHolder( LoginServlet.SERVLET_PATH, false, null)); authRequiredCache.addHolder(new AuthenticationRequirementHolder( LogoutServlet.SERVLET_PATH, false, null)); // add all registered services if (serviceListener != null) { serviceListener.registerAllServices(); } final String http; if (anonAllowed) { http = PropertiesUtil.toString(properties.get(PAR_HTTP_AUTH), HTTP_AUTH_PREEMPTIVE); } else { http = HTTP_AUTH_ENABLED; log.debug("modified: Anonymous Access is denied thus HTTP Basic Authentication is fully enabled"); } if (HTTP_AUTH_DISABLED.equals(http)) { httpBasicHandler = null; } else { final String realm = PropertiesUtil.toString(properties.get(PAR_REALM_NAME), DEFAULT_REALM); httpBasicHandler = new HttpBasicAuthenticationHandler(realm, HTTP_AUTH_ENABLED.equals(http)); } } @SuppressWarnings("unused") @Deactivate private void deactivate(final BundleContext bundleContext) { this.authRequiredCache.clear(); if (engineAuthHandlerTracker != null) { engineAuthHandlerTracker.close(); engineAuthHandlerTracker = null; } if (authHandlerTracker != null) { authHandlerTracker.close(); authHandlerTracker = null; } if (serviceListener != null) { bundleContext.removeServiceListener(serviceListener); serviceListener = null; } if (webConsolePlugin != null) { webConsolePlugin.unregister(); webConsolePlugin = null; } } // --------- AuthenticationSupport interface /** * Checks the authentication contained in the request. This check is only * based on the original request object, no URI translation has taken place * yet. * <p> * * @param request The request object containing the information for the * authentication. * @param response The response object which may be used to send the * information on the request failure to the user. * @return <code>true</code> if request processing should continue assuming * successful authentication. If <code>false</code> is returned it * is assumed a response has been sent to the client and the request * is terminated. */ @Override public boolean handleSecurity(HttpServletRequest request, HttpServletResponse response) { // 0. Nothing to do, if the session is also in the request // this might be the case if the request is handled as a result // of a servlet container include inside another Sling request Object sessionAttr = request.getAttribute(REQUEST_ATTRIBUTE_RESOLVER); if (sessionAttr instanceof ResourceResolver) { log.debug("handleSecurity: Request already authenticated, nothing to do"); return true; } else if (sessionAttr != null) { // warn and remove existing non-session log.warn("handleSecurity: Overwriting existing ResourceResolver attribute ({})", sessionAttr); request.removeAttribute(REQUEST_ATTRIBUTE_RESOLVER); } boolean process = doHandleSecurity(request, response); if (process && expectAuthenticationHandler(request)) { log.warn("handleSecurity: AuthenticationHandler did not block request; access denied"); request.removeAttribute(AuthenticationHandler.FAILURE_REASON); request.removeAttribute(AuthenticationHandler.FAILURE_REASON_CODE); AuthUtil.sendInvalid(request, response); return false; } return process; } private boolean doHandleSecurity(HttpServletRequest request, HttpServletResponse response) { // 1. Ask all authentication handlers to try to extract credentials final AuthenticationInfo authInfo = getAuthenticationInfo(request, response); // 2. PostProcess credentials try { postProcess(authInfo, request, response); } catch (LoginException e) { handleLoginFailure(request, response, authInfo, e); return false; } // 3. Check Credentials if (authInfo == AuthenticationInfo.DOING_AUTH) { log.debug("doHandleSecurity: ongoing authentication in the handler"); return false; } else if (authInfo == AuthenticationInfo.FAIL_AUTH) { log.debug("doHandleSecurity: Credentials present but not valid, request authentication again"); AuthUtil.setLoginResourceAttribute(request, request.getRequestURI()); doLogin(request, response); return false; } else if (authInfo.getAuthType() == null) { log.debug("doHandleSecurity: No credentials in the request, anonymous"); return getAnonymousResolver(request, response, authInfo); } else { log.debug("doHandleSecurity: Trying to get a session for {}", authInfo.getUser()); return getResolver(request, response, authInfo); } } // ---------- Authenticator interface /** * Requests authentication information from the client. Returns * <code>true</code> if the information has been requested and request * processing can be terminated. Otherwise the request information could not * be requested and the request should be terminated with a 403/FORBIDDEN * response. * <p> * Any response sent by the handler is also handled by the error handler * infrastructure. * * @param request The request object * @param response The response object to which to send the request * @throws IllegalStateException If response is already committed * @throws NoAuthenticationHandlerException If no authentication handler * claims responsibility to authenticate the request. */ @Override public void login(HttpServletRequest request, HttpServletResponse response) { // ensure the response is not committed yet if (response.isCommitted()) { throw new IllegalStateException("Response already committed"); } // select path used for authentication handler selection final Collection<AbstractAuthenticationHandlerHolder>[] holdersArray = this.authHandlerCache .findApplicableHolders(request); final String path = getHandlerSelectionPath(request); boolean done = false; for (int m = 0; !done && m < holdersArray.length; m++) { final Collection<AbstractAuthenticationHandlerHolder> holderList = holdersArray[m]; if ( holderList != null ) { for (AbstractAuthenticationHandlerHolder holder : holderList) { if (isNodeRequiresAuthHandler(path, holder.path)) { log.debug("login: requesting authentication using handler: {}", holder); try { done = holder.requestCredentials(request, response); } catch (IOException ioe) { log.error( "login: Failed sending authentication request through handler " + holder + ", access forbidden", ioe); done = true; } if (done) { break; } } } } } // fall back to HTTP Basic handler (if not done already) if (!done && httpBasicHandler != null) { done = httpBasicHandler.requestCredentials(request, response); } // no handler could send an authentication request, throw if (!done) { int size = 0; for (int m = 0; m < holdersArray.length; m++) { if (holdersArray[m] != null) { size += holdersArray[m].size(); } } log.info("login: No handler for request ({} handlers available)", size); throw new NoAuthenticationHandlerException(); } } /** * Logs out the user calling all applicable * {@link org.apache.sling.auth.core.spi.AuthenticationHandler} * authentication handlers. */ @Override public void logout(HttpServletRequest request, HttpServletResponse response) { // ensure the response is not committed yet if (response.isCommitted()) { throw new IllegalStateException("Response already committed"); } // make sure impersonation is dropped setSudoCookie(request, response, new AuthenticationInfo("dummy", request.getRemoteUser())); final String path = getHandlerSelectionPath(request); final Collection<AbstractAuthenticationHandlerHolder>[] holdersArray = this.authHandlerCache .findApplicableHolders(request); for (int m = 0; m < holdersArray.length; m++) { final Collection<AbstractAuthenticationHandlerHolder> holderSet = holdersArray[m]; if (holderSet != null) { for (AbstractAuthenticationHandlerHolder holder : holderSet) { if (isNodeRequiresAuthHandler(path, holder.path)) { log.debug("logout: dropping authentication using handler: {}", holder); try { holder.dropCredentials(request, response); } catch (IOException ioe) { log.error( "logout: Failed dropping authentication through handler " + holder, ioe); } } } } } if (httpBasicHandler != null) { httpBasicHandler.dropCredentials(request, response); } redirectAfterLogout(request, response); } // ---------- ServletRequestListener @Override public void requestInitialized(ServletRequestEvent sre) { // don't care } @Override public void requestDestroyed(ServletRequestEvent sre) { ServletRequest request = sre.getServletRequest(); Object resolverAttr = request.getAttribute(REQUEST_ATTRIBUTE_RESOLVER); if (resolverAttr instanceof ResourceResolver) { ((ResourceResolver) resolverAttr).close(); request.removeAttribute(REQUEST_ATTRIBUTE_RESOLVER); } } // ---------- WebConsolePlugin support /** * Returns the list of registered authentication handlers as a map */ Map<String, List<String>> getAuthenticationHandler() { List<AbstractAuthenticationHandlerHolder> registeredHolders = authHandlerCache.getHolders(); LinkedHashMap<String, List<String>> handlerMap = new LinkedHashMap<String, List<String>>(); for (AbstractAuthenticationHandlerHolder holder : registeredHolders) { List<String> provider = handlerMap.get(holder.fullPath); if (provider == null) { provider = new ArrayList<String>(); handlerMap.put(holder.fullPath, provider); } provider.add(holder.getProvider()); } if (httpBasicHandler != null) { List<String> provider = handlerMap.get("/"); if (provider == null) { provider = new ArrayList<String>(); handlerMap.put("/", provider); } provider.add(httpBasicHandler.toString()); } return handlerMap; } List<AuthenticationRequirementHolder> getAuthenticationRequirements() { return authRequiredCache.getHolders(); } /** * Returns the name of the user to assume for requests without credentials. * This may be <code>null</code> if not configured and the default anonymous * user is to be used. * <p> * The configured password cannot be requested. */ String getAnonUserName() { return anonUser; } String getSudoCookieName() { return sudoCookieName; } String getSudoParameterName() { return sudoParameterName; } // ---------- internal private String getPath(HttpServletRequest request) { final StringBuilder sb = new StringBuilder(); if (request.getServletPath() != null) { sb.append(request.getServletPath()); } if (request.getPathInfo() != null) { sb.append(request.getPathInfo()); } return sb.toString(); } private AuthenticationInfo getAuthenticationInfo(HttpServletRequest request, HttpServletResponse response) { // Get the path used to select the authenticator, if the SlingServlet // itself has been requested without any more info, this will be empty // and we assume the root (SLING-722) String path = getPath(request); if (path.length() == 0) { path = "/"; } final Collection<AbstractAuthenticationHandlerHolder>[] localArray = this.authHandlerCache .findApplicableHolders(request); for (int m = 0; m < localArray.length; m++) { final Collection<AbstractAuthenticationHandlerHolder> local = localArray[m]; if (local != null) { for (AbstractAuthenticationHandlerHolder holder : local) { if (isNodeRequiresAuthHandler(path, holder.path)){ final AuthenticationInfo authInfo = holder.extractCredentials( request, response); if (authInfo != null) { // add the feedback handler to the info (may be null) authInfo.put(AUTH_INFO_PROP_FEEDBACK_HANDLER, holder.getFeedbackHandler()); return authInfo; } } } } } // check whether the HTTP Basic handler can extract the header if (httpBasicHandler != null) { final AuthenticationInfo authInfo = httpBasicHandler.extractCredentials( request, response); if (authInfo != null) { authInfo.put(AUTH_INFO_PROP_FEEDBACK_HANDLER, httpBasicHandler); return authInfo; } } // no handler found for the request .... log.debug("getAuthenticationInfo: no handler could extract credentials; assuming anonymous"); return getAnonymousCredentials(); } /** * Run through the available post processors. */ private void postProcess(AuthenticationInfo info, HttpServletRequest request, HttpServletResponse response) throws LoginException { Object[] services = authInfoPostProcessorTracker.getServices(); if (services != null) { for (Object serviceObj : services) { ((AuthenticationInfoPostProcessor)serviceObj).postProcess(info, request, response); } } } /** * Try to acquire a ResourceResolver as indicated by authInfo * * @return <code>true</code> if request processing should continue assuming * successful authentication. If <code>false</code> is returned it * is assumed a response has been sent to the client and the request * is terminated. */ private boolean getResolver(final HttpServletRequest request, final HttpServletResponse response, final AuthenticationInfo authInfo) { // prepare the feedback handler final AuthenticationFeedbackHandler feedbackHandler = (AuthenticationFeedbackHandler) authInfo.remove(AUTH_INFO_PROP_FEEDBACK_HANDLER); final Object sendLoginEvent = authInfo.remove(AuthConstants.AUTH_INFO_LOGIN); // try to connect try { handleImpersonation(request, authInfo); handlePasswordChange(request, authInfo); ResourceResolver resolver = resourceResolverFactory.getResourceResolver(authInfo); final boolean impersChanged = setSudoCookie(request, response, authInfo); if (sendLoginEvent != null) { postLoginEvent(authInfo); } // provide the resource resolver to the feedback handler request.setAttribute(REQUEST_ATTRIBUTE_RESOLVER, resolver); boolean processRequest = true; // custom feedback handler with option to redirect if (feedbackHandler != null) { processRequest = !feedbackHandler.authenticationSucceeded(request, response, authInfo); } if (processRequest) { if (AuthUtil.isValidateRequest(request)) { AuthUtil.sendValid(response); processRequest = false; } else if (impersChanged || feedbackHandler == null) { processRequest = !DefaultAuthenticationFeedbackHandler.handleRedirect(request, response); } } if (processRequest) { // process: set required attributes setAttributes(resolver, authInfo.getAuthType(), request); } else { // terminate: cleanup resolver.close(); } return processRequest; } catch (LoginException re) { // handle failure feedback before proceeding to handling the // failed login internally if (feedbackHandler != null) { feedbackHandler.authenticationFailed(request, response, authInfo); } // now find a way to get credentials unless the feedback handler // has committed a response to the client already if (!response.isCommitted()) { return handleLoginFailure(request, response, authInfo, re); } } // end request return false; } private boolean expectAuthenticationHandler(final HttpServletRequest request) { if (this.authUriSuffices != null) { final String requestUri = request.getRequestURI(); for (final String uriSuffix : this.authUriSuffices) { if (requestUri.endsWith(uriSuffix)) { return true; } } } return false; } /** Try to acquire an anonymous ResourceResolver */ private boolean getAnonymousResolver(final HttpServletRequest request, final HttpServletResponse response, final AuthenticationInfo authInfo) { // Get an anonymous session if allowed, or if we are handling // a request for the login servlet if (isAnonAllowed(request)) { try { ResourceResolver resolver = resourceResolverFactory.getResourceResolver(authInfo); // check whether the client asked for redirect after // authentication and/or impersonation if (DefaultAuthenticationFeedbackHandler.handleRedirect( request, response)) { // request will now be terminated, so close the resolver // to release resources resolver.close(); return false; } // set the attributes for further processing setAttributes(resolver, null, request); return true; } catch (LoginException re) { // cannot login > fail login, do not try to authenticate handleLoginFailure(request, response, new AuthenticationInfo(null, "anonymous user"), re); return false; } } // If we get here, anonymous access is not allowed: redirect // to the login servlet log.info("getAnonymousResolver: Anonymous access not allowed by configuration - requesting credentials"); doLogin(request, response); // fallback to no session return false; } private boolean isAnonAllowed(HttpServletRequest request) { String path = getPath(request); if (path.length() == 0) { path = "/"; } final Collection<AuthenticationRequirementHolder>[] holderSetArray = authRequiredCache .findApplicableHolders(request); for (int m = 0; m < holderSetArray.length; m++) { final Collection<AuthenticationRequirementHolder> holders = holderSetArray[m]; if (holders != null) { for (AuthenticationRequirementHolder holder : holders) { if (isNodeRequiresAuthHandler(path, holder.path)) { return !holder.requiresAuthentication(); } } } } // fallback to anonymous not allowed (aka authentication required) return false; } private boolean isNodeRequiresAuthHandler(String path, String holderPath) { if (path == null || holderPath == null) { return false; } if (("/").equals(holderPath)) { return true; } int holderPathLength = holderPath.length(); if (path.length() < holderPathLength) { return false; } if (path.equals(holderPath)) { return true; } if (path.startsWith(holderPath)) { if (path.charAt(holderPathLength) == '/' || path.charAt(holderPathLength) == '.') { return true; } } return false; } /** * Returns credentials to use for anonymous resource access. If an anonymous * user is configued, this returns an {@link AuthenticationInfo} instance * whose authentication type is <code>null</code> and the user name and * password are set according to the {@link #PAR_ANONYMOUS_USER} and * {@link #PAR_ANONYMOUS_PASSWORD} configurations. Otherwise * the user name and password fields are just <code>null</code>. */ private AuthenticationInfo getAnonymousCredentials() { AuthenticationInfo info = new AuthenticationInfo(null); if (this.anonUser != null) { info.setUser(this.anonUser); info.setPassword(this.anonPassword); } return info; } private boolean handleLoginFailure(final HttpServletRequest request, final HttpServletResponse response, final AuthenticationInfo authInfo, final Exception reason) { String user = authInfo.getUser(); boolean processRequest = false; if (reason.getClass().getName().contains("TooManySessionsException")) { // to many users, send a 503 Service Unavailable log.info("handleLoginFailure: Too many sessions for {}: {}", user, reason.getMessage()); try { response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "SlingAuthenticator: Too Many Users"); } catch (IOException ioe) { log.error( "handleLoginFailure: Cannot send status 503 to client", ioe); } } else if (reason instanceof LoginException) { log.info("handleLoginFailure: Unable to authenticate {}: {}", user, reason.getMessage()); if (isAnonAllowed(request) && !expectAuthenticationHandler(request) && !AuthUtil.isValidateRequest(request)) { log.debug("handleLoginFailure: LoginException on an anonymous resource, fallback to getAnonymousResolver"); processRequest = getAnonymousResolver(request, response, new AuthenticationInfo(null)); } else { // request authentication information and send 403 (Forbidden) // if no handler can request authentication information. AuthenticationHandler.FAILURE_REASON_CODES code = AuthenticationHandler.FAILURE_REASON_CODES.INVALID_LOGIN; String message = "User name and password do not match"; if (reason.getCause() instanceof CredentialExpiredException) { // force failure attribute to be set so handlers can // react to this special circumstance Object creds = authInfo.get("user.jcr.credentials"); if (creds instanceof SimpleCredentials && ((SimpleCredentials) creds).getAttribute("PasswordHistoryException") != null) { code = AuthenticationHandler.FAILURE_REASON_CODES.PASSWORD_EXPIRED_AND_NEW_PASSWORD_IN_HISTORY; message = "Password expired and new password found in password history"; } else { code = AuthenticationHandler.FAILURE_REASON_CODES.PASSWORD_EXPIRED; message = "Password expired"; } } else if (reason.getCause() instanceof AccountLockedException) { code = AuthenticationHandler.FAILURE_REASON_CODES.ACCOUNT_LOCKED; message = "Account is locked"; } else if (reason.getCause() instanceof AccountNotFoundException) { code = AuthenticationHandler.FAILURE_REASON_CODES.ACCOUNT_NOT_FOUND; message = "Account was not found"; } // preset a reason for the login failure request.setAttribute(AuthenticationHandler.FAILURE_REASON_CODE, code); ensureAttribute(request, AuthenticationHandler.FAILURE_REASON, message); doLogin(request, response); } } else { // general problem, send a 500 Internal Server Error log.error("handleLoginFailure: Unable to authenticate " + user, reason); try { response.sendError( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "SlingAuthenticator: data access error, reason=" + reason.getClass().getSimpleName()); } catch (IOException ioe) { log.error( "handleLoginFailure: Cannot send status 500 to client", ioe); } } return processRequest; } /** * Tries to request credentials from the client. The following mechanisms * are implemented by this method: * <ul> * <li>If the request is a credentials validation request (see * {@link AbstractAuthenticationHandler#isValidateRequest(HttpServletRequest)} * ) a 403/FORBIDDEN response is sent back.</li> * <li>If the request is not considered a * {@link #isBrowserRequest(HttpServletRequest) browser request} and the * HTTP Basic Authentication Handler is at least enabled for preemptive * credentials processing, a 401/UNAUTHORIZED response is sent back. This * helps implementing HTTP Basic authentication with WebDAV clients. If HTTP * Basic Authentication is completely switched of a 403/FORBIDDEN response * is sent back instead.</li> * <li>If the request is considered an * {@link #isAjaxRequest(HttpServletRequest) Ajax request} a 403/FORBIDDEN * response is simply sent back because we assume an Ajax requestor cannot * properly handle any request for credentials graciously.</li> * <li>Otherwise the {@link #login(HttpServletRequest, HttpServletResponse)} * method is called to try to find and call an authentication handler to * request credentials from the client. If none is available or willing to * request credentials, a 403/FORBIDDEN response is also sent back to the * client.</li> * </ul> * <p> * If a 403/FORBIDDEN response is sent back the {@link AbstractAuthenticationHandler#X_REASON} header is * set to a either the value of the * {@link AuthenticationHandler#FAILURE_REASON} request attribute or to some * generic description describing the reason. To actually send the response * the * {@link AbstractAuthenticationHandler#sendInvalid(HttpServletRequest, HttpServletResponse)} * method is called. * <p> * This method is called in three situations: * <ul> * <li>If the request contains no credentials but anonymous login is not * allowed</li> * <li>If the request contains credentials but getting the Resource Resolver * using the provided credentials fails</li> * <li>If the selected authentication handler indicated any presented * credentials are not valid</li> * </ul> * * @param request The current request * @param response The response to send the credentials request (or access * denial to) * @see AbstractAuthenticationHandler#isValidateRequest(HttpServletRequest) * @see #isBrowserRequest(HttpServletRequest) * @see #isAjaxRequest(HttpServletRequest) * @see AbstractAuthenticationHandler#sendInvalid(HttpServletRequest, * HttpServletResponse) */ private void doLogin(HttpServletRequest request, HttpServletResponse response) { if (!AuthUtil.isValidateRequest(request)) { if (AuthUtil.isBrowserRequest(request)) { if (!AuthUtil.isAjaxRequest(request) && !isLoginLoop(request)) { try { login(request, response); return; } catch (IllegalStateException ise) { log.error("doLogin: Cannot login: Response already committed"); return; } catch (NoAuthenticationHandlerException nahe) { /* * Don't set the failureReason for missing * authentication handlers to not disclose this setup * information. */ log.error("doLogin: Cannot login: No AuthenticationHandler available to handle the request"); } } } else { // Presumably this is WebDAV. If HTTP Basic is fully enabled or // enabled for preemptive credential support, we just request // HTTP Basic credentials. Otherwise (HTTP Basic is fully // switched off, 403 is sent back) if (httpBasicHandler != null) { httpBasicHandler.sendUnauthorized(response); return; } } } // if we are here, we cannot redirect to the login form because it is // an XHR request or because there is no authentication handler willing // request credentials from the client or because it is a failed // credential validation // ensure a failure reason ensureAttribute(request, AuthenticationHandler.FAILURE_REASON, "Authentication Failed"); AuthUtil.sendInvalid(request, response); } /** * Returns <code>true</code> if the current request was referred to by the * same URL as the current request has. This is assumed to be caused by a * loop in requesting credentials from the client. Such a loop will probably * never cause the request for credentials to succeed, so it must be broken. * * @param request The request to check * @return <code>true</code> if the request is considered to be a loop; * <code>false</code> otherwise */ private boolean isLoginLoop(final HttpServletRequest request) { String referer = request.getHeader("Referer"); if (referer != null) { StringBuffer sb = request.getRequestURL(); if (request.getQueryString() != null) { sb.append('?').append(request.getQueryString()); } return referer.equals(sb.toString()); } // no referer means no loop return false; } /** * Sets the name request attribute to the given value unless the request * attribute is already set a non-<code>null</code> value. * * @param request The request on which to set the attribute * @param attribute The name of the attribute to check/set * @param value The value to set the attribute to if it is not already set */ private void ensureAttribute(final HttpServletRequest request, final String attribute, final Object value) { if (request.getAttribute(attribute) == null) { request.setAttribute(attribute, value); } } /** * Sets the request attributes required by the OSGi HttpContext interface * specification for the <code>handleSecurity</code> method. In addition the * {@link SlingAuthenticator#REQUEST_ATTRIBUTE_RESOLVER} request attribute * is set to the ResourceResolver. */ private void setAttributes(final ResourceResolver resolver, final String authType, final HttpServletRequest request) { // HttpService API required attributes request.setAttribute(ServletContextHelper.REMOTE_USER, resolver.getUserID()); request.setAttribute(ServletContextHelper.AUTHENTICATION_TYPE, authType); // resource resolver for down-stream use request.setAttribute(REQUEST_ATTRIBUTE_RESOLVER, resolver); log.debug( "setAttributes: ResourceResolver stored as request attribute: user={}", resolver.getUserID()); } /** * Sends the session cookie for the name session with the given age in * seconds. This sends a Version 1 cookie. * * @param response The {@link HttpServletResponse} on which to send * back the cookie. * @param user The name of the user to impersonate as. This will be quoted * and used as the cookie value. * @param maxAge The maximum age of the cookie in seconds. Positive values * are persisted on the browser for the indicated number of * seconds, setting the age to 0 (zero) causes the cookie to be * deleted in the browser and using a negative value defines a * temporary cookie to be deleted when the browser exits. * @param path The cookie path to use. This is intended to be the web * application's context path. If this is empty or * <code>null</code> the root path will be used assuming the web * application is registered at the root context. * @param owner The name of the user originally authenticated in the request * and who is now impersonating as <i>user</i>. */ private void sendSudoCookie(HttpServletResponse response, final String user, final int maxAge, final String path, final String owner) { final String quotedUser; String quotedOwner = null; try { quotedUser = quoteCookieValue(user); if (owner != null) { quotedOwner = quoteCookieValue(owner); } } catch (IllegalArgumentException iae) { log.error( "sendSudoCookie: Failed to quote value '{}' of cookie {}: {}", new Object[] { user, this.sudoCookieName, iae.getMessage() }); return; } catch (UnsupportedEncodingException e) { log.error( "sendSudoCookie: Failed to quote value '{}' of cookie {}: {}", new Object[] { user, this.sudoCookieName, e.getMessage() }); return; } if (quotedUser != null) { Cookie cookie = new Cookie(this.sudoCookieName, quotedUser); cookie.setMaxAge(maxAge); cookie.setPath((path == null || path.length() == 0) ? "/" : path); try { cookie.setComment(quotedOwner + " impersonates as " +quotedUser); } catch (IllegalArgumentException iae) { // ignore } response.addCookie(cookie); // Tell a potential proxy server that this request is uncacheable // due to the Set-Cookie header being sent if (this.cacheControl) { response.addHeader("Cache-Control", "no-cache=\"Set-Cookie\""); } } } /** * Handles impersonation based on the request parameter for impersonation * (see {@link #sudoParameterName}) and the current setting in the sudo * cookie. * <p> * If the sudo parameter is empty or missing, the current cookie setting for * impersonation is used. Else if the parameter is <code>-</code>, the * current cookie impersonation is removed and no impersonation will take * place for this request. Else the parameter is assumed to contain the name * of a user to impersonate as. * * @param req The {@link HttpServletRequest} optionally containing * the sudo parameter. * @param authInfo The authentication info into which the * <code>sudo.user.id</code> property is set to the impersonator * user. */ private void handleImpersonation(HttpServletRequest req, AuthenticationInfo authInfo) { String currentSudo = getSudoCookieValue(req); /** * sudo parameter : empty or missing to continue to use the setting * already stored in the session; or "-" to remove impersonation * altogether (also from the session); or the handle of a user page to * impersonate as that user (if possible) */ String sudo = req.getParameter(this.sudoParameterName); if (sudo == null || sudo.length() == 0) { sudo = currentSudo; } else if ("-".equals(sudo)) { sudo = null; } // sudo the session if needed if (sudo != null && sudo.length() > 0) { authInfo.put(ResourceResolverFactory.USER_IMPERSONATION, sudo); } } /** * Handles password change based on the request parameter for the new password * (see {@link #newPasswordParameterName}). * <p> * If the new password request parameter is present, it is added to the authInfo * object, which is later transformed to SimpleCredentials attributes. * * @param req The {@link HttpServletRequest} optionally containing * the new password parameter. * @param authInfo The authentication info into which the * <code>newPassword</code> property is set. */ private void handlePasswordChange(HttpServletRequest req, AuthenticationInfo authInfo) { String newPassword = req.getParameter(PAR_NEW_PASSWORD ); if (newPassword != null && newPassword.length() > 0) { authInfo.put("user.newpassword", newPassword); } } private String getSudoCookieValue(HttpServletRequest req) { // the current state of impersonation String currentSudo = null; Cookie[] cookies = req.getCookies(); if (cookies != null) { for (int i = 0; currentSudo == null && i < cookies.length; i++) { if (sudoCookieName.equals(cookies[i].getName())) { currentSudo = unquoteCookieValue(cookies[i].getValue()); } } } return currentSudo; } /** * Sets the impersonation cookie on the response if impersonation actually * changed and returns whether the cookie has been set (or cleared) or not. * <p> * The current impersonation state is taken from the sudo cookie value * while the desired state is taken from the user.impersonation * property of the auth info. If they don't match, the sudo cookie * is set according to the user.impersonation property where the * cookie is actually cleared if the property is <code>null</code>. * * @param req Providing the current sudo cookie value * @param res For setting the sudo cookie * @param authInfo Providing information about desired impersonation * @return <code>true</code> if the cookie has been set or cleared or * <code>false</code> if the cookie is not modified. */ private boolean setSudoCookie(HttpServletRequest req, HttpServletResponse res, AuthenticationInfo authInfo) { String sudo = (String) authInfo.get(ResourceResolverFactory.USER_IMPERSONATION); String currentSudo = getSudoCookieValue(req); // set the (new) impersonation final boolean setCookie = sudo != currentSudo; if (setCookie) { if (sudo == null) { // Parameter set to "-" to clear impersonation, which was // active due to cookie setting // clear impersonation this.sendSudoCookie(res, "", 0, req.getContextPath(), authInfo.getUser()); } else if (currentSudo == null || !currentSudo.equals(sudo)) { // Parameter set to a name. As the cookie is not set yet // or is set to another name, send the cookie with current sudo // (re-)set impersonation this.sendSudoCookie(res, sudo, -1, req.getContextPath(), sudo); } } return setCookie; } /** * Returns the path to be used to select the authentication handler to login * or logout with. * <p> * This method uses the {@link Authenticator#LOGIN_RESOURCE} request * attribute. If this attribute is not set (or is not a string), the request * path info is used. If this is not set either, or is the empty string, "/" * is returned. * * @param request The request providing the request attribute or path info. * @return The path as set by the request attribute or the path info or "/" * if neither is set. */ private String getHandlerSelectionPath(HttpServletRequest request) { final Object loginPathO = request.getAttribute(Authenticator.LOGIN_RESOURCE); String path; if (loginPathO instanceof String) { path = (String) loginPathO; final String ctxPath = request.getContextPath(); if (ctxPath != null && path.startsWith(ctxPath)) { path = path.substring(ctxPath.length()); } } else { path = getPath(request); } if (path == null || path.length() == 0) { path = "/"; } return path; } /** * If the response has not been committed yet, redirect to target requested * by the <code>resource</code> request attribute or parameter. If neither * is set to a non-null string, the request is redirected to the context * root. * <p> * The response is not reset though, since the handler may have set states * such as an updated HTTP session or some Cookie * * @param request The request providing the redirect target * @param response The response to send the redirect to */ private void redirectAfterLogout(final HttpServletRequest request, final HttpServletResponse response) { // nothing more to do if the response has already been committed if (response.isCommitted()) { log.debug("redirectAfterLogout: Response has already been committed, not redirecting"); return; } // find the redirect target from the resource attribute or parameter // falling back to the request context path (or /) if not set or invalid String target = AuthUtil.getLoginResource(request, request.getContextPath()); if (!AuthUtil.isRedirectValid(request, target)) { log.warn("redirectAfterLogout: Desired redirect target '{}' is invalid; redirecting to '/'", target); target = request.getContextPath() + "/"; } // redirect to there try { response.sendRedirect(target); } catch (IOException e) { log.error("Failed to redirect to the page: " + target, e); } } private void postLoginEvent(final AuthenticationInfo authInfo) { final Dictionary<String, Object> properties = new Hashtable<String, Object>(); properties.put(SlingConstants.PROPERTY_USERID, authInfo.getUser()); properties.put(AuthenticationInfo.AUTH_TYPE, authInfo.getAuthType()); EventAdmin localEA = this.eventAdmin; if (localEA != null) { localEA.postEvent(new Event(AuthConstants.TOPIC_LOGIN, properties)); } } /** * Ensures the cookie value is properly quoted for transmission to the * client. * <p> * The problem is, that the character set of cookie values is limited by * RFC 2109 defining that a cookie value must be token or quoted-string * according to RFC-2616: * <pre> * token = 1*<any CHAR except CTLs or separators> * separators = "(" | ")" | "<" | ">" | "@" * | "," | ";" | ":" | "\" | <"> * | "/" | "[" | "]" | "?" | "=" * | "{" | "}" | SP | HT * * quoted-string = ( <"> *(qdtext | quoted-pair ) <"> ) * qdtext = <any TEXT except <">> * quoted-pair = "\" CHAR * * @param value The cookie value to quote * @return The quoted cookie value * @throws UnsupportedEncodingException * @throws IllegalArgumentException If the cookie value is <code>null</code> * or cannot be quoted, primarily because it contains a quote * sign. */ static String quoteCookieValue(final String value) throws UnsupportedEncodingException { // method is package private to enable unit testing if (value == null) { throw new IllegalArgumentException("Cookie value may not be null"); } StringBuilder builder = new StringBuilder(value.length() * 2); builder.append('"'); for (int i = 0; i < value.length(); i++) { char c = value.charAt(i); if (c == '"') { builder.append("\\\""); } else if (c == '@') { builder.append(c); } else if (c == 127 || (c < 32 && c != '\t')) { throw new IllegalArgumentException( "Cookie value may not contain CTL character"); } else { builder.append(URLEncoder.encode(String.valueOf(c), "UTF-8")); } } builder.append('"'); return builder.toString(); } /** * Removes (optional) quotes from a cookie value to get the raw value of the * cookie. * * @param value The cookie value to unquote * @return The unquoted cookie value */ static String unquoteCookieValue(String value) { // method is package private to enable unit testing // return value unmodified if null or empty if (value == null || value.length() == 0) { return value; } if (value.startsWith("\"") && value.endsWith("\"")) { value = value.substring(1, value.length()-1); } StringBuilder builder = new StringBuilder(); String [] values = value.split("\\\\"); for (String v:values) { try { builder.append(URLDecoder.decode(v, "UTF-8")); } catch (UnsupportedEncodingException e) { builder.append(v); } } return builder.toString(); } private static class AuthenticationHandlerTracker extends ServiceTracker { private final PathBasedHolderCache<AbstractAuthenticationHandlerHolder> authHandlerCache; private final HashMap<Object, AbstractAuthenticationHandlerHolder[]> handlerMap = new HashMap<Object, AbstractAuthenticationHandlerHolder[]>(); AuthenticationHandlerTracker( final BundleContext context, final PathBasedHolderCache<AbstractAuthenticationHandlerHolder> authHandlerCache) { this(context, AuthenticationHandler.SERVICE_NAME, authHandlerCache); } protected AuthenticationHandlerTracker( final BundleContext context, final String className, final PathBasedHolderCache<AbstractAuthenticationHandlerHolder> authHandlerCache) { super(context, className, null); this.authHandlerCache = authHandlerCache; open(); } @Override public Object addingService(ServiceReference reference) { final Object service = super.addingService(reference); if (service != null) { bindAuthHandler(service, reference); } return service; } @Override public void modifiedService(ServiceReference reference, Object service) { unbindAuthHandler(reference); if (service != null) { bindAuthHandler(service, reference); } } @Override public void removedService(ServiceReference reference, Object service) { unbindAuthHandler(reference); super.removedService(reference, service); } protected AbstractAuthenticationHandlerHolder createHolder( final String path, final Object handler, final ServiceReference serviceReference) { return new AuthenticationHandlerHolder(path, (AuthenticationHandler) handler, serviceReference); } private void bindAuthHandler(final Object handler, final ServiceReference ref) { final String paths[] = PropertiesUtil.toStringArray(ref.getProperty(AuthenticationHandler.PATH_PROPERTY)); if (paths != null && paths.length > 0) { // generate the holders ArrayList<AbstractAuthenticationHandlerHolder> holderList = new ArrayList<AbstractAuthenticationHandlerHolder>(); for (String path : paths) { if (path != null && path.length() > 0) { holderList.add(createHolder(path, handler, ref)); } } // register the holders AbstractAuthenticationHandlerHolder[] holders = holderList.toArray(new AbstractAuthenticationHandlerHolder[holderList.size()]); for (AbstractAuthenticationHandlerHolder holder : holders) { authHandlerCache.addHolder(holder); } // keep a copy of them for unregistration later synchronized (handlerMap) { handlerMap.put(ref.getProperty(Constants.SERVICE_ID), holders); } } } private void unbindAuthHandler(ServiceReference ref) { final AbstractAuthenticationHandlerHolder[] holders; synchronized (handlerMap) { holders = handlerMap.remove(ref.getProperty(Constants.SERVICE_ID)); } if (holders != null) { for (AbstractAuthenticationHandlerHolder holder : holders) { authHandlerCache.removeHolder(holder); } } } } private static class EngineAuthenticationHandlerTracker extends AuthenticationHandlerTracker { EngineAuthenticationHandlerTracker( final BundleContext context, final PathBasedHolderCache<AbstractAuthenticationHandlerHolder> authHandlerCache) { super(context, "org.apache.sling.engine.auth.AuthenticationHandler", authHandlerCache); } @SuppressWarnings("deprecation") @Override protected AbstractAuthenticationHandlerHolder createHolder(String path, Object handler, final ServiceReference serviceReference) { return new EngineAuthenticationHandlerHolder(path, (org.apache.sling.engine.auth.AuthenticationHandler) handler, serviceReference); } } }