// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "net/http/http_auth_controller.h"
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/metrics/histogram.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/platform_thread.h"
#include "net/base/auth.h"
#include "net/base/net_util.h"
#include "net/dns/host_resolver.h"
#include "net/http/http_auth_handler.h"
#include "net/http/http_auth_handler_factory.h"
#include "net/http/http_network_session.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_request_info.h"
#include "net/http/http_response_headers.h"
namespace net {
namespace {
// Returns a log message for all the response headers related to the auth
// challenge.
std::string AuthChallengeLogMessage(HttpResponseHeaders* headers) {
std::string msg;
std::string header_val;
void* iter = NULL;
while (headers->EnumerateHeader(&iter, "proxy-authenticate", &header_val)) {
msg.append("\n Has header Proxy-Authenticate: ");
msg.append(header_val);
}
iter = NULL;
while (headers->EnumerateHeader(&iter, "www-authenticate", &header_val)) {
msg.append("\n Has header WWW-Authenticate: ");
msg.append(header_val);
}
// RFC 4559 requires that a proxy indicate its support of NTLM/Negotiate
// authentication with a "Proxy-Support: Session-Based-Authentication"
// response header.
iter = NULL;
while (headers->EnumerateHeader(&iter, "proxy-support", &header_val)) {
msg.append("\n Has header Proxy-Support: ");
msg.append(header_val);
}
return msg;
}
enum AuthEvent {
AUTH_EVENT_START = 0,
AUTH_EVENT_REJECT,
AUTH_EVENT_MAX,
};
enum AuthTarget {
AUTH_TARGET_PROXY = 0,
AUTH_TARGET_SECURE_PROXY,
AUTH_TARGET_SERVER,
AUTH_TARGET_SECURE_SERVER,
AUTH_TARGET_MAX,
};
AuthTarget DetermineAuthTarget(const HttpAuthHandler* handler) {
switch (handler->target()) {
case HttpAuth::AUTH_PROXY:
if (handler->origin().SchemeIsSecure())
return AUTH_TARGET_SECURE_PROXY;
else
return AUTH_TARGET_PROXY;
case HttpAuth::AUTH_SERVER:
if (handler->origin().SchemeIsSecure())
return AUTH_TARGET_SECURE_SERVER;
else
return AUTH_TARGET_SERVER;
default:
NOTREACHED();
return AUTH_TARGET_MAX;
}
}
// Records the number of authentication events per authentication scheme.
void HistogramAuthEvent(HttpAuthHandler* handler, AuthEvent auth_event) {
#if !defined(NDEBUG)
// Note: The on-same-thread check is intentionally not using a lock
// to protect access to first_thread. This method is meant to be only
// used on the same thread, in which case there are no race conditions. If
// there are race conditions (say, a read completes during a partial write),
// the DCHECK will correctly fail.
static base::PlatformThreadId first_thread =
base::PlatformThread::CurrentId();
DCHECK_EQ(first_thread, base::PlatformThread::CurrentId());
#endif
HttpAuth::Scheme auth_scheme = handler->auth_scheme();
DCHECK(auth_scheme >= 0 && auth_scheme < HttpAuth::AUTH_SCHEME_MAX);
// Record start and rejection events for authentication.
//
// The results map to:
// Basic Start: 0
// Basic Reject: 1
// Digest Start: 2
// Digest Reject: 3
// NTLM Start: 4
// NTLM Reject: 5
// Negotiate Start: 6
// Negotiate Reject: 7
static const int kEventBucketsEnd =
HttpAuth::AUTH_SCHEME_MAX * AUTH_EVENT_MAX;
int event_bucket = auth_scheme * AUTH_EVENT_MAX + auth_event;
DCHECK(event_bucket >= 0 && event_bucket < kEventBucketsEnd);
UMA_HISTOGRAM_ENUMERATION("Net.HttpAuthCount", event_bucket,
kEventBucketsEnd);
// Record the target of the authentication.
//
// The results map to:
// Basic Proxy: 0
// Basic Secure Proxy: 1
// Basic Server: 2
// Basic Secure Server: 3
// Digest Proxy: 4
// Digest Secure Proxy: 5
// Digest Server: 6
// Digest Secure Server: 7
// NTLM Proxy: 8
// NTLM Secure Proxy: 9
// NTLM Server: 10
// NTLM Secure Server: 11
// Negotiate Proxy: 12
// Negotiate Secure Proxy: 13
// Negotiate Server: 14
// Negotiate Secure Server: 15
if (auth_event != AUTH_EVENT_START)
return;
static const int kTargetBucketsEnd =
HttpAuth::AUTH_SCHEME_MAX * AUTH_TARGET_MAX;
AuthTarget auth_target = DetermineAuthTarget(handler);
int target_bucket = auth_scheme * AUTH_TARGET_MAX + auth_target;
DCHECK(target_bucket >= 0 && target_bucket < kTargetBucketsEnd);
UMA_HISTOGRAM_ENUMERATION("Net.HttpAuthTarget", target_bucket,
kTargetBucketsEnd);
}
} // namespace
HttpAuthController::HttpAuthController(
HttpAuth::Target target,
const GURL& auth_url,
HttpAuthCache* http_auth_cache,
HttpAuthHandlerFactory* http_auth_handler_factory)
: target_(target),
auth_url_(auth_url),
auth_origin_(auth_url.GetOrigin()),
auth_path_(HttpAuth::AUTH_PROXY ? std::string() : auth_url.path()),
embedded_identity_used_(false),
default_credentials_used_(false),
http_auth_cache_(http_auth_cache),
http_auth_handler_factory_(http_auth_handler_factory) {
}
HttpAuthController::~HttpAuthController() {
DCHECK(CalledOnValidThread());
}
int HttpAuthController::MaybeGenerateAuthToken(
const HttpRequestInfo* request, const CompletionCallback& callback,
const BoundNetLog& net_log) {
DCHECK(CalledOnValidThread());
bool needs_auth = HaveAuth() || SelectPreemptiveAuth(net_log);
if (!needs_auth)
return OK;
const AuthCredentials* credentials = NULL;
if (identity_.source != HttpAuth::IDENT_SRC_DEFAULT_CREDENTIALS)
credentials = &identity_.credentials;
DCHECK(auth_token_.empty());
DCHECK(callback_.is_null());
int rv = handler_->GenerateAuthToken(
credentials, request,
base::Bind(&HttpAuthController::OnIOComplete, base::Unretained(this)),
&auth_token_);
if (DisableOnAuthHandlerResult(rv))
rv = OK;
if (rv == ERR_IO_PENDING)
callback_ = callback;
else
OnIOComplete(rv);
return rv;
}
bool HttpAuthController::SelectPreemptiveAuth(const BoundNetLog& net_log) {
DCHECK(CalledOnValidThread());
DCHECK(!HaveAuth());
DCHECK(identity_.invalid);
// Don't do preemptive authorization if the URL contains a username:password,
// since we must first be challenged in order to use the URL's identity.
if (auth_url_.has_username())
return false;
// SelectPreemptiveAuth() is on the critical path for each request, so it
// is expected to be fast. LookupByPath() is fast in the common case, since
// the number of http auth cache entries is expected to be very small.
// (For most users in fact, it will be 0.)
HttpAuthCache::Entry* entry = http_auth_cache_->LookupByPath(
auth_origin_, auth_path_);
if (!entry)
return false;
// Try to create a handler using the previous auth challenge.
scoped_ptr<HttpAuthHandler> handler_preemptive;
int rv_create = http_auth_handler_factory_->
CreatePreemptiveAuthHandlerFromString(entry->auth_challenge(), target_,
auth_origin_,
entry->IncrementNonceCount(),
net_log, &handler_preemptive);
if (rv_create != OK)
return false;
// Set the state
identity_.source = HttpAuth::IDENT_SRC_PATH_LOOKUP;
identity_.invalid = false;
identity_.credentials = entry->credentials();
handler_.swap(handler_preemptive);
return true;
}
void HttpAuthController::AddAuthorizationHeader(
HttpRequestHeaders* authorization_headers) {
DCHECK(CalledOnValidThread());
DCHECK(HaveAuth());
// auth_token_ can be empty if we encountered a permanent error with
// the auth scheme and want to retry.
if (!auth_token_.empty()) {
authorization_headers->SetHeader(
HttpAuth::GetAuthorizationHeaderName(target_), auth_token_);
auth_token_.clear();
}
}
int HttpAuthController::HandleAuthChallenge(
scoped_refptr<HttpResponseHeaders> headers,
bool do_not_send_server_auth,
bool establishing_tunnel,
const BoundNetLog& net_log) {
DCHECK(CalledOnValidThread());
DCHECK(headers.get());
DCHECK(auth_origin_.is_valid());
VLOG(1) << "The " << HttpAuth::GetAuthTargetString(target_) << " "
<< auth_origin_ << " requested auth "
<< AuthChallengeLogMessage(headers.get());
// Give the existing auth handler first try at the authentication headers.
// This will also evict the entry in the HttpAuthCache if the previous
// challenge appeared to be rejected, or is using a stale nonce in the Digest
// case.
if (HaveAuth()) {
std::string challenge_used;
HttpAuth::AuthorizationResult result =
HttpAuth::HandleChallengeResponse(handler_.get(),
headers.get(),
target_,
disabled_schemes_,
&challenge_used);
switch (result) {
case HttpAuth::AUTHORIZATION_RESULT_ACCEPT:
break;
case HttpAuth::AUTHORIZATION_RESULT_INVALID:
InvalidateCurrentHandler(INVALIDATE_HANDLER_AND_CACHED_CREDENTIALS);
break;
case HttpAuth::AUTHORIZATION_RESULT_REJECT:
HistogramAuthEvent(handler_.get(), AUTH_EVENT_REJECT);
InvalidateCurrentHandler(INVALIDATE_HANDLER_AND_CACHED_CREDENTIALS);
break;
case HttpAuth::AUTHORIZATION_RESULT_STALE:
if (http_auth_cache_->UpdateStaleChallenge(auth_origin_,
handler_->realm(),
handler_->auth_scheme(),
challenge_used)) {
InvalidateCurrentHandler(INVALIDATE_HANDLER);
} else {
// It's possible that a server could incorrectly issue a stale
// response when the entry is not in the cache. Just evict the
// current value from the cache.
InvalidateCurrentHandler(INVALIDATE_HANDLER_AND_CACHED_CREDENTIALS);
}
break;
case HttpAuth::AUTHORIZATION_RESULT_DIFFERENT_REALM:
// If the server changes the authentication realm in a
// subsequent challenge, invalidate cached credentials for the
// previous realm. If the server rejects a preemptive
// authorization and requests credentials for a different
// realm, we keep the cached credentials.
InvalidateCurrentHandler(
(identity_.source == HttpAuth::IDENT_SRC_PATH_LOOKUP) ?
INVALIDATE_HANDLER :
INVALIDATE_HANDLER_AND_CACHED_CREDENTIALS);
break;
default:
NOTREACHED();
break;
}
}
identity_.invalid = true;
bool can_send_auth = (target_ != HttpAuth::AUTH_SERVER ||
!do_not_send_server_auth);
do {
if (!handler_.get() && can_send_auth) {
// Find the best authentication challenge that we support.
HttpAuth::ChooseBestChallenge(http_auth_handler_factory_,
headers.get(),
target_,
auth_origin_,
disabled_schemes_,
net_log,
&handler_);
if (handler_.get())
HistogramAuthEvent(handler_.get(), AUTH_EVENT_START);
}
if (!handler_.get()) {
if (establishing_tunnel) {
LOG(ERROR) << "Can't perform auth to the "
<< HttpAuth::GetAuthTargetString(target_) << " "
<< auth_origin_ << " when establishing a tunnel"
<< AuthChallengeLogMessage(headers.get());
// We are establishing a tunnel, we can't show the error page because an
// active network attacker could control its contents. Instead, we just
// fail to establish the tunnel.
DCHECK(target_ == HttpAuth::AUTH_PROXY);
return ERR_PROXY_AUTH_UNSUPPORTED;
}
// We found no supported challenge -- let the transaction continue so we
// end up displaying the error page.
return OK;
}
if (handler_->NeedsIdentity()) {
// Pick a new auth identity to try, by looking to the URL and auth cache.
// If an identity to try is found, it is saved to identity_.
SelectNextAuthIdentityToTry();
} else {
// Proceed with the existing identity or a null identity.
identity_.invalid = false;
}
// From this point on, we are restartable.
if (identity_.invalid) {
// We have exhausted all identity possibilities.
if (!handler_->AllowsExplicitCredentials()) {
// If the handler doesn't accept explicit credentials, then we need to
// choose a different auth scheme.
HistogramAuthEvent(handler_.get(), AUTH_EVENT_REJECT);
InvalidateCurrentHandler(INVALIDATE_HANDLER_AND_DISABLE_SCHEME);
} else {
// Pass the challenge information back to the client.
PopulateAuthChallenge();
}
} else {
auth_info_ = NULL;
}
// If we get here and we don't have a handler_, that's because we
// invalidated it due to not having any viable identities to use with it. Go
// back and try again.
// TODO(asanka): Instead we should create a priority list of
// <handler,identity> and iterate through that.
} while(!handler_.get());
return OK;
}
void HttpAuthController::ResetAuth(const AuthCredentials& credentials) {
DCHECK(CalledOnValidThread());
DCHECK(identity_.invalid || credentials.Empty());
if (identity_.invalid) {
// Update the credentials.
identity_.source = HttpAuth::IDENT_SRC_EXTERNAL;
identity_.invalid = false;
identity_.credentials = credentials;
}
DCHECK(identity_.source != HttpAuth::IDENT_SRC_PATH_LOOKUP);
// Add the auth entry to the cache before restarting. We don't know whether
// the identity is valid yet, but if it is valid we want other transactions
// to know about it. If an entry for (origin, handler->realm()) already
// exists, we update it.
//
// If identity_.source is HttpAuth::IDENT_SRC_NONE or
// HttpAuth::IDENT_SRC_DEFAULT_CREDENTIALS, identity_ contains no
// identity because identity is not required yet or we're using default
// credentials.
//
// TODO(wtc): For NTLM_SSPI, we add the same auth entry to the cache in
// round 1 and round 2, which is redundant but correct. It would be nice
// to add an auth entry to the cache only once, preferrably in round 1.
// See http://crbug.com/21015.
switch (identity_.source) {
case HttpAuth::IDENT_SRC_NONE:
case HttpAuth::IDENT_SRC_DEFAULT_CREDENTIALS:
break;
default:
http_auth_cache_->Add(auth_origin_, handler_->realm(),
handler_->auth_scheme(), handler_->challenge(),
identity_.credentials, auth_path_);
break;
}
}
bool HttpAuthController::HaveAuthHandler() const {
return handler_.get() != NULL;
}
bool HttpAuthController::HaveAuth() const {
return handler_.get() && !identity_.invalid;
}
void HttpAuthController::InvalidateCurrentHandler(
InvalidateHandlerAction action) {
DCHECK(CalledOnValidThread());
DCHECK(handler_.get());
if (action == INVALIDATE_HANDLER_AND_CACHED_CREDENTIALS)
InvalidateRejectedAuthFromCache();
if (action == INVALIDATE_HANDLER_AND_DISABLE_SCHEME)
DisableAuthScheme(handler_->auth_scheme());
handler_.reset();
identity_ = HttpAuth::Identity();
}
void HttpAuthController::InvalidateRejectedAuthFromCache() {
DCHECK(CalledOnValidThread());
DCHECK(HaveAuth());
// Clear the cache entry for the identity we just failed on.
// Note: we require the credentials to match before invalidating
// since the entry in the cache may be newer than what we used last time.
http_auth_cache_->Remove(auth_origin_, handler_->realm(),
handler_->auth_scheme(), identity_.credentials);
}
bool HttpAuthController::SelectNextAuthIdentityToTry() {
DCHECK(CalledOnValidThread());
DCHECK(handler_.get());
DCHECK(identity_.invalid);
// Try to use the username:password encoded into the URL first.
if (target_ == HttpAuth::AUTH_SERVER && auth_url_.has_username() &&
!embedded_identity_used_) {
identity_.source = HttpAuth::IDENT_SRC_URL;
identity_.invalid = false;
// Extract the username:password from the URL.
base::string16 username;
base::string16 password;
GetIdentityFromURL(auth_url_, &username, &password);
identity_.credentials.Set(username, password);
embedded_identity_used_ = true;
// TODO(eroman): If the password is blank, should we also try combining
// with a password from the cache?
UMA_HISTOGRAM_BOOLEAN("net.HttpIdentSrcURL", true);
return true;
}
// Check the auth cache for a realm entry.
HttpAuthCache::Entry* entry =
http_auth_cache_->Lookup(auth_origin_, handler_->realm(),
handler_->auth_scheme());
if (entry) {
identity_.source = HttpAuth::IDENT_SRC_REALM_LOOKUP;
identity_.invalid = false;
identity_.credentials = entry->credentials();
return true;
}
// Use default credentials (single sign on) if this is the first attempt
// at identity. Do not allow multiple times as it will infinite loop.
// We use default credentials after checking the auth cache so that if
// single sign-on doesn't work, we won't try default credentials for future
// transactions.
if (!default_credentials_used_ && handler_->AllowsDefaultCredentials()) {
identity_.source = HttpAuth::IDENT_SRC_DEFAULT_CREDENTIALS;
identity_.invalid = false;
default_credentials_used_ = true;
return true;
}
return false;
}
void HttpAuthController::PopulateAuthChallenge() {
DCHECK(CalledOnValidThread());
// Populates response_.auth_challenge with the authentication challenge info.
// This info is consumed by URLRequestHttpJob::GetAuthChallengeInfo().
auth_info_ = new AuthChallengeInfo;
auth_info_->is_proxy = (target_ == HttpAuth::AUTH_PROXY);
auth_info_->challenger = HostPortPair::FromURL(auth_origin_);
auth_info_->scheme = HttpAuth::SchemeToString(handler_->auth_scheme());
auth_info_->realm = handler_->realm();
}
bool HttpAuthController::DisableOnAuthHandlerResult(int result) {
DCHECK(CalledOnValidThread());
switch (result) {
// Occurs with GSSAPI, if the user has not already logged in.
case ERR_MISSING_AUTH_CREDENTIALS:
// Can occur with GSSAPI or SSPI if the underlying library reports
// a permanent error.
case ERR_UNSUPPORTED_AUTH_SCHEME:
// These two error codes represent failures we aren't handling.
case ERR_UNEXPECTED_SECURITY_LIBRARY_STATUS:
case ERR_UNDOCUMENTED_SECURITY_LIBRARY_STATUS:
// Can be returned by SSPI if the authenticating authority or
// target is not known.
case ERR_MISCONFIGURED_AUTH_ENVIRONMENT:
// In these cases, disable the current scheme as it cannot
// succeed.
DisableAuthScheme(handler_->auth_scheme());
auth_token_.clear();
return true;
default:
return false;
}
}
void HttpAuthController::OnIOComplete(int result) {
DCHECK(CalledOnValidThread());
if (DisableOnAuthHandlerResult(result))
result = OK;
if (!callback_.is_null()) {
CompletionCallback c = callback_;
callback_.Reset();
c.Run(result);
}
}
scoped_refptr<AuthChallengeInfo> HttpAuthController::auth_info() {
DCHECK(CalledOnValidThread());
return auth_info_;
}
bool HttpAuthController::IsAuthSchemeDisabled(HttpAuth::Scheme scheme) const {
DCHECK(CalledOnValidThread());
return disabled_schemes_.find(scheme) != disabled_schemes_.end();
}
void HttpAuthController::DisableAuthScheme(HttpAuth::Scheme scheme) {
DCHECK(CalledOnValidThread());
disabled_schemes_.insert(scheme);
}
void HttpAuthController::DisableEmbeddedIdentity() {
DCHECK(CalledOnValidThread());
embedded_identity_used_ = true;
}
} // namespace net