org.apache.sling.auth.core.impl.HttpBasicAuthenticationHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.sling.auth.core.impl.HttpBasicAuthenticationHandler.java

Source

/*
 * 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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.codec.binary.Base64;
import org.apache.sling.auth.core.AuthUtil;
import org.apache.sling.auth.core.spi.AuthenticationHandler;
import org.apache.sling.auth.core.spi.AuthenticationInfo;
import org.apache.sling.auth.core.spi.DefaultAuthenticationFeedbackHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The <code>HttpBasicAuthenticationHandler</code> class supports plain old HTTP
 * Basic authentication. While {@link #extractCredentials(HttpServletRequest)}
 * always accesses the header if called and if present, the
 * {@link #requestCredentials(HttpServletRequest, HttpServletResponse)} and
 * {@link #dropCredentials(HttpServletRequest, HttpServletResponse)} methods
 * must be explicitly enabled to send back a 401/UNAUTHORIZED reply to force the
 * client into HTTP Basic authentication.
 * <p>
 * Being able to just extract credentials but not actively request them provides
 * an easy way for tools (like cURL) or libraries (like Apache HttpCLient) to
 * preemptively authenticate with HTTP Basic authentication.
 */
class HttpBasicAuthenticationHandler extends DefaultAuthenticationFeedbackHandler implements AuthenticationHandler {

    private static final String HEADER_WWW_AUTHENTICATE = "WWW-Authenticate";

    private static final String HEADER_AUTHORIZATION = "Authorization";

    private static final String AUTHENTICATION_SCHEME_BASIC = "Basic";

    /** default log */
    private final Logger log = LoggerFactory.getLogger(getClass());

    /** The realm to send back with the 401 response */
    private final String realm;

    /**
     * Whether this authentication handler is fully enabled and sends back 401
     * responses from the
     * {@link #requestCredentials(HttpServletRequest, HttpServletResponse)} and
     * {@link #dropCredentials(HttpServletRequest, HttpServletResponse)}
     * methods.
     */
    private final boolean fullSupport;

    HttpBasicAuthenticationHandler(final String realm, final boolean fullSupport) {
        this.realm = realm;
        this.fullSupport = fullSupport;
    }

    // ----------- AuthenticationHandler interface ----------------------------

    /**
     * Returns the credential present within in an HTTP Basic authentication
     * header or <code>null</code> if no credentials are provided and the
     * {@link AuthenticationHandler#REQUEST_LOGIN_PARAMETER} is neither set as a
     * request parameter nor as a request attribute.
     * <p>
     * If the {@link AuthenticationHandler#REQUEST_LOGIN_PARAMETER} is set as a
     * request parameter or request attribute, a 401 response is sent to the
     * client and the method returns {@link AuthenticationInfo#DOING_AUTH} to
     * indicate that the handler has started its own credentials requesting.
     *
     * @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 A valid Credentials instance identifying the request user,
     *         DOING_AUTH if the handler is in an authentication transaction with
     *         the client or null if the request does not contain authentication
     *         information. In case of DOING_AUTH, the method has sent back a
     *         401 requesting the client to provide credentials.
     */
    public AuthenticationInfo extractCredentials(HttpServletRequest request, HttpServletResponse response) {

        // extract credentials and return
        AuthenticationInfo info = this.extractCredentials(request);
        if (info != null) {
            return info;
        }

        // no credentials, check whether the client wants to login
        if (forceAuthentication(request, response)) {
            return AuthenticationInfo.DOING_AUTH;
        }

        // no special header, so we will not authenticate here
        return null;
    }

