blob: 76abd9e516565f62a74edfc3315e2c43b0a97c79 [file] [log] [blame]
/*
* 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);
}
}