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