    /**
     * Called by the SlingAuthenticator.login method in case no other
     * authentication handler was willing to request credentials from the
     * client. In this case this HTTP Basic authentication handler will send
     * back a {@link #sendUnauthorized(HttpServletResponse) 401 response} to
     * request HTTP Basic authentication from the client if full support has
     * been configured in the
     * {@link #HttpBasicAuthenticationHandler(String, boolean) constructor}
     *
     * @param request The request object
     * @param response The response object to which to send the request
     * @return <code>true</code> if full support is enabled and the 401 response
     *         could be sent. If full support is not enabled <code>false</code>
     *         is always returned.
     */
    public boolean requestCredentials(HttpServletRequest request, HttpServletResponse response) {
        return fullSupport ? sendUnauthorized(response) : false;
    }

    /**
     * Sends a 401/UNAUTHORIZED response if the request has an Authorization
     * header and if this handler is configured to actually send this response
     * in response to a request to drop the credentials; that is if full support
     * has been enabled in the
     * {@link #HttpBasicAuthenticationHandler(String, boolean) constructor}.
     * <p>
     * Note, that sending a 401/UNAUTHORIZED response is generally the only save
     * means to remove HTTP Basic credentials from a browser's cache. Yet, the
     * nasty side-effect is that the browser's login form is displayed as a
     * reaction to the 401/UNAUTHORIZED response.
     */
    public void dropCredentials(HttpServletRequest request, HttpServletResponse response) {
        if (fullSupport && request.getHeader(HEADER_AUTHORIZATION) != null) {
            sendUnauthorized(response);
        }
    }

    /**
     * Called if the credentials extracted by the
     * {@link #extractCredentials(HttpServletRequest, HttpServletResponse)}
     * method are not valid and sends back a 401/UNAUTHORIZED response
     * requesting the credentials again.
     * <p>
     * The only way to get a browser (or a client in general) into forgetting
     * the current credentials and sending different credentials is sending back
     * such a response. Otherwise the browser sends the same credentials over
     * and over again.
     * <p>
     * The assumption of this method unconditionally sending back the
     * 401/UNAUTHORIZED response is that this method here is only called if the
     * request actually provided invalid HTTP Basic credentials.
     * <p>
     * If the request is a
     * {@link AuthUtil#isValidateRequest(HttpServletRequest) validation request}
     * this method actually does nothing to allow for the expected 403/FORBIDDEN
     * response to be sent.
     */
    @Override
    public void authenticationFailed(HttpServletRequest request, HttpServletResponse response,
            AuthenticationInfo authInfo) {
        if (!AuthUtil.isValidateRequest(request)) {
            sendUnauthorized(response);
        }
    }

    /**
     * Returns true if the {@link #REQUEST_LOGIN_PARAMETER} parameter or request
     * attribute is set to any non-<code>null</code> value.
     * <p>
     * This method always returns <code>true</code> if the parameter or request
     * attribute is set regardless of its value because the client indicated it
     * wanted to login but no authentication handler was willing to actually
     * handle this request. So as a last fallback this handler request HTTP
     * Basic Credentials.
     *
     * @param request The request object providing the parameter or attribute.
     * @return <code>true</code> if the
     *         {@link AuthenticationHandler#REQUEST_LOGIN_PARAMETER} parameter
     *         or attribute is set to any value.
     */
    private boolean isLoginRequested(HttpServletRequest request) {
        return AuthUtil.getAttributeOrParameter(request, REQUEST_LOGIN_PARAMETER, null) != null;
    }

    /**
     * If the {@link #REQUEST_LOGIN_PARAMETER} parameter is set this method
     * sends status <code>401</code> (Unauthorized) with a
     * <code>WWW-Authenticate</code> requesting standard HTTP header
     * authentication with the <code>Basic</code> scheme and the configured
     * realm name. If the response is already committed, an error message is
     * logged but the 401 status is not sent.
     * <p>
     * <code>false</code> is returned if the request parameter is not set, if
     * the response is already committed or if an error occurred sending the
     * status response. The latter two situations are logged as errors.
     *
     * @param request The request object
     * @param response The response object to which to send the request
     * @return <code>true</code> if the 401/UNAUTHORIZED method has successfully
     *         been sent.
     */
    private boolean forceAuthentication(HttpServletRequest request, HttpServletResponse response) {

        // presume 401/UNAUTHORIZED has not been sent
        boolean authenticationForced = false;

        if (isLoginRequested(request)) {

            authenticationForced = sendUnauthorized(response);

        } else {

            log.debug("forceAuthentication: Not forcing authentication because request parameter {} is not set",
                    REQUEST_LOGIN_PARAMETER);

        }

        // true if 401/UNAUTHORIZED has been sent, false otherwise
        return authenticationForced;
    }

