// 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 #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 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(prefs); #elif BUILDFLAG(IS_WIN) return std::make_unique(auth_library, HttpAuth::AUTH_SCHEME_NEGOTIATE); #elif BUILDFLAG(IS_POSIX) return std::make_unique(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* 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 tmp_handler( std::make_unique( 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 tmp_handler( std::make_unique( 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) || BUILDFLAG(IS_LINUX) // 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) || BUILDFLAG(IS_LINUX) 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 tmp_handler( std::make_unique( 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 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/: through SSPI, // and in the form HTTP@: 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 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 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