/* * 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.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 <code>AuthUtil</code> provides utility functions for implementations of * {@link org.apache.sling.auth.core.spi.AuthenticationHandler} services and * users of the Sling authentication infrastructure. * <p> * 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}. * <p> * 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: * <ol> * <li>If there is a request attribute of that name, which is a non-empty * string, it is returned.</li> * <li>If there is a non-empty request parameter of * that name, this parameter is returned. </li> * <li>Otherwise the <code>defaultValue</code> is returned.</li> * </ol> * * @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 <code>defaultValue</code> 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 * <code>defaultLoginResource</code> parameter. First the * <code>resource</code> request attribute is checked. If it is a non-empty * string, it is returned. Second the <code>resource</code> 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 * <code>defaultLoginResource</code>. */ 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: * <ol> * <li>If the {@link Authenticator#LOGIN_RESOURCE} request parameter is set * to a non-empty string, that parameter is set</li> * <li>Otherwise if the <code>defaultValue</code> is a non-empty string the * default value is used</li> * <li>Otherwise the attribute is set to "/"</li> * </ol> * * @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 <code>null</code> 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. * <p> * This method implements the following functionality: * <ul> * <li>If the <code>params</code> map does not contain a (non- * <code>null</code>) 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 <code>request</code>.</li> * <li>The parameters from the <code>params</code> 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 * <code>java.net.URLEncoder</code> with UTF-8 encoding to make it safe for * requests</li> * </ul> * <p> * After checking the redirect target and creating the target URL from the * parameter map, the response buffer is reset and the * <code>HttpServletResponse.sendRedirect</code> 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 <code>params</code> 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 <code>null</code>. * @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<String, String> 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<String, String>(); } // 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<Entry<String, String>> ei = params.entrySet().iterator(); while (ei.hasNext()) { Entry<String, String> 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 <code>null</code> 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 <code>true</code> if the the client just asks for validation of * submitted username/password credentials. * <p> * This implementation returns <code>true</code> if the request parameter * {@link AuthConstants#PAR_J_VALIDATE} is set to <code>true</code> (case-insensitve). If * the request parameter is not set or to any value other than * <code>true</code> this method returns <code>false</code>. * * @param request The request to provide the parameter to check * @return <code>true</code> if the {@link AuthConstants#PAR_J_VALIDATE} parameter is set * to <code>true</code>. */ 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. * <p> * 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. * <p> * 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 <code>true</code> if the given redirect <code>target</code> is * valid according to the following list of requirements: * <ul> * <li>The <code>target</code> is neither <code>null</code> nor an empty * string</li> * <li>The <code>target</code> is not an URL which is identified by the * character sequence <code>://</code> separating the scheme from the host</li> * <li>The <code>target</code> is normalized such that it contains no * consecutive slashes and no path segment contains a single or double dot</li> * <li>The <code>target</code> must be prefixed with the servlet context * path</li> * <li>If a <code>ResourceResolver</code> is available as a request * attribute the <code>target</code> (without the servlet context path * prefix) must resolve to an existing resource</li> * <li>If a <code>ResourceResolver</code> is <i>not</i> available as a * request attribute the <code>target</code> must be an absolute path * starting with a slash character does not contain any of the characters * <code><</code>, <code>></code>, <code>'</code>, or <code>"</code> * in plain or URL encoding</li> * </ul> * <p> * If any of the conditions does not hold, the method returns * <code>false</code> and logs a <i>warning</i> level message with the * <i>org.apache.sling.auth.core.AuthUtil</i> logger. * * @param request Providing the <code>ResourceResolver</code> attribute and * the context to resolve the resource from the * <code>target</code>. This may be <code>null</code> which * causes the target to not be validated with a * <code>ResoureResolver</code> * @param target The redirect target to validate. This path must be * prefixed with the request's servlet context path. * @return <code>true</code> 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; } 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 <code>null</code>. */ 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 <code>null</code> if the request object is <code>null</code> * 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 <code>true</code> if the given request can be assumed to be sent * by a client browser such as Firefix, Internet Explorer, etc. * <p> * This method inspects the <code>User-Agent</code> header and returns * <code>true</code> if the header contains the string <i>Mozilla</i> (known * to be contained in Firefox, Internet Explorer, WebKit-based browsers * User-Agent) or <i>Opera</i> (known to be contained in the Opera * User-Agent). * * @param request The request to inspect * @return <code>true</code> 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 <code>true</code> if the request is to be considered an AJAX * request placed using the <code>XMLHttpRequest</code> browser host object. * Currently a request is considered an AJAX request if the client sends the * <i>X-Requested-With</i> request header set to <code>XMLHttpRequest</code> * . * * @param request The current request * @return <code>true</code> 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 * <code>IllegalStateException</code> 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 <i>org.apache.sling.auth.core.AuthUtil</i> logger. */ private static Logger getLog() { return LoggerFactory.getLogger("org.apache.sling.auth.core.AuthUtil"); } }