    /**
     * Sends status <code>401</code> (Unauthorized) with a
     * <code>WWW-Authenticate</code> requesting standard HTTP header
     * authentication with the <code>Basic</code> scheme and the configured
     * realm name.
     *
     * @param response The response object to which to send the request
     * @return <code>true</code> if the 401/UNAUTHORIZED method has successfully
     *         been sent and the response has been committed.
     */
    boolean sendUnauthorized(HttpServletResponse response) {

        if (response.isCommitted()) {

            log.error("sendUnauthorized: Cannot send 401/UNAUTHORIZED; response is already committed");

        } else {

            response.resetBuffer();

            /*
             * TODO: Check whether we have to redirect
             * If this is a GET request not targeted at the registration path
             * for which this handler is selected we have to redirect to the
             * registration path using either the provided resource attribute
             * or parameter or the current URL as the "resource" parameter
             * for the redirect and also setting the "sling:authRequestLogin"
             * parameter to "BASIC" to get the 401 response for the registration
             * path and redirect back to actual path afterwards.
             */

            // just set the status because this may be called as part of an
            // error handler in which case sendError would result in an error
            // handler loop and thus be ignored.
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setHeader(HEADER_WWW_AUTHENTICATE,
                    AUTHENTICATION_SCHEME_BASIC + " realm=\"" + this.realm + "\"");

            try {
                response.flushBuffer();
                return true;
            } catch (IOException ioe) {
                log.error("sendUnauthorized: Failed requesting authentication", ioe);
            }
        }

        return false;
    }

    @Override
    public String toString() {
        return "HTTP Basic Authentication Handler (" + (fullSupport ? "enabled" : "preemptive") + ")";
    }

    // ---------- internal -----------------------------------------------------

    /**
     * Extract the Base64 authentication string from the request
     */
    protected AuthenticationInfo extractCredentials(HttpServletRequest request) {

        // Return immediately if the header is missing
        String authHeader = request.getHeader(HEADER_AUTHORIZATION);
        if (authHeader == null || authHeader.length() == 0) {
            return null;
        }

        // Get the authType (Basic, Digest) and authInfo (user/password) from
        // the header
        authHeader = authHeader.trim();
        int blank = authHeader.indexOf(' ');
        if (blank <= 0) {
            return null;
        }
        String authType = authHeader.substring(0, blank);
        String authInfo = authHeader.substring(blank).trim();

        // Check whether authorization type matches
        if (!authType.equalsIgnoreCase(AUTHENTICATION_SCHEME_BASIC)) {
            return null;
        }

        // Base64 decode and split on colon

        // we cannot use default base64, since we need iso encoding
        // (nb: ISO-8859-1 is required as per API spec to be available)
        String decoded;
        try {
            byte[] encoded = authInfo.getBytes("ISO-8859-1");
            byte[] bytes = Base64.decodeBase64(encoded);
            decoded = new String(bytes, "ISO-8859-1");
        } catch (UnsupportedEncodingException uee) {
            // unexpected
            log.error("extractAuthentication: Cannot en/decode authentication info", uee);
            return null;
        }

        final int colIdx = decoded.indexOf(':');
        final String userId;
        final char[] password;
        if (colIdx < 0) {
            userId = decoded;
            password = new char[0];
        } else {
            userId = decoded.substring(0, colIdx);
            password = decoded.substring(colIdx + 1).toCharArray();
        }

        return new AuthenticationInfo(HttpServletRequest.BASIC_AUTH, userId, password);
    }
}