All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.sling.auth.core.AuthUtil Maven / Gradle / Ivy

/*
 * 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;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.sling.api.auth.Authenticator;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceUtil;
import org.apache.sling.auth.core.spi.AuthenticationHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The AuthUtil provides utility functions for implementations of
 * {@link org.apache.sling.auth.core.spi.AuthenticationHandler} services and
 * users of the Sling authentication infrastructure.
 * 

* This utility class can neither be extended from nor can it be instantiated. * * @since 1.1 (bundle version 1.0.8) */ public final class AuthUtil { /** * Request header commonly set by Ajax Frameworks to indicate the request is * posted as an Ajax request. The value set is expected to be * {@link #XML_HTTP_REQUEST}. *

* This header is known to be set by JQuery, ExtJS and Prototype. Other * client-side JavaScript framework most probably also set it. * * @see #isAjaxRequest(javax.servlet.http.HttpServletRequest) */ private static final String X_REQUESTED_WITH = "X-Requested-With"; /** * The expected value of the {@link #X_REQUESTED_WITH} request header to * identify a request as an Ajax request. * * @see #isAjaxRequest(javax.servlet.http.HttpServletRequest) */ private static final String XML_HTTP_REQUEST = "XMLHttpRequest"; /** * Request header providing the clients user agent information used * by {@link #isBrowserRequest(HttpServletRequest)} to decide whether * a request is probably sent by a browser or not. */ private static final String USER_AGENT = "User-Agent"; /** * String contained in a {@link #USER_AGENT} header indicating a Mozilla * class browser. Examples of such browsers are Firefox (generally Gecko * based browsers), Safari, Chrome (probably generally WebKit based * browsers), and Microsoft IE. */ private static final String BROWSER_CLASS_MOZILLA = "Mozilla"; /** * String contained in a {@link #USER_AGENT} header indicating a Opera class * browser. The only known browser in this class is the Opera browser. */ private static final String BROWSER_CLASS_OPERA = "Opera"; // no instantiation private AuthUtil() { } /** * Returns the value of the named request attribute or parameter as a string * as follows: *

    *
  1. If there is a request attribute of that name, which is a non-empty * string, it is returned.
  2. *
  3. If there is a non-empty request parameter of * that name, this parameter is returned.
  4. *
  5. Otherwise the defaultValue is returned.
  6. *
* * @param request The request from which to return the attribute or request * parameter * @param name The name of the attribute/parameter * @param defaultValue The default value to use if neither a non-empty * string attribute or a non-empty parameter exists in the * request. * @return The attribute, parameter or defaultValue as defined * above. */ public static String getAttributeOrParameter( final HttpServletRequest request, final String name, final String defaultValue) { final String resourceAttr = getAttributeString(request, name); if (resourceAttr != null) { return resourceAttr; } final String resource = request.getParameter(name); if (resource != null && resource.length() > 0) { return resource; } return defaultValue; } /** * Returns any resource target to redirect to after successful * authentication. This method either returns a non-empty string or the * defaultLoginResource parameter. First the * resource request attribute is checked. If it is a non-empty * string, it is returned. Second the resource request * parameter is checked and returned if it is a non-empty string. * * @param request The request providing the attribute or parameter * @param defaultLoginResource The default login resource value * @return The non-empty redirection target or * defaultLoginResource. */ public static String getLoginResource(final HttpServletRequest request, String defaultLoginResource) { return getAttributeOrParameter(request, Authenticator.LOGIN_RESOURCE, defaultLoginResource); } /** * Ensures and returns the {@link Authenticator#LOGIN_RESOURCE} request * attribute is set to a non-null, non-empty string. If the attribute is not * currently set, this method sets it as follows: *
    *
  1. If the {@link Authenticator#LOGIN_RESOURCE} request parameter is set * to a non-empty string, that parameter is set
  2. *
  3. Otherwise if the defaultValue is a non-empty string the * default value is used
  4. *
  5. Otherwise the attribute is set to "/"
  6. *
* * @param request The request to check for the resource attribute * @param defaultValue The default value to use if the attribute is not set * and the request parameter is not set. This parameter is * ignored if it is null or an empty string. * @return returns the value of resource request attribute */ public static String setLoginResourceAttribute( final HttpServletRequest request, final String defaultValue) { String resourceAttr = getAttributeString(request, Authenticator.LOGIN_RESOURCE); if (resourceAttr == null) { final String resourcePar = request.getParameter(Authenticator.LOGIN_RESOURCE); if (resourcePar != null && resourcePar.length() > 0) { resourceAttr = resourcePar; } else if (defaultValue != null && defaultValue.length() > 0) { resourceAttr = defaultValue; } else { resourceAttr = "/"; } request.setAttribute(Authenticator.LOGIN_RESOURCE, resourceAttr); } return resourceAttr; } /** * Redirects to the given target path appending any parameters provided in * the parameter map. *

* This method implements the following functionality: *

    *
  • If the params map does not contain a (non- * null) value for the {@link Authenticator#LOGIN_RESOURCE * resource} entry, such an entry is generated from the request URI and the * (optional) query string of the given request.
  • *
  • The parameters from the params map or at least a single * {@link Authenticator#LOGIN_RESOURCE resource} parameter are added to the * target path for the redirect. Each parameter value is encoded using the * java.net.URLEncoder with UTF-8 encoding to make it safe for * requests
  • *
*

* After checking the redirect target and creating the target URL from the * parameter map, the response buffer is reset and the * HttpServletResponse.sendRedirect is called. Any headers * already set before calling this method are preserved. * * @param request The request object used to get the current request URI and * request query string if the params map does not * have the {@link Authenticator#LOGIN_RESOURCE resource} * parameter set. * @param response The response used to send the redirect to the client. * @param target The redirect target to validate. This path must be prefixed * with the request's servlet context path. If this parameter is * not a valid target request as per the * {@link #isRedirectValid(HttpServletRequest, String)} method * the target is modified to be the root of the request's * context. * @param params The map of parameters to be added to the target path. This * may be null. * @throws IOException If an error occurs sending the redirect request * @throws IllegalStateException If the response was committed or if a * partial URL is given and cannot be converted into a valid URL * @throws InternalError If the UTF-8 character encoding is not supported by * the platform. This should not be caught, because it is a real * problem if the encoding required by the specification is * missing. */ public static void sendRedirect(final HttpServletRequest request, final HttpServletResponse response, final String target, Map params) throws IOException { checkAndReset(response); StringBuilder b = new StringBuilder(); if (AuthUtil.isRedirectValid(request, target)) { b.append(target); } else if (request.getContextPath().length() == 0) { b.append("/"); } else { b.append(request.getContextPath()); } if (params == null) { params = new HashMap(); } // ensure the login resource is provided with the redirect if (params.get(Authenticator.LOGIN_RESOURCE) == null) { String resource = request.getRequestURI(); if (request.getQueryString() != null) { resource += "?" + request.getQueryString(); } params.put(Authenticator.LOGIN_RESOURCE, resource); } b.append('?'); Iterator> ei = params.entrySet().iterator(); while (ei.hasNext()) { Entry entry = ei.next(); if (entry.getKey() != null && entry.getValue() != null) { try { b.append(entry.getKey()).append('=').append( URLEncoder.encode(entry.getValue(), "UTF-8")); } catch (UnsupportedEncodingException uee) { throw new InternalError( "Unexpected UnsupportedEncodingException for UTF-8"); } if (ei.hasNext()) { b.append('&'); } } } response.sendRedirect(b.toString()); } /** * Returns the name request attribute if it is a non-empty string value. * * @param request The request from which to retrieve the attribute * @param name The name of the attribute to return * @return The named request attribute or null if the attribute * is not set or is not a non-empty string value. */ private static String getAttributeString(final HttpServletRequest request, final String name) { Object resObj = request.getAttribute(name); if ((resObj instanceof String) && ((String) resObj).length() > 0) { return (String) resObj; } // not set or not a non-empty string return null; } /** * Returns true if the the client just asks for validation of * submitted username/password credentials. *

* This implementation returns true if the request parameter * {@link AuthConstants#PAR_J_VALIDATE} is set to true (case-insensitve). If * the request parameter is not set or to any value other than * true this method returns false. * * @param request The request to provide the parameter to check * @return true if the {@link AuthConstants#PAR_J_VALIDATE} parameter is set * to true. */ public static boolean isValidateRequest(final HttpServletRequest request) { return "true".equalsIgnoreCase(request.getParameter(AuthConstants.PAR_J_VALIDATE)); } /** * Sends a 200/OK response to a credential validation request. *

* This method just overwrites the response status to 200/OK, sends no * content (content length header set to zero) and prevents caching on * clients and proxies. Any other response headers set before calling this * methods are preserved and sent along with the response. * * @param response The response object * @throws IllegalStateException if the response has already been committed */ public static void sendValid(final HttpServletResponse response) { checkAndReset(response); try { response.setStatus(HttpServletResponse.SC_OK); // explicitly tell we have no content but set content type // to prevent firefox from trying to parse the response // (SLING-1841) response.setContentType("text/plain"); response.setContentLength(0); // prevent the client from aggressively caching the response // (SLING-1841) response.setHeader("Pragma", "no-cache"); response.setHeader("Cache-Control", "no-cache"); response.addHeader("Cache-Control", "no-store"); response.flushBuffer(); } catch (IOException ioe) { getLog().error("Failed to send 200/OK response", ioe); } } /** * Sends a 403/FORBIDDEN response optionally stating the reason for this * response code in the {@link AuthConstants#X_REASON} header. The value for the * {@link AuthConstants#X_REASON} header is taken from * {@link AuthenticationHandler#FAILURE_REASON} request attribute if set. *

* This method just overwrites the response status to 403/FORBIDDEN, adds * the {@link AuthConstants#X_REASON} header and sends the reason as result * back. Any other response headers set before calling this methods are * preserved and sent along with the response. * * @param request The request object * @param response The response object * @throws IllegalStateException if the response has already been committed */ public static void sendInvalid(final HttpServletRequest request, final HttpServletResponse response) { checkAndReset(response); try { response.setStatus(HttpServletResponse.SC_FORBIDDEN); Object reason = request.getAttribute(AuthenticationHandler.FAILURE_REASON); Object reasonCode = request.getAttribute(AuthenticationHandler.FAILURE_REASON_CODE); if (reason != null) { response.setHeader(AuthConstants.X_REASON, reason.toString()); if ( reasonCode != null ) { response.setHeader(AuthConstants.X_REASON_CODE, reasonCode.toString()); } response.setContentType("text/plain"); response.setCharacterEncoding("UTF-8"); response.getWriter().println(reason); } response.flushBuffer(); } catch (IOException ioe) { getLog().error("Failed to send 403/Forbidden response", ioe); } } /** * Check if the request is for this authentication handler. * * @param request the current request * @return true if the referer matches this handler, or false otherwise */ public static boolean checkReferer(HttpServletRequest request, String loginForm) { //SLING-2165: if a Referer header is supplied check if it matches the login path for this handler if ("POST".equals(request.getMethod())) { String referer = request.getHeader("Referer"); if (referer != null) { String expectedPath = String.format("%s%s", request.getContextPath(), loginForm); try { URL uri = new URL(referer); if (!expectedPath.equals(uri.getPath())) { //not for this selector, so let the next one handle it. return false; } } catch (MalformedURLException e) { getLog().debug("Failed to parse the referer value for the login form " + loginForm, e); } } } return true; } /** * Returns true if the given redirect target is * valid according to the following list of requirements: *

    *
  • The target is neither null nor an empty * string
  • *
  • The target is not an URL which is identified by the * character sequence :// separating the scheme from the host
  • *
  • The target is normalized such that it contains no * consecutive slashes and no path segment contains a single or double dot
  • *
  • The target must be prefixed with the servlet context * path
  • *
  • If a ResourceResolver is available as a request * attribute the target (without the servlet context path * prefix) must resolve to an existing resource
  • *
  • If a ResourceResolver is not available as a * request attribute the target must be an absolute path * starting with a slash character does not contain any of the characters * <, >, ', or " * in plain or URL encoding
  • *
*

* If any of the conditions does not hold, the method returns * false and logs a warning level message with the * org.apache.sling.auth.core.AuthUtil logger. * * @param request Providing the ResourceResolver attribute and * the context to resolve the resource from the * target. This may be null which * causes the target to not be validated with a * ResoureResolver * @param target The redirect target to validate. This path must be * prefixed with the request's servlet context path. * @return true if the redirect target can be considered valid */ public static boolean isRedirectValid(final HttpServletRequest request, final String target) { if (target == null || target.length() == 0) { getLog().warn("isRedirectValid: Redirect target must not be empty or null"); return false; } try { new URI(target); } catch (URISyntaxException e) { getLog().warn("isRedirectValid: Redirect target '{}' contains illegal characters", target); return false; } if (target.contains("://")) { getLog().warn("isRedirectValid: Redirect target '{}' must not be an URL", target); return false; } if (target.contains("//") || target.contains("/../") || target.contains("/./") || target.endsWith("/.") || target.endsWith("/..")) { getLog().warn("isRedirectValid: Redirect target '{}' is not normalized", target); return false; } final String ctxPath = getContextPath(request); if (ctxPath.length() > 0 && !target.startsWith(ctxPath)) { getLog().warn("isRedirectValid: Redirect target '{}' does not start with servlet context path '{}'", target, ctxPath); return false; } // special case of requesting the servlet context root path if (ctxPath.length() == target.length()) { return true; } final String localTarget = target.substring(ctxPath.length()); if (!localTarget.startsWith("/")) { getLog().warn( "isRedirectValid: Redirect target '{}' without servlet context path '{}' must be an absolute path", target, ctxPath); return false; } final int query = localTarget.indexOf('?'); final String path = (query > 0) ? localTarget.substring(0, query) : localTarget; ResourceResolver resolver = getResourceResolver(request); if (resolver != null) { // assume all is fine if the path resolves to a resource if (!ResourceUtil.isNonExistingResource(resolver.resolve(request, path))) { return true; } // not resolving to a resource, check for illegal characters } final Pattern illegal = Pattern.compile("[<>'\"]"); if (illegal.matcher(path).find()) { getLog().warn("isRedirectValid: Redirect target '{}' must not contain any of <>'\"", target); return false; } return true; } /** * Returns the context path from the request or an empty string if the * request is null. */ private static String getContextPath(final HttpServletRequest request) { if (request != null) { return request.getContextPath(); } return ""; } /** * Returns the resource resolver set as the * {@link AuthenticationSupport#REQUEST_ATTRIBUTE_RESOLVER} request * attribute or null if the request object is null * or the resource resolver is not present. */ private static ResourceResolver getResourceResolver(final HttpServletRequest request) { if (request != null) { return (ResourceResolver) request.getAttribute(AuthenticationSupport.REQUEST_ATTRIBUTE_RESOLVER); } return null; } /** * Returns true if the given request can be assumed to be sent * by a client browser such as Firefix, Internet Explorer, etc. *

* This method inspects the User-Agent header and returns * true if the header contains the string Mozilla (known * to be contained in Firefox, Internet Explorer, WebKit-based browsers * User-Agent) or Opera (known to be contained in the Opera * User-Agent). * * @param request The request to inspect * @return true if the request is assumed to be sent by a * browser. */ public static boolean isBrowserRequest(final HttpServletRequest request) { final String userAgent = request.getHeader(USER_AGENT); if (userAgent != null && (userAgent.contains(BROWSER_CLASS_MOZILLA) || userAgent.contains(BROWSER_CLASS_OPERA))) { return true; } return false; } /** * Returns true if the request is to be considered an AJAX * request placed using the XMLHttpRequest browser host object. * Currently a request is considered an AJAX request if the client sends the * X-Requested-With request header set to XMLHttpRequest * . * * @param request The current request * @return true if the request can be considered an AJAX * request. */ public static boolean isAjaxRequest(final HttpServletRequest request) { return XML_HTTP_REQUEST.equals(request.getHeader(X_REQUESTED_WITH)); } /** * Checks whether the response has already been committed. If so an * IllegalStateException is thrown. Otherwise the response * buffer is cleared leaving any headers and status already set untouched. * * @param response The response to check and reset. * @throws IllegalStateException if the response has already been committed */ private static void checkAndReset(final HttpServletResponse response) { if (response.isCommitted()) { throw new IllegalStateException("Response is already committed"); } response.resetBuffer(); } /** * Helper method returning a org.apache.sling.auth.core.AuthUtil logger. */ private static Logger getLog() { return LoggerFactory.getLogger("org.apache.sling.auth.core.AuthUtil"); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy