// Copyright 2012 The Chromium Authors // 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 #include "base/functional/bind.h" #include "base/functional/callback_helpers.h" #include "base/metrics/histogram_macros.h" #include "base/strings/string_util.h" #include "base/strings/utf_string_conversions.h" #include "base/threading/platform_thread.h" #include "base/values.h" #include "net/base/auth.h" #include "net/base/url_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" #include "net/log/net_log_event_type.h" #include "net/log/net_log_source.h" #include "net/log/net_log_source_type.h" #include "net/log/net_log_with_source.h" #include "url/scheme_host_port.h" namespace net { namespace { 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 (GURL::SchemeIsCryptographic(handler->scheme_host_port().scheme())) { return AUTH_TARGET_SECURE_PROXY; } else { return AUTH_TARGET_PROXY; } case HttpAuth::AUTH_SERVER: if (GURL::SchemeIsCryptographic(handler->scheme_host_port().scheme())) { return AUTH_TARGET_SECURE_SERVER; } else { return AUTH_TARGET_SERVER; } default: NOTREACHED_NORETURN(); } } // 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 constexpr int kEventBucketsEnd = int{HttpAuth::AUTH_SCHEME_MAX} * AUTH_EVENT_MAX; int event_bucket = int{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 constexpr int kTargetBucketsEnd = int{HttpAuth::AUTH_SCHEME_MAX} * AUTH_TARGET_MAX; AuthTarget auth_target = DetermineAuthTarget(handler); int target_bucket = int{auth_scheme} * AUTH_TARGET_MAX + auth_target; DCHECK(target_bucket >= 0 && target_bucket < kTargetBucketsEnd); UMA_HISTOGRAM_ENUMERATION("Net.HttpAuthTarget", target_bucket, kTargetBucketsEnd); } base::Value::Dict ControllerParamsToValue(HttpAuth::Target target, const GURL& url) { base::Value::Dict params; params.Set("target", HttpAuth::GetAuthTargetString(target)); params.Set("url", url.spec()); return params; } } // namespace HttpAuthController::HttpAuthController( HttpAuth::Target target, const GURL& auth_url, const NetworkAnonymizationKey& network_anonymization_key, HttpAuthCache* http_auth_cache, HttpAuthHandlerFactory* http_auth_handler_factory, HostResolver* host_resolver) : target_(target), auth_url_(auth_url), auth_scheme_host_port_(auth_url), auth_path_(auth_url.path()), network_anonymization_key_(network_anonymization_key), http_auth_cache_(http_auth_cache), http_auth_handler_factory_(http_auth_handler_factory), host_resolver_(host_resolver) { DCHECK(target != HttpAuth::AUTH_PROXY || auth_path_ == "/"); DCHECK(auth_scheme_host_port_.IsValid()); } HttpAuthController::~HttpAuthController() { DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); if (net_log_.source().IsValid()) net_log_.EndEvent(NetLogEventType::AUTH_CONTROLLER); } void HttpAuthController::BindToCallingNetLog( const NetLogWithSource& caller_net_log) { if (!net_log_.source().IsValid()) { net_log_ = NetLogWithSource::Make(caller_net_log.net_log(), NetLogSourceType::HTTP_AUTH_CONTROLLER); net_log_.BeginEvent(NetLogEventType::AUTH_CONTROLLER, [&] { return ControllerParamsToValue(target_, auth_url_); }); } caller_net_log.AddEventReferencingSource( NetLogEventType::AUTH_BOUND_TO_CONTROLLER, net_log_.source()); } int HttpAuthController::MaybeGenerateAuthToken( const HttpRequestInfo* request, CompletionOnceCallback callback, const NetLogWithSource& caller_net_log) { DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); DCHECK(!auth_info_); bool needs_auth = HaveAuth() || SelectPreemptiveAuth(caller_net_log); if (!needs_auth) return OK; net_log_.BeginEventReferencingSource(NetLogEventType::AUTH_GENERATE_TOKEN, caller_net_log.source()); const AuthCredentials* credentials = nullptr; 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::BindOnce(&HttpAuthController::OnGenerateAuthTokenDone, base::Unretained(this)), &auth_token_); if (rv == ERR_IO_PENDING) { callback_ = std::move(callback); return rv; } return HandleGenerateTokenResult(rv); } bool HttpAuthController::SelectPreemptiveAuth( const NetLogWithSource& caller_net_log) { DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); 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_scheme_host_port_, target_, network_anonymization_key_, auth_path_); if (!entry) return false; BindToCallingNetLog(caller_net_log); // Try to create a handler using the previous auth challenge. std::unique_ptr handler_preemptive; int rv_create = http_auth_handler_factory_->CreatePreemptiveAuthHandlerFromString( entry->auth_challenge(), target_, network_anonymization_key_, auth_scheme_host_port_, entry->IncrementNonceCount(), net_log_, host_resolver_, &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_CALLED_ON_VALID_THREAD(thread_checker_); 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 headers, const SSLInfo& ssl_info, bool do_not_send_server_auth, bool establishing_tunnel, const NetLogWithSource& caller_net_log) { DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); DCHECK(headers.get()); DCHECK(auth_scheme_host_port_.IsValid()); DCHECK(!auth_info_); BindToCallingNetLog(caller_net_log); net_log_.BeginEventReferencingSource(NetLogEventType::AUTH_HANDLE_CHALLENGE, caller_net_log.source()); // 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, 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_scheme_host_port_, target_, handler_->realm(), handler_->auth_scheme(), network_anonymization_key_, 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, ssl_info, network_anonymization_key_, target_, auth_scheme_host_port_, disabled_schemes_, net_log_, host_resolver_, &handler_); if (handler_.get()) { HistogramAuthEvent(handler_.get(), AUTH_EVENT_START); } } if (!handler_.get()) { if (establishing_tunnel) { // 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_EQ(target_, HttpAuth::AUTH_PROXY); net_log_.EndEventWithNetErrorCode( NetLogEventType::AUTH_HANDLE_CHALLENGE, ERR_PROXY_AUTH_UNSUPPORTED); return ERR_PROXY_AUTH_UNSUPPORTED; } // We found no supported challenge -- let the transaction continue so we // end up displaying the error page. net_log_.EndEvent(NetLogEventType::AUTH_HANDLE_CHALLENGE); 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(); } } // 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 // and iterate through that. } while(!handler_.get()); net_log_.EndEvent(NetLogEventType::AUTH_HANDLE_CHALLENGE); return OK; } void HttpAuthController::ResetAuth(const AuthCredentials& credentials) { DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); DCHECK(identity_.invalid || credentials.Empty()); if (identity_.invalid) { // Update the credentials. identity_.source = HttpAuth::IDENT_SRC_EXTERNAL; identity_.invalid = false; identity_.credentials = credentials; // auth_info_ is no longer necessary. auth_info_ = std::nullopt; } 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_scheme_host_port_, target_, handler_->realm(), handler_->auth_scheme(), network_anonymization_key_, handler_->challenge(), identity_.credentials, auth_path_); break; } } bool HttpAuthController::HaveAuthHandler() const { return handler_.get() != nullptr; } bool HttpAuthController::HaveAuth() const { return handler_.get() && !identity_.invalid; } bool HttpAuthController::NeedsHTTP11() const { return handler_ && handler_->is_connection_based(); } void HttpAuthController::InvalidateCurrentHandler( InvalidateHandlerAction action) { DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); DCHECK(handler_.get()); switch (action) { case INVALIDATE_HANDLER_AND_CACHED_CREDENTIALS: InvalidateRejectedAuthFromCache(); break; case INVALIDATE_HANDLER_AND_DISABLE_SCHEME: DisableAuthScheme(handler_->auth_scheme()); break; case INVALIDATE_HANDLER: PrepareIdentityForReuse(); break; } handler_.reset(); identity_ = HttpAuth::Identity(); } void HttpAuthController::InvalidateRejectedAuthFromCache() { DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); 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_scheme_host_port_, target_, handler_->realm(), handler_->auth_scheme(), network_anonymization_key_, identity_.credentials); } void HttpAuthController::PrepareIdentityForReuse() { if (identity_.invalid) return; switch (identity_.source) { case HttpAuth::IDENT_SRC_DEFAULT_CREDENTIALS: DCHECK(default_credentials_used_); default_credentials_used_ = false; break; case HttpAuth::IDENT_SRC_URL: DCHECK(embedded_identity_used_); embedded_identity_used_ = false; break; case HttpAuth::IDENT_SRC_NONE: case HttpAuth::IDENT_SRC_PATH_LOOKUP: case HttpAuth::IDENT_SRC_REALM_LOOKUP: case HttpAuth::IDENT_SRC_EXTERNAL: break; } } bool HttpAuthController::SelectNextAuthIdentityToTry() { DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); 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. std::u16string username; std::u16string 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? return true; } // Check the auth cache for a realm entry. HttpAuthCache::Entry* entry = http_auth_cache_->Lookup( auth_scheme_host_port_, target_, handler_->realm(), handler_->auth_scheme(), network_anonymization_key_); 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 they're allowed and this is the // first attempt at using an 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_CALLED_ON_VALID_THREAD(thread_checker_); // Populates response_.auth_challenge with the authentication challenge info. // This info is consumed by URLRequestHttpJob::GetAuthChallengeInfo(). auth_info_ = AuthChallengeInfo(); auth_info_->is_proxy = (target_ == HttpAuth::AUTH_PROXY); auth_info_->challenger = auth_scheme_host_port_; auth_info_->scheme = HttpAuth::SchemeToString(handler_->auth_scheme()); auth_info_->realm = handler_->realm(); auth_info_->path = auth_path_; auth_info_->challenge = handler_->challenge(); } int HttpAuthController::HandleGenerateTokenResult(int result) { DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); net_log_.EndEventWithNetErrorCode(NetLogEventType::AUTH_GENERATE_TOKEN, result); switch (result) { // Occurs if the credential handle is found to be invalid at the point it is // exercised (i.e. GenerateAuthToken stage). We are going to consider this // to be an error that invalidates the identity but not necessarily the // scheme. Doing so allows a different identity to be used with the same // scheme. See https://crbug.com/648366. case ERR_INVALID_HANDLE: // If the GenerateAuthToken call fails with this error, this means that the // handler can no longer be used. However, the authentication scheme is // considered still usable. This allows a scheme that attempted and failed // to use default credentials to recover and use explicit credentials. // // The current handler may be tied to external state that is no longer // valid, hence should be discarded. Since the scheme is still valid, a new // handler can be created for the current scheme. case ERR_INVALID_AUTH_CREDENTIALS: InvalidateCurrentHandler(INVALIDATE_HANDLER_AND_CACHED_CREDENTIALS); auth_token_.clear(); return OK; // Occurs with GSSAPI, if the user has not already logged in. case ERR_MISSING_AUTH_CREDENTIALS: // Usually, GSSAPI doesn't allow explicit credentials and the scheme // cannot succeed anymore hence it gets disabled. However, on ChromeOS // it's not the case so we invalidate the current handler and can ask for // explicit credentials later. (See b/260522530). if (!handler_->AllowsExplicitCredentials()) { InvalidateCurrentHandler(INVALIDATE_HANDLER_AND_DISABLE_SCHEME); } else { InvalidateCurrentHandler(INVALIDATE_HANDLER_AND_CACHED_CREDENTIALS); } auth_token_.clear(); return OK; // 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. InvalidateCurrentHandler(INVALIDATE_HANDLER_AND_DISABLE_SCHEME); auth_token_.clear(); return OK; default: return result; } } void HttpAuthController::OnGenerateAuthTokenDone(int result) { DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); result = HandleGenerateTokenResult(result); if (!callback_.is_null()) { std::move(callback_).Run(result); } } void HttpAuthController::TakeAuthInfo(std::optional* other) { DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); auth_info_.swap(*other); } bool HttpAuthController::IsAuthSchemeDisabled(HttpAuth::Scheme scheme) const { DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); return disabled_schemes_.find(scheme) != disabled_schemes_.end(); } void HttpAuthController::DisableAuthScheme(HttpAuth::Scheme scheme) { DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); disabled_schemes_.insert(scheme); } void HttpAuthController::DisableEmbeddedIdentity() { DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); embedded_identity_used_ = true; } void HttpAuthController::OnConnectionClosed() { DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); InvalidateCurrentHandler(INVALIDATE_HANDLER); } } // namespace net