| // 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 <utility> |
| |
| #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/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 { |
| |
| 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<HttpAuthHandler> 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<HttpResponseHeaders> 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: |
| 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()) { |
| 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. |
| 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 |
| // <handler,identity> 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_ = absl::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? |
| UMA_HISTOGRAM_BOOLEAN("net.HttpIdentSrcURL", true); |
| 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( |
| absl::optional<AuthChallengeInfo>* 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 |