| // 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_handler_negotiate.h" |
| |
| #include <utility> |
| |
| #include "base/check_op.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/logging.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/values.h" |
| #include "build/build_config.h" |
| #include "build/chromeos_buildflags.h" |
| #include "net/base/address_family.h" |
| #include "net/base/address_list.h" |
| #include "net/base/host_port_pair.h" |
| #include "net/base/net_errors.h" |
| #include "net/cert/x509_util.h" |
| #include "net/dns/host_resolver.h" |
| #include "net/http/http_auth.h" |
| #include "net/http/http_auth_filter.h" |
| #include "net/http/http_auth_preferences.h" |
| #include "net/log/net_log_capture_mode.h" |
| #include "net/log/net_log_event_type.h" |
| #include "net/log/net_log_with_source.h" |
| #include "net/ssl/ssl_info.h" |
| #include "url/scheme_host_port.h" |
| |
| namespace net { |
| |
| using DelegationType = HttpAuth::DelegationType; |
| |
| namespace { |
| |
| base::Value::Dict NetLogParameterChannelBindings( |
| const std::string& channel_binding_token, |
| NetLogCaptureMode capture_mode) { |
| base::Value::Dict dict; |
| if (!NetLogCaptureIncludesSocketBytes(capture_mode)) |
| return dict; |
| |
| dict.Set("token", base::HexEncode(channel_binding_token.data(), |
| channel_binding_token.size())); |
| return dict; |
| } |
| |
| // Uses |negotiate_auth_system_factory| to create the auth system, otherwise |
| // creates the default auth system for each platform. |
| std::unique_ptr<HttpAuthMechanism> CreateAuthSystem( |
| #if !BUILDFLAG(IS_ANDROID) |
| HttpAuthHandlerNegotiate::AuthLibrary* auth_library, |
| #endif |
| const HttpAuthPreferences* prefs, |
| HttpAuthMechanismFactory negotiate_auth_system_factory) { |
| if (negotiate_auth_system_factory) |
| return negotiate_auth_system_factory.Run(prefs); |
| #if BUILDFLAG(IS_ANDROID) |
| return std::make_unique<net::android::HttpAuthNegotiateAndroid>(prefs); |
| #elif BUILDFLAG(IS_WIN) |
| return std::make_unique<HttpAuthSSPI>(auth_library, |
| HttpAuth::AUTH_SCHEME_NEGOTIATE); |
| #elif BUILDFLAG(IS_POSIX) |
| return std::make_unique<HttpAuthGSSAPI>(auth_library, |
| CHROME_GSS_SPNEGO_MECH_OID_DESC); |
| #endif |
| } |
| |
| } // namespace |
| |
| HttpAuthHandlerNegotiate::Factory::Factory( |
| HttpAuthMechanismFactory negotiate_auth_system_factory) |
| : negotiate_auth_system_factory_(negotiate_auth_system_factory) {} |
| |
| HttpAuthHandlerNegotiate::Factory::~Factory() = default; |
| |
| #if !BUILDFLAG(IS_ANDROID) && BUILDFLAG(IS_POSIX) |
| const std::string& HttpAuthHandlerNegotiate::Factory::GetLibraryNameForTesting() |
| const { |
| return auth_library_->GetLibraryNameForTesting(); |
| } |
| #endif // !BUILDFLAG(IS_ANDROID) && BUILDFLAG(IS_POSIX) |
| |
| int HttpAuthHandlerNegotiate::Factory::CreateAuthHandler( |
| HttpAuthChallengeTokenizer* challenge, |
| HttpAuth::Target target, |
| const SSLInfo& ssl_info, |
| const NetworkAnonymizationKey& network_anonymization_key, |
| const url::SchemeHostPort& scheme_host_port, |
| CreateReason reason, |
| int digest_nonce_count, |
| const NetLogWithSource& net_log, |
| HostResolver* host_resolver, |
| std::unique_ptr<HttpAuthHandler>* handler) { |
| #if BUILDFLAG(IS_WIN) |
| if (is_unsupported_ || reason == CREATE_PREEMPTIVE) |
| return ERR_UNSUPPORTED_AUTH_SCHEME; |
| // TODO(cbentzel): Move towards model of parsing in the factory |
| // method and only constructing when valid. |
| std::unique_ptr<HttpAuthHandler> tmp_handler( |
| std::make_unique<HttpAuthHandlerNegotiate>( |
| CreateAuthSystem(auth_library_.get(), http_auth_preferences(), |
| negotiate_auth_system_factory_), |
| http_auth_preferences(), host_resolver)); |
| #elif BUILDFLAG(IS_ANDROID) |
| if (is_unsupported_ || !http_auth_preferences() || |
| http_auth_preferences()->AuthAndroidNegotiateAccountType().empty() || |
| reason == CREATE_PREEMPTIVE) |
| return ERR_UNSUPPORTED_AUTH_SCHEME; |
| // TODO(cbentzel): Move towards model of parsing in the factory |
| // method and only constructing when valid. |
| std::unique_ptr<HttpAuthHandler> tmp_handler( |
| std::make_unique<HttpAuthHandlerNegotiate>( |
| CreateAuthSystem(http_auth_preferences(), |
| negotiate_auth_system_factory_), |
| http_auth_preferences(), host_resolver)); |
| #elif BUILDFLAG(IS_POSIX) |
| if (is_unsupported_) |
| return ERR_UNSUPPORTED_AUTH_SCHEME; |
| #if BUILDFLAG(IS_CHROMEOS) |
| // Note: Don't set is_unsupported_ = true here. AllowGssapiLibraryLoad() |
| // might change to true during a session. |
| if (!http_auth_preferences() || |
| !http_auth_preferences()->AllowGssapiLibraryLoad()) { |
| return ERR_UNSUPPORTED_AUTH_SCHEME; |
| } |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| if (!auth_library_->Init(net_log)) { |
| is_unsupported_ = true; |
| return ERR_UNSUPPORTED_AUTH_SCHEME; |
| } |
| // TODO(ahendrickson): Move towards model of parsing in the factory |
| // method and only constructing when valid. |
| std::unique_ptr<HttpAuthHandler> tmp_handler( |
| std::make_unique<HttpAuthHandlerNegotiate>( |
| CreateAuthSystem(auth_library_.get(), http_auth_preferences(), |
| negotiate_auth_system_factory_), |
| http_auth_preferences(), host_resolver)); |
| #endif |
| if (!tmp_handler->InitFromChallenge(challenge, target, ssl_info, |
| network_anonymization_key, |
| scheme_host_port, net_log)) { |
| return ERR_INVALID_RESPONSE; |
| } |
| handler->swap(tmp_handler); |
| return OK; |
| } |
| |
| HttpAuthHandlerNegotiate::HttpAuthHandlerNegotiate( |
| std::unique_ptr<HttpAuthMechanism> auth_system, |
| const HttpAuthPreferences* prefs, |
| HostResolver* resolver) |
| : auth_system_(std::move(auth_system)), |
| resolver_(resolver), |
| http_auth_preferences_(prefs) {} |
| |
| HttpAuthHandlerNegotiate::~HttpAuthHandlerNegotiate() = default; |
| |
| // Require identity on first pass instead of second. |
| bool HttpAuthHandlerNegotiate::NeedsIdentity() { |
| return auth_system_->NeedsIdentity(); |
| } |
| |
| bool HttpAuthHandlerNegotiate::AllowsDefaultCredentials() { |
| if (target_ == HttpAuth::AUTH_PROXY) |
| return true; |
| if (!http_auth_preferences_) |
| return false; |
| return http_auth_preferences_->CanUseDefaultCredentials(scheme_host_port_); |
| } |
| |
| bool HttpAuthHandlerNegotiate::AllowsExplicitCredentials() { |
| return auth_system_->AllowsExplicitCredentials(); |
| } |
| |
| // The Negotiate challenge header looks like: |
| // WWW-Authenticate: NEGOTIATE auth-data |
| bool HttpAuthHandlerNegotiate::Init( |
| HttpAuthChallengeTokenizer* challenge, |
| const SSLInfo& ssl_info, |
| const NetworkAnonymizationKey& network_anonymization_key) { |
| network_anonymization_key_ = network_anonymization_key; |
| #if BUILDFLAG(IS_POSIX) |
| if (!auth_system_->Init(net_log())) { |
| VLOG(1) << "can't initialize GSSAPI library"; |
| return false; |
| } |
| // GSSAPI does not provide a way to enter username/password to obtain a TGT, |
| // however ChromesOS provides the user an opportunity to enter their |
| // credentials and generate a new TGT on OS level (see b/260522530). If the |
| // default credentials are not allowed for a particular site |
| // (based on allowlist), fall back to a different scheme. |
| if (!AllowsDefaultCredentials()) { |
| return false; |
| } |
| #endif |
| auth_system_->SetDelegation(GetDelegationType()); |
| auth_scheme_ = HttpAuth::AUTH_SCHEME_NEGOTIATE; |
| score_ = 4; |
| properties_ = ENCRYPTS_IDENTITY | IS_CONNECTION_BASED; |
| |
| HttpAuth::AuthorizationResult auth_result = |
| auth_system_->ParseChallenge(challenge); |
| if (auth_result != HttpAuth::AUTHORIZATION_RESULT_ACCEPT) |
| return false; |
| |
| // Try to extract channel bindings. |
| if (ssl_info.is_valid()) |
| x509_util::GetTLSServerEndPointChannelBinding(*ssl_info.cert, |
| &channel_bindings_); |
| if (!channel_bindings_.empty()) |
| net_log().AddEvent(NetLogEventType::AUTH_CHANNEL_BINDINGS, |
| [&](NetLogCaptureMode capture_mode) { |
| return NetLogParameterChannelBindings( |
| channel_bindings_, capture_mode); |
| }); |
| return true; |
| } |
| |
| int HttpAuthHandlerNegotiate::GenerateAuthTokenImpl( |
| const AuthCredentials* credentials, |
| const HttpRequestInfo* request, |
| CompletionOnceCallback callback, |
| std::string* auth_token) { |
| DCHECK(callback_.is_null()); |
| DCHECK(auth_token_ == nullptr); |
| auth_token_ = auth_token; |
| if (already_called_) { |
| DCHECK((!has_credentials_ && credentials == nullptr) || |
| (has_credentials_ && credentials->Equals(credentials_))); |
| next_state_ = STATE_GENERATE_AUTH_TOKEN; |
| } else { |
| already_called_ = true; |
| if (credentials) { |
| has_credentials_ = true; |
| credentials_ = *credentials; |
| } |
| next_state_ = STATE_RESOLVE_CANONICAL_NAME; |
| } |
| int rv = DoLoop(OK); |
| if (rv == ERR_IO_PENDING) |
| callback_ = std::move(callback); |
| return rv; |
| } |
| |
| HttpAuth::AuthorizationResult |
| HttpAuthHandlerNegotiate::HandleAnotherChallengeImpl( |
| HttpAuthChallengeTokenizer* challenge) { |
| return auth_system_->ParseChallenge(challenge); |
| } |
| |
| std::string HttpAuthHandlerNegotiate::CreateSPN( |
| const std::string& server, |
| const url::SchemeHostPort& scheme_host_port) { |
| // Kerberos Web Server SPNs are in the form HTTP/<host>:<port> through SSPI, |
| // and in the form HTTP@<host>:<port> through GSSAPI |
| // http://msdn.microsoft.com/en-us/library/ms677601%28VS.85%29.aspx |
| // |
| // However, reality differs from the specification. A good description of |
| // the problems can be found here: |
| // http://blog.michelbarneveld.nl/michel/archive/2009/11/14/the-reason-why-kb911149-and-kb908209-are-not-the-soluton.aspx |
| // |
| // Typically the <host> portion should be the canonical FQDN for the service. |
| // If this could not be resolved, the original hostname in the URL will be |
| // attempted instead. However, some intranets register SPNs using aliases |
| // for the same canonical DNS name to allow multiple web services to reside |
| // on the same host machine without requiring different ports. IE6 and IE7 |
| // have hotpatches that allow the default behavior to be overridden. |
| // http://support.microsoft.com/kb/911149 |
| // http://support.microsoft.com/kb/938305 |
| // |
| // According to the spec, the <port> option should be included if it is a |
| // non-standard port (i.e. not 80 or 443 in the HTTP case). However, |
| // historically browsers have not included the port, even on non-standard |
| // ports. IE6 required a hotpatch and a registry setting to enable |
| // including non-standard ports, and IE7 and IE8 also require the same |
| // registry setting, but no hotpatch. Firefox does not appear to have an |
| // option to include non-standard ports as of 3.6. |
| // http://support.microsoft.com/kb/908209 |
| // |
| // Without any command-line flags, Chrome matches the behavior of Firefox |
| // and IE. Users can override the behavior so aliases are allowed and |
| // non-standard ports are included. |
| int port = scheme_host_port.port(); |
| #if BUILDFLAG(IS_WIN) |
| static const char kSpnSeparator = '/'; |
| #elif BUILDFLAG(IS_POSIX) |
| static const char kSpnSeparator = '@'; |
| #endif |
| if (port != 80 && port != 443 && |
| (http_auth_preferences_ && |
| http_auth_preferences_->NegotiateEnablePort())) { |
| return base::StringPrintf("HTTP%c%s:%d", kSpnSeparator, server.c_str(), |
| port); |
| } else { |
| return base::StringPrintf("HTTP%c%s", kSpnSeparator, server.c_str()); |
| } |
| } |
| |
| void HttpAuthHandlerNegotiate::OnIOComplete(int result) { |
| int rv = DoLoop(result); |
| if (rv != ERR_IO_PENDING) |
| DoCallback(rv); |
| } |
| |
| void HttpAuthHandlerNegotiate::DoCallback(int rv) { |
| DCHECK(rv != ERR_IO_PENDING); |
| DCHECK(!callback_.is_null()); |
| std::move(callback_).Run(rv); |
| } |
| |
| int HttpAuthHandlerNegotiate::DoLoop(int result) { |
| DCHECK(next_state_ != STATE_NONE); |
| |
| int rv = result; |
| do { |
| State state = next_state_; |
| next_state_ = STATE_NONE; |
| switch (state) { |
| case STATE_RESOLVE_CANONICAL_NAME: |
| DCHECK_EQ(OK, rv); |
| rv = DoResolveCanonicalName(); |
| break; |
| case STATE_RESOLVE_CANONICAL_NAME_COMPLETE: |
| rv = DoResolveCanonicalNameComplete(rv); |
| break; |
| case STATE_GENERATE_AUTH_TOKEN: |
| DCHECK_EQ(OK, rv); |
| rv = DoGenerateAuthToken(); |
| break; |
| case STATE_GENERATE_AUTH_TOKEN_COMPLETE: |
| rv = DoGenerateAuthTokenComplete(rv); |
| break; |
| default: |
| NOTREACHED() << "bad state"; |
| rv = ERR_FAILED; |
| break; |
| } |
| } while (rv != ERR_IO_PENDING && next_state_ != STATE_NONE); |
| |
| return rv; |
| } |
| |
| int HttpAuthHandlerNegotiate::DoResolveCanonicalName() { |
| next_state_ = STATE_RESOLVE_CANONICAL_NAME_COMPLETE; |
| if ((http_auth_preferences_ && |
| http_auth_preferences_->NegotiateDisableCnameLookup()) || |
| !resolver_) |
| return OK; |
| |
| // TODO(cbentzel): Add reverse DNS lookup for numeric addresses. |
| HostResolver::ResolveHostParameters parameters; |
| parameters.include_canonical_name = true; |
| resolve_host_request_ = resolver_->CreateRequest( |
| scheme_host_port_, network_anonymization_key_, net_log(), parameters); |
| return resolve_host_request_->Start(base::BindOnce( |
| &HttpAuthHandlerNegotiate::OnIOComplete, base::Unretained(this))); |
| } |
| |
| int HttpAuthHandlerNegotiate::DoResolveCanonicalNameComplete(int rv) { |
| DCHECK_NE(ERR_IO_PENDING, rv); |
| std::string server = scheme_host_port_.host(); |
| if (resolve_host_request_) { |
| if (rv == OK) { |
| // Expect at most a single DNS alias representing the canonical name |
| // because the `HostResolver` request was made with |
| // `include_canonical_name`. |
| DCHECK(resolve_host_request_->GetDnsAliasResults()); |
| DCHECK_LE(resolve_host_request_->GetDnsAliasResults()->size(), 1u); |
| if (!resolve_host_request_->GetDnsAliasResults()->empty()) { |
| server = *resolve_host_request_->GetDnsAliasResults()->begin(); |
| DCHECK(!server.empty()); |
| } |
| } else { |
| // Even in the error case, try to use origin_.host instead of |
| // passing the failure on to the caller. |
| VLOG(1) << "Problem finding canonical name for SPN for host " |
| << scheme_host_port_.host() << ": " << ErrorToString(rv); |
| rv = OK; |
| } |
| } |
| |
| next_state_ = STATE_GENERATE_AUTH_TOKEN; |
| spn_ = CreateSPN(server, scheme_host_port_); |
| resolve_host_request_ = nullptr; |
| return rv; |
| } |
| |
| int HttpAuthHandlerNegotiate::DoGenerateAuthToken() { |
| next_state_ = STATE_GENERATE_AUTH_TOKEN_COMPLETE; |
| AuthCredentials* credentials = has_credentials_ ? &credentials_ : nullptr; |
| return auth_system_->GenerateAuthToken( |
| credentials, spn_, channel_bindings_, auth_token_, net_log(), |
| base::BindOnce(&HttpAuthHandlerNegotiate::OnIOComplete, |
| base::Unretained(this))); |
| } |
| |
| int HttpAuthHandlerNegotiate::DoGenerateAuthTokenComplete(int rv) { |
| DCHECK_NE(ERR_IO_PENDING, rv); |
| auth_token_ = nullptr; |
| return rv; |
| } |
| |
| DelegationType HttpAuthHandlerNegotiate::GetDelegationType() const { |
| if (!http_auth_preferences_) |
| return DelegationType::kNone; |
| |
| // TODO(cbentzel): Should delegation be allowed on proxies? |
| if (target_ == HttpAuth::AUTH_PROXY) |
| return DelegationType::kNone; |
| |
| return http_auth_preferences_->GetDelegationType(scheme_host_port_); |
| } |
| |
| } // namespace net |