diff options
author | cbentzel@chromium.org <cbentzel@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-09-11 14:03:30 +0000 |
---|---|---|
committer | cbentzel@chromium.org <cbentzel@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-09-11 14:03:30 +0000 |
commit | eca50e128ff1bc41bc0cc1d3fdf2e015ba459d4c (patch) | |
tree | ae0368388f38766781c5ddff86c9e0e2c0c9c362 | |
parent | 4630db4630bc415cc3b7be70bce87160559810df (diff) | |
download | chromium_src-eca50e128ff1bc41bc0cc1d3fdf2e015ba459d4c.zip chromium_src-eca50e128ff1bc41bc0cc1d3fdf2e015ba459d4c.tar.gz chromium_src-eca50e128ff1bc41bc0cc1d3fdf2e015ba459d4c.tar.bz2 |
Fix multi-round authentication.
In the case of Negotiate, authentication can look like
C: GET
S: 401, WWW-Authenticate: Negotiate
C: GET, WWW-Authorization: Negotiate <client_token_1>
S: 401, WWW-Authenticate: Negotiate <server_token_1>
C: GET, WWW-Authorization: Negotiate <client_token_2>
S: 401, WWW-Authenticate: Negotiate <server_token_2>
on that third challenge, the handler was reported as being in "the final round" and this was treated as a rejection of the authentication attempt. After that, the new challenge token was used by a new auth handler that hadn't established a security context, and an ERR_INVALID_HANDLE would be returned.
This CL also does some prep work to correctly handle the "stale=true" value for Digest authentication, but I decided to defer the HttpAuthCache changes needed for that to a separate CL since this was large enough.
BUG=53282
TEST=net_unittests. Unfortunately, I haven't been able to set up a proxy/server to do more than two auth challenges, but this does happen in the wild.
Review URL: http://codereview.chromium.org/3360017
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@59188 0039d316-1c4b-4281-b951-d872f2087c98
30 files changed, 692 insertions, 284 deletions
diff --git a/net/http/http_auth.cc b/net/http/http_auth.cc index bb8c805..aaa4de0 100644 --- a/net/http/http_auth.cc +++ b/net/http/http_auth.cc @@ -28,21 +28,7 @@ void HttpAuth::ChooseBestChallenge( const BoundNetLog& net_log, scoped_ptr<HttpAuthHandler>* handler) { DCHECK(http_auth_handler_factory); - - // A connection-based authentication scheme must continue to use the - // existing handler object in |*handler|. - if (handler->get() && (*handler)->is_connection_based() && - (disabled_schemes.find((*handler)->scheme()) == disabled_schemes.end())) { - const std::string header_name = GetChallengeHeaderName(target); - std::string challenge; - void* iter = NULL; - while (headers->EnumerateHeader(&iter, header_name, &challenge)) { - ChallengeTokenizer props(challenge.begin(), challenge.end()); - if (LowerCaseEqualsASCII(props.scheme(), (*handler)->scheme().c_str()) && - (*handler)->InitFromChallenge(&props, target, origin, net_log)) - return; - } - } + DCHECK(handler->get() == NULL); // Choose the challenge whose authentication handler gives the maximum score. scoped_ptr<HttpAuthHandler> best; @@ -65,6 +51,32 @@ void HttpAuth::ChooseBestChallenge( handler->swap(best); } +// static +HttpAuth::AuthorizationResult HttpAuth::HandleChallengeResponse( + HttpAuthHandler* handler, + const HttpResponseHeaders* headers, + Target target, + const std::set<std::string>& disabled_schemes) { + const std::string& current_scheme = handler->scheme(); + if (disabled_schemes.find(current_scheme) != disabled_schemes.end()) + return HttpAuth::AUTHORIZATION_RESULT_REJECT; + const std::string header_name = GetChallengeHeaderName(target); + void* iter = NULL; + std::string challenge; + HttpAuth::AuthorizationResult authorization_result = + HttpAuth::AUTHORIZATION_RESULT_INVALID; + while (headers->EnumerateHeader(&iter, header_name, &challenge)) { + HttpAuth::ChallengeTokenizer props(challenge.begin(), challenge.end()); + if (!LowerCaseEqualsASCII(props.scheme(), current_scheme.c_str())) + continue; + authorization_result = handler->HandleAnotherChallenge(&props); + if (authorization_result != HttpAuth::AUTHORIZATION_RESULT_INVALID) + return authorization_result; + } + // Finding no matches is equivalent to rejection + return HttpAuth::AUTHORIZATION_RESULT_REJECT; +} + void HttpAuth::ChallengeTokenizer::Init(std::string::const_iterator begin, std::string::const_iterator end) { // The first space-separated token is the auth-scheme. diff --git a/net/http/http_auth.h b/net/http/http_auth.h index 347bdec..d7d0347 100644 --- a/net/http/http_auth.h +++ b/net/http/http_auth.h @@ -25,7 +25,6 @@ class HttpResponseHeaders; // Utility class for http authentication. class HttpAuth { public: - // Http authentication can be done the the proxy server, origin server, // or both. This enum tracks who the target is. enum Target { @@ -37,6 +36,24 @@ class HttpAuth { AUTH_NUM_TARGETS = 2, }; + // What the HTTP WWW-Authenticate/Proxy-Authenticate headers indicate about + // the previous authorization attempt. + enum AuthorizationResult { + AUTHORIZATION_RESULT_ACCEPT, // The authorization attempt was accepted, + // although there still may be additional + // rounds of challenges. + + AUTHORIZATION_RESULT_REJECT, // The authorization attempt was rejected. + + AUTHORIZATION_RESULT_STALE, // (Digest) The nonce used in the + // authorization attempt is stale, but + // otherwise the attempt was valid. + + AUTHORIZATION_RESULT_INVALID, // The authentication challenge headers are + // poorly formed (the authorization attempt + // itself may have been fine). + }; + // Describes where the identity used for authentication came from. enum IdentitySource { // Came from nowhere -- the identity is not initialized. @@ -88,19 +105,13 @@ class HttpAuth { // Iterate through the challenge headers, and pick the best one that // we support. Obtains the implementation class for handling the challenge, - // and passes it back in |*handler|. If the existing handler in |*handler| - // should continue to be used (such as for the NTLM authentication scheme), - // |*handler| is unchanged. If no supported challenge was found, |*handler| - // is set to NULL. + // and passes it back in |*handler|. If no supported challenge was found, + // |*handler| is set to NULL. // // |disabled_schemes| is the set of schemes that we should not use. // - // |origin| is used by the NTLM authentication scheme to construct the - // service principal name. It is ignored by other schemes. - // - // TODO(wtc): Continuing to use the existing handler in |*handler| (for - // NTLM) is new behavior. Rename ChooseBestChallenge to fully encompass - // what it does now. + // |origin| is used by the NTLM and Negotiation authentication scheme to + // construct the service principal name. It is ignored by other schemes. static void ChooseBestChallenge( HttpAuthHandlerFactory* http_auth_handler_factory, const HttpResponseHeaders* headers, @@ -110,6 +121,13 @@ class HttpAuth { const BoundNetLog& net_log, scoped_ptr<HttpAuthHandler>* handler); + // Handle a response to a previous authentication attempt. + static AuthorizationResult HandleChallengeResponse( + HttpAuthHandler* handler, + const HttpResponseHeaders* headers, + Target target, + const std::set<std::string>& disabled_schemes); + // ChallengeTokenizer breaks up a challenge string into the the auth scheme // and parameter list, according to RFC 2617 Sec 1.2: // challenge = auth-scheme 1*SP 1#auth-param diff --git a/net/http/http_auth_cache_unittest.cc b/net/http/http_auth_cache_unittest.cc index 5db49ee..7b68ae3 100644 --- a/net/http/http_auth_cache_unittest.cc +++ b/net/http/http_auth_cache_unittest.cc @@ -10,7 +10,6 @@ #include "net/base/net_errors.h" #include "net/http/http_auth_cache.h" #include "net/http/http_auth_handler.h" - #include "testing/gtest/include/gtest/gtest.h" namespace net { @@ -29,6 +28,11 @@ class MockAuthHandler : public HttpAuthHandler { properties_ = 0; } + HttpAuth::AuthorizationResult HandleAnotherChallenge( + HttpAuth::ChallengeTokenizer* challenge) { + return HttpAuth::AUTHORIZATION_RESULT_REJECT; + } + protected: virtual bool Init(HttpAuth::ChallengeTokenizer* challenge) { return false; // Unused. diff --git a/net/http/http_auth_controller.cc b/net/http/http_auth_controller.cc index e753b59..5cdb1c6 100644 --- a/net/http/http_auth_controller.cc +++ b/net/http/http_auth_controller.cc @@ -4,6 +4,7 @@ #include "net/http/http_auth_controller.h" +#include "base/string_util.h" #include "base/utf_string_conversions.h" #include "net/base/auth.h" #include "net/base/host_resolver.h" @@ -151,27 +152,40 @@ int HttpAuthController::HandleAuthChallenge( const BoundNetLog& net_log) { DCHECK(headers); DCHECK(auth_origin_.is_valid()); - LOG(INFO) << "The " << HttpAuth::GetAuthTargetString(target_) << " " << auth_origin_ << " requested auth" << AuthChallengeLogMessage(headers.get()); - // The auth we tried just failed, hence it can't be valid. Remove it from - // the cache so it won't be used again. - // TODO(wtc): IsFinalRound is not the right condition. In a multi-round - // auth sequence, the server may fail the auth in round 1 if our first - // authorization header is broken. We should inspect response_.headers to - // determine if the server already failed the auth or wants us to continue. - // See http://crbug.com/21015. - if (HaveAuth() && handler_->IsFinalRound()) { - InvalidateRejectedAuthFromCache(); - handler_.reset(); - identity_ = HttpAuth::Identity(); + // 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()) { + HttpAuth::AuthorizationResult result = HttpAuth::HandleChallengeResponse( + handler_.get(), headers, target_, disabled_schemes_); + switch (result) { + case HttpAuth::AUTHORIZATION_RESULT_ACCEPT: + break; + case HttpAuth::AUTHORIZATION_RESULT_INVALID: + case HttpAuth::AUTHORIZATION_RESULT_REJECT: + InvalidateCurrentHandler(); + break; + case HttpAuth::AUTHORIZATION_RESULT_STALE: + // TODO(cbentzel): Support "stale" invalidation in HttpAuthCache. + // Right now this does the same invalidation as before. + InvalidateCurrentHandler(); + break; + default: + NOTREACHED(); + break; + } } identity_.invalid = true; - if (target_ != HttpAuth::AUTH_SERVER || !do_not_send_server_auth) { + bool can_send_auth = (target_ != HttpAuth::AUTH_SERVER || + !do_not_send_server_auth); + if (!handler_.get() && can_send_auth) { // Find the best authentication challenge that we support. HttpAuth::ChooseBestChallenge(session_->http_auth_handler_factory(), headers, target_, auth_origin_, @@ -203,9 +217,6 @@ int HttpAuthController::HandleAuthChallenge( SelectNextAuthIdentityToTry(); } else { // Proceed with the existing identity or a null identity. - // - // TODO(wtc): Add a safeguard against infinite transaction restarts, if - // the server keeps returning "NTLM". identity_.invalid = false; } @@ -263,6 +274,12 @@ void HttpAuthController::ResetAuth(const string16& username, } } +void HttpAuthController::InvalidateCurrentHandler() { + InvalidateRejectedAuthFromCache(); + handler_.reset(); + identity_ = HttpAuth::Identity(); +} + void HttpAuthController::InvalidateRejectedAuthFromCache() { DCHECK(HaveAuth()); @@ -302,8 +319,8 @@ bool HttpAuthController::SelectNextAuthIdentityToTry() { // Check the auth cache for a realm entry. HttpAuthCache::Entry* entry = - session_->auth_cache()->Lookup(auth_origin_, handler_->realm(), - handler_->scheme()); + session_->auth_cache()->Lookup(auth_origin_, handler_->realm(), + handler_->scheme()); if (entry) { identity_.source = HttpAuth::IDENT_SRC_REALM_LOOKUP; diff --git a/net/http/http_auth_controller.h b/net/http/http_auth_controller.h index 721bce9..5ee156c 100644 --- a/net/http/http_auth_controller.h +++ b/net/http/http_auth_controller.h @@ -81,6 +81,9 @@ class HttpAuthController : public base::RefCounted<HttpAuthController> { // cache entry's data and returns true. bool SelectPreemptiveAuth(const BoundNetLog& net_log); + // Invalidates the current handler, including cache. + void InvalidateCurrentHandler(); + // Invalidates any auth cache entries after authentication has failed. // The identity that was rejected is |identity_|. void InvalidateRejectedAuthFromCache(); diff --git a/net/http/http_auth_gssapi_posix.cc b/net/http/http_auth_gssapi_posix.cc index 2c605a0..db8f712 100644 --- a/net/http/http_auth_gssapi_posix.cc +++ b/net/http/http_auth_gssapi_posix.cc @@ -114,7 +114,7 @@ std::string DisplayCode(GSSAPILibrary* gssapi_lib, // individual message may exceed |kMaxMsgLength|, and the final result // will not exceed |kMaxMsgLength|*2-1. for (int i = 0; i < kMaxDisplayIterations && rv.size() < kMaxMsgLength; - ++i) { + ++i) { OM_uint32 min_stat; gss_buffer_desc_struct msg = GSS_C_EMPTY_BUFFER; OM_uint32 maj_stat = @@ -482,26 +482,26 @@ base::NativeLibrary GSSAPISharedLibrary::LoadSharedLibrary() { return NULL; } -#define BIND(lib, x) \ - gss_##x##_type x = reinterpret_cast<gss_##x##_type>( \ - base::GetFunctionPointerFromNativeLibrary(lib, "gss_" #x)); \ - if (x == NULL) { \ - LOG(WARNING) << "Unable to bind function \"" << "gss_" #x << "\""; \ - return false; \ +#define BIND(lib, x) \ + gss_##x##_type x = reinterpret_cast<gss_##x##_type>( \ + base::GetFunctionPointerFromNativeLibrary(lib, "gss_" #x)); \ + if (x == NULL) { \ + LOG(WARNING) << "Unable to bind function \"" << "gss_" #x << "\""; \ + return false; \ } bool GSSAPISharedLibrary::BindMethods(base::NativeLibrary lib) { DCHECK(lib != NULL); - BIND(lib, import_name) - BIND(lib, release_name) - BIND(lib, release_buffer) - BIND(lib, display_name) - BIND(lib, display_status) - BIND(lib, init_sec_context) - BIND(lib, wrap_size_limit) - BIND(lib, delete_sec_context) - BIND(lib, inquire_context) + BIND(lib, import_name); + BIND(lib, release_name); + BIND(lib, release_buffer); + BIND(lib, display_name); + BIND(lib, display_status); + BIND(lib, init_sec_context); + BIND(lib, wrap_size_limit); + BIND(lib, delete_sec_context); + BIND(lib, inquire_context); import_name_ = import_name; release_name_ = release_name; @@ -638,14 +638,14 @@ OM_uint32 GSSAPISharedLibrary::inquire_context( int* open) { DCHECK(initialized_); return inquire_context_(minor_status, - context_handle, - src_name, - targ_name, - lifetime_rec, - mech_type, - ctx_flags, - locally_initiated, - open); + context_handle, + src_name, + targ_name, + lifetime_rec, + mech_type, + ctx_flags, + locally_initiated, + open); } GSSAPILibrary* GSSAPILibrary::GetDefault() { return Singleton<GSSAPISharedLibrary>::get(); @@ -695,35 +695,40 @@ bool HttpAuthGSSAPI::NeedsIdentity() const { return decoded_server_auth_token_.empty(); } -bool HttpAuthGSSAPI::IsFinalRound() const { - return !NeedsIdentity(); -} - void HttpAuthGSSAPI::Delegate() { can_delegate_ = true; } -bool HttpAuthGSSAPI::ParseChallenge(HttpAuth::ChallengeTokenizer* tok) { +HttpAuth::AuthorizationResult HttpAuthGSSAPI::ParseChallenge( + HttpAuth::ChallengeTokenizer* tok) { // Verify the challenge's auth-scheme. if (!tok->valid() || !LowerCaseEqualsASCII(tok->scheme(), StringToLowerASCII(scheme_).c_str())) - return false; + return HttpAuth::AUTHORIZATION_RESULT_INVALID; tok->set_expect_base64_token(true); if (!tok->GetNext()) { - decoded_server_auth_token_.clear(); - return true; + // If a context has already been established, an empty Negotiate challenge + // should be treated as a rejection of the current attempt. + if (scoped_sec_context_.get() != GSS_C_NO_CONTEXT) + return HttpAuth::AUTHORIZATION_RESULT_REJECT; + DCHECK(decoded_server_auth_token_.empty()); + return HttpAuth::AUTHORIZATION_RESULT_ACCEPT; + } else { + // If a context has not already been established, additional tokens should + // not be present in the auth challenge. + if (scoped_sec_context_.get() == GSS_C_NO_CONTEXT) + return HttpAuth::AUTHORIZATION_RESULT_INVALID; } + // Make sure the additional token is base64 encoded. std::string encoded_auth_token = tok->value(); std::string decoded_auth_token; bool base64_rv = base::Base64Decode(encoded_auth_token, &decoded_auth_token); - if (!base64_rv) { - LOG(ERROR) << "Base64 decoding of auth token failed."; - return false; - } + if (!base64_rv) + return HttpAuth::AUTHORIZATION_RESULT_INVALID; decoded_server_auth_token_ = decoded_auth_token; - return true; + return HttpAuth::AUTHORIZATION_RESULT_ACCEPT; } int HttpAuthGSSAPI::GenerateAuthToken(const string16* username, @@ -735,10 +740,9 @@ int HttpAuthGSSAPI::GenerateAuthToken(const string16* username, gss_buffer_desc input_token = GSS_C_EMPTY_BUFFER; input_token.length = decoded_server_auth_token_.length(); - input_token.value = - (input_token.length > 0) ? - const_cast<char*>(decoded_server_auth_token_.data()) : - NULL; + input_token.value = (input_token.length > 0) ? + const_cast<char*>(decoded_server_auth_token_.data()) : + NULL; gss_buffer_desc output_token = GSS_C_EMPTY_BUFFER; ScopedBuffer scoped_output_token(&output_token, library_); int rv = GetNextSecurityToken(spn, &input_token, &output_token); diff --git a/net/http/http_auth_gssapi_posix.h b/net/http/http_auth_gssapi_posix.h index 7f6610e..3ea1131 100644 --- a/net/http/http_auth_gssapi_posix.h +++ b/net/http/http_auth_gssapi_posix.h @@ -223,9 +223,9 @@ class HttpAuthGSSAPI { bool Init(); bool NeedsIdentity() const; - bool IsFinalRound() const; - bool ParseChallenge(HttpAuth::ChallengeTokenizer* tok); + HttpAuth::AuthorizationResult ParseChallenge( + HttpAuth::ChallengeTokenizer* tok); // Generates an authentication token. // The return value is an error code. If it's not |OK|, the value of diff --git a/net/http/http_auth_gssapi_posix_unittest.cc b/net/http/http_auth_gssapi_posix_unittest.cc index e66bf85..62bae71 100644 --- a/net/http/http_auth_gssapi_posix_unittest.cc +++ b/net/http/http_auth_gssapi_posix_unittest.cc @@ -8,6 +8,7 @@ #include "base/logging.h" #include "base/native_library.h" #include "base/scoped_ptr.h" +#include "net/base/net_errors.h" #include "net/http/mock_gssapi_library_posix.h" #include "testing/gtest/include/gtest/gtest.h" @@ -46,6 +47,29 @@ void CopyBuffer(gss_buffer_t dest, const gss_buffer_t src) { SetBuffer(dest, src->value, src->length); } +const char kInitialAuthResponse[] = "Mary had a little lamb"; + +void EstablishInitialContext(test::MockGSSAPILibrary* library) { + test::GssContextMockImpl context_info( + "localhost", // Source name + "example.com", // Target name + 23, // Lifetime + *GSS_C_NT_HOSTBASED_SERVICE, // Mechanism + 0, // Context flags + 1, // Locally initiated + 0); // Open + gss_buffer_desc in_buffer = {0, NULL}; + gss_buffer_desc out_buffer = {arraysize(kInitialAuthResponse), + const_cast<char*>(kInitialAuthResponse)}; + library->ExpectSecurityContext( + "Negotiate", + GSS_S_CONTINUE_NEEDED, + 0, + context_info, + in_buffer, + out_buffer); +} + } // namespace TEST(HttpAuthGSSAPIPOSIXTest, GSSAPIStartup) { @@ -144,4 +168,103 @@ TEST(HttpAuthGSSAPIPOSIXTest, GSSAPICycle) { GSS_C_NO_BUFFER); } +TEST(HttpAuthGSSAPITest, ParseChallenge_FirstRound) { + // The first round should just consist of an unadorned "Negotiate" header. + test::MockGSSAPILibrary mock_library; + HttpAuthGSSAPI auth_gssapi(&mock_library, "Negotiate", + CHROME_GSS_KRB5_MECH_OID_DESC); + std::string challenge_text = "Negotiate"; + HttpAuth::ChallengeTokenizer challenge(challenge_text.begin(), + challenge_text.end()); + EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_ACCEPT, + auth_gssapi.ParseChallenge(&challenge)); +} + +TEST(HttpAuthGSSAPITest, ParseChallenge_TwoRounds) { + // The first round should just have "Negotiate", and the second round should + // have a valid base64 token associated with it. + test::MockGSSAPILibrary mock_library; + HttpAuthGSSAPI auth_gssapi(&mock_library, "Negotiate", + CHROME_GSS_KRB5_MECH_OID_DESC); + std::string first_challenge_text = "Negotiate"; + HttpAuth::ChallengeTokenizer first_challenge(first_challenge_text.begin(), + first_challenge_text.end()); + EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_ACCEPT, + auth_gssapi.ParseChallenge(&first_challenge)); + + // Generate an auth token and create another thing. + EstablishInitialContext(&mock_library); + std::string auth_token; + EXPECT_EQ(OK, auth_gssapi.GenerateAuthToken(NULL, NULL, + L"HTTP/intranet.google.com", + &auth_token)); + + std::string second_challenge_text = "Negotiate Zm9vYmFy"; + HttpAuth::ChallengeTokenizer second_challenge(second_challenge_text.begin(), + second_challenge_text.end()); + EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_ACCEPT, + auth_gssapi.ParseChallenge(&second_challenge)); +} + +TEST(HttpAuthGSSAPITest, ParseChallenge_UnexpectedTokenFirstRound) { + // If the first round challenge has an additional authentication token, it + // should be treated as an invalid challenge from the server. + test::MockGSSAPILibrary mock_library; + HttpAuthGSSAPI auth_gssapi(&mock_library, "Negotiate", + CHROME_GSS_KRB5_MECH_OID_DESC); + std::string challenge_text = "Negotiate Zm9vYmFy"; + HttpAuth::ChallengeTokenizer challenge(challenge_text.begin(), + challenge_text.end()); + EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_INVALID, + auth_gssapi.ParseChallenge(&challenge)); +} + +TEST(HttpAuthGSSAPITest, ParseChallenge_MissingTokenSecondRound) { + // If a later-round challenge is simply "Negotiate", it should be treated as + // an authentication challenge rejection from the server or proxy. + test::MockGSSAPILibrary mock_library; + HttpAuthGSSAPI auth_gssapi(&mock_library, "Negotiate", + CHROME_GSS_KRB5_MECH_OID_DESC); + std::string first_challenge_text = "Negotiate"; + HttpAuth::ChallengeTokenizer first_challenge(first_challenge_text.begin(), + first_challenge_text.end()); + EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_ACCEPT, + auth_gssapi.ParseChallenge(&first_challenge)); + + EstablishInitialContext(&mock_library); + std::string auth_token; + EXPECT_EQ(OK, auth_gssapi.GenerateAuthToken(NULL, NULL, + L"HTTP/intranet.google.com", + &auth_token)); + std::string second_challenge_text = "Negotiate"; + HttpAuth::ChallengeTokenizer second_challenge(second_challenge_text.begin(), + second_challenge_text.end()); + EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_REJECT, + auth_gssapi.ParseChallenge(&second_challenge)); +} + +TEST(HttpAuthGSSAPITest, ParseChallenge_NonBase64EncodedToken) { + // If a later-round challenge has an invalid base64 encoded token, it should + // be treated as an invalid challenge. + test::MockGSSAPILibrary mock_library; + HttpAuthGSSAPI auth_gssapi(&mock_library, "Negotiate", + CHROME_GSS_KRB5_MECH_OID_DESC); + std::string first_challenge_text = "Negotiate"; + HttpAuth::ChallengeTokenizer first_challenge(first_challenge_text.begin(), + first_challenge_text.end()); + EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_ACCEPT, + auth_gssapi.ParseChallenge(&first_challenge)); + + EstablishInitialContext(&mock_library); + std::string auth_token; + EXPECT_EQ(OK, auth_gssapi.GenerateAuthToken(NULL, NULL, + L"HTTP/intranet.google.com", + &auth_token)); + std::string second_challenge_text = "Negotiate =happyjoy="; + HttpAuth::ChallengeTokenizer second_challenge(second_challenge_text.begin(), + second_challenge_text.end()); + EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_INVALID, + auth_gssapi.ParseChallenge(&second_challenge)); +} + } // namespace net diff --git a/net/http/http_auth_handler.h b/net/http/http_auth_handler.h index 2895387..4c03a01 100644 --- a/net/http/http_auth_handler.h +++ b/net/http/http_auth_handler.h @@ -38,6 +38,22 @@ class HttpAuthHandler { const GURL& origin, const BoundNetLog& net_log); + // Determines how the previous authorization attempt was received. + // + // This is called when the server/proxy responds with a 401/407 after an + // earlier authorization attempt. Although this normally means that the + // previous attempt was rejected, in multi-round schemes such as + // NTLM+Negotiate it may indicate that another round of challenge+response + // is required. For Digest authentication it may also mean that the previous + // attempt used a stale nonce (and nonce-count) and that a new attempt should + // be made with a different nonce provided in the challenge. + // + // |challenge| must be non-NULL and have already tokenized the + // authentication scheme, but none of the tokens occuring after the + // authentication scheme. + virtual HttpAuth::AuthorizationResult HandleAnotherChallenge( + HttpAuth::ChallengeTokenizer* challenge) = 0; + // Generates an authentication token, potentially asynchronously. // // When |username| and |password| are NULL, the default credentials for @@ -107,11 +123,6 @@ class HttpAuthHandler { // sequence used by a connection-based authentication scheme. virtual bool NeedsIdentity() { return true; } - // Returns true if this is the final round of the authentication sequence. - // For Basic and Digest, the method always returns true because they are - // single-round schemes. - virtual bool IsFinalRound() { return true; } - // Returns whether the default credentials may be used for the |origin| passed // into |InitFromChallenge|. If true, the user does not need to be prompted // for username and password to establish credentials. diff --git a/net/http/http_auth_handler_basic.cc b/net/http/http_auth_handler_basic.cc index 31edf23..355ce8b 100644 --- a/net/http/http_auth_handler_basic.cc +++ b/net/http/http_auth_handler_basic.cc @@ -26,19 +26,35 @@ bool HttpAuthHandlerBasic::Init(HttpAuth::ChallengeTokenizer* challenge) { scheme_ = "basic"; score_ = 1; properties_ = 0; + return ParseChallenge(challenge); +} +bool HttpAuthHandlerBasic::ParseChallenge( + HttpAuth::ChallengeTokenizer* challenge) { // Verify the challenge's auth-scheme. if (!challenge->valid() || !LowerCaseEqualsASCII(challenge->scheme(), "basic")) return false; // Extract the realm (may be missing). + std::string realm; while (challenge->GetNext()) { if (LowerCaseEqualsASCII(challenge->name(), "realm")) - realm_ = challenge->unquoted_value(); + realm = challenge->unquoted_value(); } - return challenge->valid(); + if (!challenge->valid()) + return false; + + realm_ = realm; + return true; +} + +HttpAuth::AuthorizationResult HttpAuthHandlerBasic::HandleAnotherChallenge( + HttpAuth::ChallengeTokenizer* challenge) { + // Basic authentication is always a single round, so any responses should + // be treated as a rejection. + return HttpAuth::AUTHORIZATION_RESULT_REJECT; } int HttpAuthHandlerBasic::GenerateAuthTokenImpl( diff --git a/net/http/http_auth_handler_basic.h b/net/http/http_auth_handler_basic.h index 3204e94..a9031bb 100644 --- a/net/http/http_auth_handler_basic.h +++ b/net/http/http_auth_handler_basic.h @@ -31,6 +31,9 @@ class HttpAuthHandlerBasic : public HttpAuthHandler { scoped_ptr<HttpAuthHandler>* handler); }; + HttpAuth::AuthorizationResult HandleAnotherChallenge( + HttpAuth::ChallengeTokenizer* challenge); + protected: virtual bool Init(HttpAuth::ChallengeTokenizer* challenge); @@ -42,6 +45,8 @@ class HttpAuthHandlerBasic : public HttpAuthHandler { private: ~HttpAuthHandlerBasic() {} + + bool ParseChallenge(HttpAuth::ChallengeTokenizer* challenge); }; } // namespace net diff --git a/net/http/http_auth_handler_basic_unittest.cc b/net/http/http_auth_handler_basic_unittest.cc index 61ce864..f2ddbebc 100644 --- a/net/http/http_auth_handler_basic_unittest.cc +++ b/net/http/http_auth_handler_basic_unittest.cc @@ -66,6 +66,52 @@ TEST(HttpAuthHandlerBasicTest, InitFromChallenge) { OK, "", }, + + // Realm is valid. + { + "Basic realm=\"test_realm\"", + OK, + "test_realm", + }, + + // The parser ignores tokens which aren't known. + { + "Basic realm=\"test_realm\",unknown_token=foobar", + OK, + "test_realm", + }, + + // The parser skips over tokens which aren't known. + { + "Basic unknown_token=foobar,realm=\"test_realm\"", + OK, + "test_realm", + }, + +#if 0 + // TODO(cbentzel): It's unclear what the parser should do in these cases. + // It seems like this should either be treated as invalid, + // or the spaces should be used as a separator. + { + "Basic realm=\"test_realm\" unknown_token=foobar", + OK, + "test_realm", + }, + + // The parser skips over tokens which aren't known. + { + "Basic unknown_token=foobar realm=\"test_realm\"", + OK, + "test_realm", + }, +#endif + + // The parser fails when the first token is not "Basic". + { + "Negotiate", + ERR_INVALID_RESPONSE, + "" + }, }; HttpAuthHandlerBasic::Factory factory; GURL origin("http://www.example.com"); diff --git a/net/http/http_auth_handler_digest.cc b/net/http/http_auth_handler_digest.cc index ebb2494..289e0c7 100644 --- a/net/http/http_auth_handler_digest.cc +++ b/net/http/http_auth_handler_digest.cc @@ -173,8 +173,8 @@ std::string HttpAuthHandlerDigest::AssembleCredentials( std::string nc = StringPrintf("%08x", nonce_count); // TODO(eroman): is this the right encoding? - std::string authorization = std::string("Digest username=") + - HttpUtil::Quote(UTF16ToUTF8(username)); + std::string authorization = (std::string("Digest username=") + + HttpUtil::Quote(UTF16ToUTF8(username))); authorization += ", realm=" + HttpUtil::Quote(realm_); authorization += ", nonce=" + HttpUtil::Quote(nonce_); authorization += ", uri=" + HttpUtil::Quote(path); @@ -201,6 +201,31 @@ std::string HttpAuthHandlerDigest::AssembleCredentials( return authorization; } +bool HttpAuthHandlerDigest::Init(HttpAuth::ChallengeTokenizer* challenge) { + return ParseChallenge(challenge); +} + +HttpAuth::AuthorizationResult HttpAuthHandlerDigest::HandleAnotherChallenge( + HttpAuth::ChallengeTokenizer* challenge) { + // Even though Digest is not connection based, a "second round" is parsed + // to differentiate between stale and rejected responses. + // Note that the state of the current handler is not mutated - this way if + // there is a rejection the realm hasn't changed. + if (!challenge->valid() || + !LowerCaseEqualsASCII(challenge->scheme(), "digest")) + return HttpAuth::AUTHORIZATION_RESULT_INVALID; + + // Try to find the "stale" value. + while (challenge->GetNext()) { + if (!LowerCaseEqualsASCII(challenge->name(), "stale")) + continue; + if (LowerCaseEqualsASCII(challenge->unquoted_value(), "true")) + return HttpAuth::AUTHORIZATION_RESULT_STALE; + } + + return HttpAuth::AUTHORIZATION_RESULT_REJECT; +} + // The digest challenge header looks like: // WWW-Authenticate: Digest // [realm="<realm-value>"] @@ -231,9 +256,10 @@ bool HttpAuthHandlerDigest::ParseChallenge( qop_ = QOP_UNSPECIFIED; realm_ = nonce_ = domain_ = opaque_ = std::string(); + // FAIL -- Couldn't match auth-scheme. if (!challenge->valid() || !LowerCaseEqualsASCII(challenge->scheme(), "digest")) - return false; // FAIL -- Couldn't match auth-scheme. + return false; // Loop through all the properties. while (challenge->GetNext()) { @@ -242,17 +268,18 @@ bool HttpAuthHandlerDigest::ParseChallenge( return false; } + // FAIL -- couldn't parse a property. if (!ParseChallengeProperty(challenge->name(), challenge->unquoted_value())) - return false; // FAIL -- couldn't parse a property. + return false; } // Check if tokenizer failed. if (!challenge->valid()) - return false; // FAIL + return false; // Check that a minimum set of properties were provided. if (nonce_.empty()) - return false; // FAIL + return false; return true; } diff --git a/net/http/http_auth_handler_digest.h b/net/http/http_auth_handler_digest.h index ad70ac1..0c38641 100644 --- a/net/http/http_auth_handler_digest.h +++ b/net/http/http_auth_handler_digest.h @@ -32,10 +32,11 @@ class HttpAuthHandlerDigest : public HttpAuthHandler { scoped_ptr<HttpAuthHandler>* handler); }; + HttpAuth::AuthorizationResult HandleAnotherChallenge( + HttpAuth::ChallengeTokenizer* challenge); + protected: - virtual bool Init(HttpAuth::ChallengeTokenizer* challenge) { - return ParseChallenge(challenge); - } + virtual bool Init(HttpAuth::ChallengeTokenizer* challenge); virtual int GenerateAuthTokenImpl(const string16* username, const string16* password, diff --git a/net/http/http_auth_handler_digest_unittest.cc b/net/http/http_auth_handler_digest_unittest.cc index 5129001..8026613 100644 --- a/net/http/http_auth_handler_digest_unittest.cc +++ b/net/http/http_auth_handler_digest_unittest.cc @@ -291,4 +291,34 @@ TEST(HttpAuthHandlerDigestTest, AssembleCredentials) { } } +TEST(HttpAuthHandlerDigest, HandleAnotherChallenge_Failed) { + scoped_ptr<HttpAuthHandlerDigest::Factory> factory( + new HttpAuthHandlerDigest::Factory()); + scoped_ptr<HttpAuthHandler> handler; + std::string default_challenge = + "Digest realm=\"Oblivion\", nonce=\"nonce-value\""; + GURL origin("intranet.google.com"); + int rv = factory->CreateAuthHandlerFromString( + default_challenge, HttpAuth::AUTH_SERVER, origin, BoundNetLog(), + &handler); + EXPECT_EQ(OK, rv); + + HttpAuth::ChallengeTokenizer tok_default(default_challenge.begin(), + default_challenge.end()); + EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_REJECT, + handler->HandleAnotherChallenge(&tok_default)); + + std::string stale_challenge = default_challenge + ", stale=true"; + HttpAuth::ChallengeTokenizer tok_stale(stale_challenge.begin(), + stale_challenge.end()); + EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_STALE, + handler->HandleAnotherChallenge(&tok_stale)); + + std::string stale_false_challenge = default_challenge + ", stale=false"; + HttpAuth::ChallengeTokenizer tok_stale_false(stale_false_challenge.begin(), + stale_false_challenge.end()); + EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_REJECT, + handler->HandleAnotherChallenge(&tok_stale_false)); +} + } // namespace net diff --git a/net/http/http_auth_handler_mock.cc b/net/http/http_auth_handler_mock.cc index 9f0011d..90602d0 100644 --- a/net/http/http_auth_handler_mock.cc +++ b/net/http/http_auth_handler_mock.cc @@ -5,6 +5,7 @@ #include "net/http/http_auth_handler_mock.h" #include "base/message_loop.h" +#include "base/string_util.h" #include "net/base/net_errors.h" #include "net/http/http_request_info.h" #include "testing/gtest/include/gtest/gtest.h" @@ -77,6 +78,16 @@ bool HttpAuthHandlerMock::Init(HttpAuth::ChallengeTokenizer* challenge) { return true; } +HttpAuth::AuthorizationResult HttpAuthHandlerMock::HandleAnotherChallenge( + HttpAuth::ChallengeTokenizer* challenge) { + if (!is_connection_based()) + return HttpAuth::AUTHORIZATION_RESULT_REJECT; + if (!challenge->valid() || + !LowerCaseEqualsASCII(challenge->scheme(), "mock")) + return HttpAuth::AUTHORIZATION_RESULT_INVALID; + return HttpAuth::AUTHORIZATION_RESULT_ACCEPT; +} + int HttpAuthHandlerMock::GenerateAuthTokenImpl(const string16* username, const string16* password, const HttpRequestInfo* request, diff --git a/net/http/http_auth_handler_mock.h b/net/http/http_auth_handler_mock.h index 618ec7a..bef8b2b 100644 --- a/net/http/http_auth_handler_mock.h +++ b/net/http/http_auth_handler_mock.h @@ -41,7 +41,6 @@ class HttpAuthHandlerMock : public HttpAuthHandler { CompletionCallback* callback); virtual bool NeedsIdentity() { return first_round_; } - virtual bool IsFinalRound() { return false; } void SetGenerateExpectation(bool async, int rv); @@ -53,6 +52,9 @@ class HttpAuthHandlerMock : public HttpAuthHandler { return request_url_; } + HttpAuth::AuthorizationResult HandleAnotherChallenge( + HttpAuth::ChallengeTokenizer* challenge); + // The Factory class simply returns the same handler each time // CreateAuthHandler is called. class Factory : public HttpAuthHandlerFactory { diff --git a/net/http/http_auth_handler_negotiate.cc b/net/http/http_auth_handler_negotiate.cc index 27e2571..c66ea45 100644 --- a/net/http/http_auth_handler_negotiate.cc +++ b/net/http/http_auth_handler_negotiate.cc @@ -95,6 +95,13 @@ bool HttpAuthHandlerNegotiate::Init(HttpAuth::ChallengeTokenizer* challenge) { scheme_ = "negotiate"; score_ = 4; properties_ = ENCRYPTS_IDENTITY | IS_CONNECTION_BASED; + HttpAuth::AuthorizationResult auth_result = + auth_system_.ParseChallenge(challenge); + return (auth_result == HttpAuth::AUTHORIZATION_RESULT_ACCEPT); +} + +HttpAuth::AuthorizationResult HttpAuthHandlerNegotiate::HandleAnotherChallenge( + HttpAuth::ChallengeTokenizer* challenge) { return auth_system_.ParseChallenge(challenge); } @@ -103,10 +110,6 @@ bool HttpAuthHandlerNegotiate::NeedsIdentity() { return auth_system_.NeedsIdentity(); } -bool HttpAuthHandlerNegotiate::IsFinalRound() { - return auth_system_.IsFinalRound(); -} - bool HttpAuthHandlerNegotiate::AllowsDefaultCredentials() { if (target_ == HttpAuth::AUTH_PROXY) return true; diff --git a/net/http/http_auth_handler_negotiate.h b/net/http/http_auth_handler_negotiate.h index ca9a577..2ff4a9b 100644 --- a/net/http/http_auth_handler_negotiate.h +++ b/net/http/http_auth_handler_negotiate.h @@ -107,10 +107,11 @@ class HttpAuthHandlerNegotiate : public HttpAuthHandler { virtual bool NeedsIdentity(); - virtual bool IsFinalRound(); - virtual bool AllowsDefaultCredentials(); + virtual HttpAuth::AuthorizationResult HandleAnotherChallenge( + HttpAuth::ChallengeTokenizer* challenge); + // These are public for unit tests std::wstring CreateSPN(const AddressList& address_list, const GURL& orign); const std::wstring& spn() const { return spn_; } diff --git a/net/http/http_auth_handler_ntlm.cc b/net/http/http_auth_handler_ntlm.cc index 3a7d25e..dddddb4 100644 --- a/net/http/http_auth_handler_ntlm.cc +++ b/net/http/http_auth_handler_ntlm.cc @@ -92,27 +92,48 @@ int HttpAuthHandlerNTLM::GenerateAuthTokenImpl( #endif } -// The NTLM challenge header looks like: -// WWW-Authenticate: NTLM auth-data -bool HttpAuthHandlerNTLM::ParseChallenge( - HttpAuth::ChallengeTokenizer* tok) { +bool HttpAuthHandlerNTLM::Init(HttpAuth::ChallengeTokenizer* tok) { scheme_ = "ntlm"; score_ = 3; properties_ = ENCRYPTS_IDENTITY | IS_CONNECTION_BASED; + return ParseChallenge(tok, true) == HttpAuth::AUTHORIZATION_RESULT_ACCEPT; +} + +HttpAuth::AuthorizationResult HttpAuthHandlerNTLM::HandleAnotherChallenge( + HttpAuth::ChallengeTokenizer* challenge) { + return ParseChallenge(challenge, false); +} + +// The NTLM challenge header looks like: +// WWW-Authenticate: NTLM auth-data +HttpAuth::AuthorizationResult HttpAuthHandlerNTLM::ParseChallenge( + HttpAuth::ChallengeTokenizer* tok, bool initial_challenge) { #if defined(NTLM_SSPI) + // auth_sspi_ contains state for whether or not this is the initial challenge. return auth_sspi_.ParseChallenge(tok); #else + // TODO(cbentzel): Most of the logic between SSPI, GSSAPI, and portable NTLM + // authentication parsing could probably be shared - just need to know if + // there was previously a challenge round. auth_data_.clear(); // Verify the challenge's auth-scheme. if (!tok->valid() || !LowerCaseEqualsASCII(tok->scheme(), "ntlm")) - return false; + return HttpAuth::AUTHORIZATION_RESULT_INVALID; tok->set_expect_base64_token(true); - if (tok->GetNext()) - auth_data_.assign(tok->value_begin(), tok->value_end()); - return true; + if (!tok->GetNext()) { + if (!initial_challenge) + return HttpAuth::AUTHORIZATION_RESULT_REJECT; + return HttpAuth::AUTHORIZATION_RESULT_ACCEPT; + } else { + if (initial_challenge) + return HttpAuth::AUTHORIZATION_RESULT_INVALID; + } + + auth_data_.assign(tok->value_begin(), tok->value_end()); + return HttpAuth::AUTHORIZATION_RESULT_ACCEPT; #endif // defined(NTLM_SSPI) } diff --git a/net/http/http_auth_handler_ntlm.h b/net/http/http_auth_handler_ntlm.h index 00d6905..831e43d 100644 --- a/net/http/http_auth_handler_ntlm.h +++ b/net/http/http_auth_handler_ntlm.h @@ -108,14 +108,13 @@ class HttpAuthHandlerNTLM : public HttpAuthHandler { virtual bool NeedsIdentity(); - virtual bool IsFinalRound(); - virtual bool AllowsDefaultCredentials(); + virtual HttpAuth::AuthorizationResult HandleAnotherChallenge( + HttpAuth::ChallengeTokenizer* challenge); + protected: - virtual bool Init(HttpAuth::ChallengeTokenizer* tok) { - return ParseChallenge(tok); - } + virtual bool Init(HttpAuth::ChallengeTokenizer* tok); virtual int GenerateAuthTokenImpl(const string16* username, const string16* password, @@ -138,8 +137,8 @@ class HttpAuthHandlerNTLM : public HttpAuthHandler { #endif // Parse the challenge, saving the results into this instance. - // Returns true on success. - bool ParseChallenge(HttpAuth::ChallengeTokenizer* tok); + HttpAuth::AuthorizationResult ParseChallenge( + HttpAuth::ChallengeTokenizer* tok, bool initial_challenge); // Given an input token received from the server, generate the next output // token to be sent to the server. diff --git a/net/http/http_auth_handler_ntlm_portable.cc b/net/http/http_auth_handler_ntlm_portable.cc index e16d32c..d3abc98 100644 --- a/net/http/http_auth_handler_ntlm_portable.cc +++ b/net/http/http_auth_handler_ntlm_portable.cc @@ -653,10 +653,6 @@ bool HttpAuthHandlerNTLM::NeedsIdentity() { return !auth_data_.empty(); } -bool HttpAuthHandlerNTLM::IsFinalRound() { - return !auth_data_.empty(); -} - bool HttpAuthHandlerNTLM::AllowsDefaultCredentials() { // Default credentials are not supported in the portable implementation of // NTLM, but are supported in the SSPI implementation. diff --git a/net/http/http_auth_handler_ntlm_win.cc b/net/http/http_auth_handler_ntlm_win.cc index c1990d37..1b7d1ac 100644 --- a/net/http/http_auth_handler_ntlm_win.cc +++ b/net/http/http_auth_handler_ntlm_win.cc @@ -34,10 +34,6 @@ bool HttpAuthHandlerNTLM::NeedsIdentity() { return auth_sspi_.NeedsIdentity(); } -bool HttpAuthHandlerNTLM::IsFinalRound() { - return auth_sspi_.IsFinalRound(); -} - bool HttpAuthHandlerNTLM::AllowsDefaultCredentials() { if (target_ == HttpAuth::AUTH_PROXY) return true; diff --git a/net/http/http_auth_sspi_win.cc b/net/http/http_auth_sspi_win.cc index 6ff9529..03742e4 100644 --- a/net/http/http_auth_sspi_win.cc +++ b/net/http/http_auth_sspi_win.cc @@ -129,10 +129,6 @@ bool HttpAuthSSPI::NeedsIdentity() const { return decoded_server_auth_token_.empty(); } -bool HttpAuthSSPI::IsFinalRound() const { - return !decoded_server_auth_token_.empty(); -} - void HttpAuthSSPI::Delegate() { can_delegate_ = true; } @@ -144,27 +140,35 @@ void HttpAuthSSPI::ResetSecurityContext() { } } -bool HttpAuthSSPI::ParseChallenge(HttpAuth::ChallengeTokenizer* tok) { +HttpAuth::AuthorizationResult HttpAuthSSPI::ParseChallenge( + HttpAuth::ChallengeTokenizer* tok) { // Verify the challenge's auth-scheme. if (!tok->valid() || !LowerCaseEqualsASCII(tok->scheme(), StringToLowerASCII(scheme_).c_str())) - return false; + return HttpAuth::AUTHORIZATION_RESULT_INVALID; tok->set_expect_base64_token(true); if (!tok->GetNext()) { - decoded_server_auth_token_.clear(); - return true; + // If a context has already been established, an empty challenge + // should be treated as a rejection of the current attempt. + if (SecIsValidHandle(&ctxt_)) + return HttpAuth::AUTHORIZATION_RESULT_REJECT; + DCHECK(decoded_server_auth_token_.empty()); + return HttpAuth::AUTHORIZATION_RESULT_ACCEPT; + } else { + // If a context has not already been established, additional tokens should + // not be present in the auth challenge. + if (!SecIsValidHandle(&ctxt_)) + return HttpAuth::AUTHORIZATION_RESULT_INVALID; } std::string encoded_auth_token = tok->value(); std::string decoded_auth_token; bool base64_rv = base::Base64Decode(encoded_auth_token, &decoded_auth_token); - if (!base64_rv) { - LOG(ERROR) << "Base64 decoding of auth token failed."; - return false; - } + if (!base64_rv) + return HttpAuth::AUTHORIZATION_RESULT_INVALID; decoded_server_auth_token_ = decoded_auth_token; - return true; + return HttpAuth::AUTHORIZATION_RESULT_ACCEPT; } int HttpAuthSSPI::GenerateAuthToken(const string16* username, @@ -174,7 +178,7 @@ int HttpAuthSSPI::GenerateAuthToken(const string16* username, DCHECK((username == NULL) == (password == NULL)); // Initial challenge. - if (!IsFinalRound()) { + if (!SecIsValidHandle(&cred_)) { int rv = OnFirstRound(username, password); if (rv != OK) return rv; diff --git a/net/http/http_auth_sspi_win.h b/net/http/http_auth_sspi_win.h index 482ab6b..c4df58e 100644 --- a/net/http/http_auth_sspi_win.h +++ b/net/http/http_auth_sspi_win.h @@ -79,9 +79,9 @@ class HttpAuthSSPI { ~HttpAuthSSPI(); bool NeedsIdentity() const; - bool IsFinalRound() const; - bool ParseChallenge(HttpAuth::ChallengeTokenizer* tok); + HttpAuth::AuthorizationResult ParseChallenge( + HttpAuth::ChallengeTokenizer* tok); // Generates an authentication token for the service specified by the // Service Principal Name |spn| and stores the value in |*auth_token|. @@ -102,8 +102,7 @@ class HttpAuthSSPI { void Delegate(); private: - int OnFirstRound(const string16* username, - const string16* password); + int OnFirstRound(const string16* username, const string16* password); int GetNextSecurityToken( const std::wstring& spn, diff --git a/net/http/http_auth_sspi_win_unittest.cc b/net/http/http_auth_sspi_win_unittest.cc index fdef793..331dcd6 100644 --- a/net/http/http_auth_sspi_win_unittest.cc +++ b/net/http/http_auth_sspi_win_unittest.cc @@ -22,6 +22,8 @@ void MatchDomainUserAfterSplit(const std::wstring& combined, EXPECT_EQ(expected_user, actual_user); } +const ULONG kMaxTokenLength = 100; + } // namespace TEST(HttpAuthSSPITest, SplitUserAndDomain) { @@ -36,7 +38,7 @@ TEST(HttpAuthSSPITest, DetermineMaxTokenLength_Normal) { MockSSPILibrary mock_library; mock_library.ExpectQuerySecurityPackageInfo(L"NTLM", SEC_E_OK, &package_info); - ULONG max_token_length = 100; + ULONG max_token_length = kMaxTokenLength; int rv = DetermineMaxTokenLength(&mock_library, L"NTLM", &max_token_length); EXPECT_EQ(OK, rv); EXPECT_EQ(1337, max_token_length); @@ -46,7 +48,7 @@ TEST(HttpAuthSSPITest, DetermineMaxTokenLength_InvalidPackage) { MockSSPILibrary mock_library; mock_library.ExpectQuerySecurityPackageInfo(L"Foo", SEC_E_SECPKG_NOT_FOUND, NULL); - ULONG max_token_length = 100; + ULONG max_token_length = kMaxTokenLength; int rv = DetermineMaxTokenLength(&mock_library, L"Foo", &max_token_length); EXPECT_EQ(ERR_UNSUPPORTED_AUTH_SCHEME, rv); // |DetermineMaxTokenLength()| interface states that |max_token_length| should @@ -54,4 +56,100 @@ TEST(HttpAuthSSPITest, DetermineMaxTokenLength_InvalidPackage) { EXPECT_EQ(100, max_token_length); } +TEST(HttpAuthSSPITest, ParseChallenge_FirstRound) { + // The first round should just consist of an unadorned "Negotiate" header. + MockSSPILibrary mock_library; + HttpAuthSSPI auth_sspi(&mock_library, "Negotiate", + NEGOSSP_NAME, kMaxTokenLength); + std::string challenge_text = "Negotiate"; + HttpAuth::ChallengeTokenizer challenge(challenge_text.begin(), + challenge_text.end()); + EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_ACCEPT, + auth_sspi.ParseChallenge(&challenge)); +} + +TEST(HttpAuthSSPITest, ParseChallenge_TwoRounds) { + // The first round should just have "Negotiate", and the second round should + // have a valid base64 token associated with it. + MockSSPILibrary mock_library; + HttpAuthSSPI auth_sspi(&mock_library, "Negotiate", + NEGOSSP_NAME, kMaxTokenLength); + std::string first_challenge_text = "Negotiate"; + HttpAuth::ChallengeTokenizer first_challenge(first_challenge_text.begin(), + first_challenge_text.end()); + EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_ACCEPT, + auth_sspi.ParseChallenge(&first_challenge)); + + // Generate an auth token and create another thing. + std::string auth_token; + EXPECT_EQ(OK, auth_sspi.GenerateAuthToken(NULL, NULL, + L"HTTP/intranet.google.com", + &auth_token)); + + std::string second_challenge_text = "Negotiate Zm9vYmFy"; + HttpAuth::ChallengeTokenizer second_challenge(second_challenge_text.begin(), + second_challenge_text.end()); + EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_ACCEPT, + auth_sspi.ParseChallenge(&second_challenge)); +} + +TEST(HttpAuthSSPITest, ParseChallenge_UnexpectedTokenFirstRound) { + // If the first round challenge has an additional authentication token, it + // should be treated as an invalid challenge from the server. + MockSSPILibrary mock_library; + HttpAuthSSPI auth_sspi(&mock_library, "Negotiate", + NEGOSSP_NAME, kMaxTokenLength); + std::string challenge_text = "Negotiate Zm9vYmFy"; + HttpAuth::ChallengeTokenizer challenge(challenge_text.begin(), + challenge_text.end()); + EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_INVALID, + auth_sspi.ParseChallenge(&challenge)); +} + +TEST(HttpAuthSSPITest, ParseChallenge_MissingTokenSecondRound) { + // If a later-round challenge is simply "Negotiate", it should be treated as + // an authentication challenge rejection from the server or proxy. + MockSSPILibrary mock_library; + HttpAuthSSPI auth_sspi(&mock_library, "Negotiate", + NEGOSSP_NAME, kMaxTokenLength); + std::string first_challenge_text = "Negotiate"; + HttpAuth::ChallengeTokenizer first_challenge(first_challenge_text.begin(), + first_challenge_text.end()); + EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_ACCEPT, + auth_sspi.ParseChallenge(&first_challenge)); + + std::string auth_token; + EXPECT_EQ(OK, auth_sspi.GenerateAuthToken(NULL, NULL, + L"HTTP/intranet.google.com", + &auth_token)); + std::string second_challenge_text = "Negotiate"; + HttpAuth::ChallengeTokenizer second_challenge(second_challenge_text.begin(), + second_challenge_text.end()); + EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_REJECT, + auth_sspi.ParseChallenge(&second_challenge)); +} + +TEST(HttpAuthSSPITest, ParseChallenge_NonBase64EncodedToken) { + // If a later-round challenge has an invalid base64 encoded token, it should + // be treated as an invalid challenge. + MockSSPILibrary mock_library; + HttpAuthSSPI auth_sspi(&mock_library, "Negotiate", + NEGOSSP_NAME, kMaxTokenLength); + std::string first_challenge_text = "Negotiate"; + HttpAuth::ChallengeTokenizer first_challenge(first_challenge_text.begin(), + first_challenge_text.end()); + EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_ACCEPT, + auth_sspi.ParseChallenge(&first_challenge)); + + std::string auth_token; + EXPECT_EQ(OK, auth_sspi.GenerateAuthToken(NULL, NULL, + L"HTTP/intranet.google.com", + &auth_token)); + std::string second_challenge_text = "Negotiate =happyjoy="; + HttpAuth::ChallengeTokenizer second_challenge(second_challenge_text.begin(), + second_challenge_text.end()); + EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_INVALID, + auth_sspi.ParseChallenge(&second_challenge)); +} + } // namespace net diff --git a/net/http/http_auth_unittest.cc b/net/http/http_auth_unittest.cc index cdc7e16..4d8ddee 100644 --- a/net/http/http_auth_unittest.cc +++ b/net/http/http_auth_unittest.cc @@ -13,12 +13,36 @@ #include "net/http/http_auth_filter.h" #include "net/http/http_auth_handler.h" #include "net/http/http_auth_handler_factory.h" +#include "net/http/http_auth_handler_mock.h" #include "net/http/http_response_headers.h" #include "net/http/http_util.h" #include "testing/gtest/include/gtest/gtest.h" namespace net { +namespace { + +HttpAuthHandlerMock* CreateMockHandler(bool connection_based) { + HttpAuthHandlerMock* auth_handler = new HttpAuthHandlerMock(); + auth_handler->set_connection_based(connection_based); + std::string challenge_text = "Mock"; + HttpAuth::ChallengeTokenizer challenge(challenge_text.begin(), + challenge_text.end()); + GURL origin("www.example.com"); + EXPECT_TRUE(auth_handler->InitFromChallenge(&challenge, + HttpAuth::AUTH_SERVER, + origin, + BoundNetLog())); + return auth_handler; +} + +HttpResponseHeaders* HeadersFromResponseText(const std::string& response) { + return new HttpResponseHeaders( + HttpUtil::AssembleRawHeaders(response.c_str(), response.length())); +} + +} // namespace + TEST(HttpAuthTest, ChooseBestChallenge) { static const struct { const char* headers; @@ -82,11 +106,8 @@ TEST(HttpAuthTest, ChooseBestChallenge) { // Make a HttpResponseHeaders object. std::string headers_with_status_line("HTTP/1.1 401 Unauthorized\n"); headers_with_status_line += tests[i].headers; - scoped_refptr<net::HttpResponseHeaders> headers( - new net::HttpResponseHeaders( - net::HttpUtil::AssembleRawHeaders( - headers_with_status_line.c_str(), - headers_with_status_line.length()))); + scoped_refptr<HttpResponseHeaders> headers( + HeadersFromResponseText(headers_with_status_line)); scoped_ptr<HttpAuthHandler> handler; HttpAuth::ChooseBestChallenge(http_auth_handler_factory.get(), @@ -107,130 +128,49 @@ TEST(HttpAuthTest, ChooseBestChallenge) { } } -TEST(HttpAuthTest, ChooseBestChallengeConnectionBasedNTLM) { - static const struct { - const char* headers; - const char* challenge_realm; - } tests[] = { - { - "WWW-Authenticate: NTLM\r\n", - - "", - }, - { - "WWW-Authenticate: NTLM " - "TlRMTVNTUAACAAAADAAMADgAAAAFgokCTroKF1e/DRcAAAAAAAAAALo" - "AugBEAAAABQEoCgAAAA9HAE8ATwBHAEwARQACAAwARwBPAE8ARwBMAE" - "UAAQAaAEEASwBFAEUAUwBBAFIAQQAtAEMATwBSAFAABAAeAGMAbwByA" - "HAALgBnAG8AbwBnAGwAZQAuAGMAbwBtAAMAQABhAGsAZQBlAHMAYQBy" - "AGEALQBjAG8AcgBwAC4AYQBkAC4AYwBvAHIAcAAuAGcAbwBvAGcAbAB" - "lAC4AYwBvAG0ABQAeAGMAbwByAHAALgBnAG8AbwBnAGwAZQAuAGMAbw" - "BtAAAAAAA=\r\n", - - // Realm is empty. - "", - } - }; - GURL origin("http://www.example.com"); +TEST(HttpAuthTest, HandleChallengeResponse_RequestBased) { + scoped_ptr<HttpAuthHandlerMock> mock_handler(CreateMockHandler(false)); std::set<std::string> disabled_schemes; - - scoped_ptr<HttpAuthHandlerFactory> http_auth_handler_factory( - HttpAuthHandlerFactory::CreateDefault()); - scoped_ptr<HttpAuthHandler> handler; - - for (size_t i = 0; i < ARRAYSIZE_UNSAFE(tests); ++i) { - // Make a HttpResponseHeaders object. - std::string headers_with_status_line("HTTP/1.1 401 Unauthorized\n"); - headers_with_status_line += tests[i].headers; - scoped_refptr<net::HttpResponseHeaders> headers( - new net::HttpResponseHeaders( - net::HttpUtil::AssembleRawHeaders( - headers_with_status_line.c_str(), - headers_with_status_line.length()))); - - // possibly_deleted_old_handler may point to deleted memory - // after ChooseBestChallenge has been called, and should not - // be dereferenced. - HttpAuthHandler* possibly_deleted_old_handler = handler.get(); - HttpAuth::ChooseBestChallenge(http_auth_handler_factory.get(), - headers.get(), - HttpAuth::AUTH_SERVER, - origin, - disabled_schemes, - BoundNetLog(), - &handler); - EXPECT_TRUE(handler != NULL); - // Since NTLM is connection-based, we should continue to use the existing - // handler rather than creating a new one. - if (i != 0) - EXPECT_EQ(possibly_deleted_old_handler, handler.get()); - ASSERT_NE(reinterpret_cast<net::HttpAuthHandler *>(NULL), handler.get()); - EXPECT_STREQ(tests[i].challenge_realm, handler->realm().c_str()); - } + scoped_refptr<HttpResponseHeaders> headers( + HeadersFromResponseText( + "HTTP/1.1 401 Unauthorized\n" + "WWW-Authenticate: Mock token_here\n")); + EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_REJECT, + HttpAuth::HandleChallengeResponse( + mock_handler.get(), + headers.get(), + HttpAuth::AUTH_SERVER, + disabled_schemes)); } -TEST(HttpAuthTest, ChooseBestChallengeConnectionBasedNegotiate) { - static const struct { - const char* headers; - const char* challenge_realm; - } tests[] = { - { - "WWW-Authenticate: Negotiate\r\n", - - "", - }, - { - "WWW-Authenticate: Negotiate " - "TlRMTVNTUAACAAAADAAMADgAAAAFgokCTroKF1e/DRcAAAAAAAAAALo" - "AugBEAAAABQEoCgAAAA9HAE8ATwBHAEwARQACAAwARwBPAE8ARwBMAE" - "UAAQAaAEEASwBFAEUAUwBBAFIAQQAtAEMATwBSAFAABAAeAGMAbwByA" - "HAALgBnAG8AbwBnAGwAZQAuAGMAbwBtAAMAQABhAGsAZQBlAHMAYQBy" - "AGEALQBjAG8AcgBwAC4AYQBkAC4AYwBvAHIAcAAuAGcAbwBvAGcAbAB" - "lAC4AYwBvAG0ABQAeAGMAbwByAHAALgBnAG8AbwBnAGwAZQAuAGMAbw" - "BtAAAAAAA=\r\n", - - // Realm is empty. - "", - } - }; - GURL origin("http://www.example.com"); +TEST(HttpAuthTest, HandleChallengeResponse_ConnectionBased) { + scoped_ptr<HttpAuthHandlerMock> mock_handler(CreateMockHandler(true)); std::set<std::string> disabled_schemes; - URLSecurityManagerAllow url_security_manager; - scoped_ptr<HttpAuthHandlerRegistryFactory> http_auth_handler_factory( - HttpAuthHandlerFactory::CreateDefault()); - http_auth_handler_factory->SetURLSecurityManager( - "negotiate", &url_security_manager); - - scoped_ptr<HttpAuthHandler> handler; - for (size_t i = 0; i < ARRAYSIZE_UNSAFE(tests); ++i) { - // Make a HttpResponseHeaders object. - std::string headers_with_status_line("HTTP/1.1 401 Unauthorized\n"); - headers_with_status_line += tests[i].headers; - scoped_refptr<net::HttpResponseHeaders> headers( - new net::HttpResponseHeaders( - net::HttpUtil::AssembleRawHeaders( - headers_with_status_line.c_str(), - headers_with_status_line.length()))); - - HttpAuthHandler* old_handler = handler.get(); - HttpAuth::ChooseBestChallenge(http_auth_handler_factory.get(), - headers.get(), - HttpAuth::AUTH_SERVER, - origin, - disabled_schemes, - BoundNetLog(), - &handler); - - EXPECT_TRUE(handler != NULL); - // Since Negotiate is connection-based, we should continue to use the - // existing handler rather than creating a new one. - if (i != 0) - EXPECT_EQ(old_handler, handler.get()); - - ASSERT_NE(reinterpret_cast<net::HttpAuthHandler *>(NULL), handler.get()); + scoped_refptr<HttpResponseHeaders> headers( + HeadersFromResponseText( + "HTTP/1.1 401 Unauthorized\n" + "WWW-Authenticate: Mock token_here\n")); + EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_ACCEPT, + HttpAuth::HandleChallengeResponse( + mock_handler.get(), + headers.get(), + HttpAuth::AUTH_SERVER, + disabled_schemes)); +} - EXPECT_STREQ(tests[i].challenge_realm, handler->realm().c_str()); - } +TEST(HttpAuthTest, HandleChallengeResponse_ConnectionBasedNoMatch) { + scoped_ptr<HttpAuthHandlerMock> mock_handler(CreateMockHandler(true)); + std::set<std::string> disabled_schemes; + scoped_refptr<HttpResponseHeaders> headers( + HeadersFromResponseText( + "HTTP/1.1 401 Unauthorized\n" + "WWW-Authenticate: Basic realm=\"happy\"\n")); + EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_REJECT, + HttpAuth::HandleChallengeResponse( + mock_handler.get(), + headers.get(), + HttpAuth::AUTH_SERVER, + disabled_schemes)); } TEST(HttpAuthTest, ChallengeTokenizer) { diff --git a/net/http/http_network_transaction_unittest.cc b/net/http/http_network_transaction_unittest.cc index 5ecd6fd..ef27954 100644 --- a/net/http/http_network_transaction_unittest.cc +++ b/net/http/http_network_transaction_unittest.cc @@ -6654,6 +6654,8 @@ TEST_F(HttpNetworkTransactionTest, MultiRoundAuth) { kGetAuth, // Third round kGetAuth, + // Fourth round + kGetAuth }; MockRead reads[] = { // First round @@ -6661,6 +6663,8 @@ TEST_F(HttpNetworkTransactionTest, MultiRoundAuth) { // Second round kServerChallenge, // Third round + kServerChallenge, + // Fourth round kSuccess, }; StaticSocketDataProvider data_provider(reads, arraysize(reads), @@ -6696,6 +6700,16 @@ TEST_F(HttpNetworkTransactionTest, MultiRoundAuth) { response = trans->GetResponseInfo(); ASSERT_FALSE(response == NULL); EXPECT_TRUE(response->auth_challenge.get() == NULL); + + // Fourth round + auth_handler->SetGenerateExpectation(false, OK); + rv = trans->RestartWithAuth(string16(), string16(), &callback); + if (rv == ERR_IO_PENDING) + rv = callback.WaitForResult(); + EXPECT_EQ(OK, rv); + response = trans->GetResponseInfo(); + ASSERT_FALSE(response == NULL); + EXPECT_TRUE(response->auth_challenge.get() == NULL); } class TLSDecompressionFailureSocketDataProvider : public SocketDataProvider { diff --git a/net/http/mock_sspi_library_win.cc b/net/http/mock_sspi_library_win.cc index d0dae51..c3d875c 100644 --- a/net/http/mock_sspi_library_win.cc +++ b/net/http/mock_sspi_library_win.cc @@ -50,6 +50,10 @@ SECURITY_STATUS MockSSPILibrary::InitializeSecurityContext( uint8* buf = reinterpret_cast<uint8 *>(out_buffer->pvBuffer); buf[0] = 0xAB; buf[1] = 0xBA; + + // Fill in phNewContext with arbitrary value if it's invalid. + if (phNewContext != phContext) + phNewContext->dwLower = phNewContext->dwUpper = ((ULONG_PTR) ((INT_PTR)0)); return SEC_E_OK; } @@ -75,8 +79,10 @@ SECURITY_STATUS MockSSPILibrary::FreeCredentialsHandle( } SECURITY_STATUS MockSSPILibrary::DeleteSecurityContext(PCtxtHandle phContext) { - ADD_FAILURE(); - return ERROR_CALL_NOT_IMPLEMENTED; + EXPECT_TRUE(phContext->dwLower == ((ULONG_PTR) ((INT_PTR) 0))); + EXPECT_TRUE(phContext->dwLower == ((ULONG_PTR) ((INT_PTR) 0))); + SecInvalidateHandle(phContext); + return SEC_E_OK; } SECURITY_STATUS MockSSPILibrary::FreeContextBuffer(PVOID pvContextBuffer) { diff --git a/net/socket_stream/socket_stream.cc b/net/socket_stream/socket_stream.cc index ce82c9b..382571a 100644 --- a/net/socket_stream/socket_stream.cc +++ b/net/socket_stream/socket_stream.cc @@ -930,10 +930,11 @@ int SocketStream::HandleAuthChallenge(const HttpResponseHeaders* headers) { LOG(INFO) << "The proxy " << auth_origin << " requested auth"; - // The auth we tried just failed, hence it can't be valid. - // Remove it from the cache so it won't be used again. - if (auth_handler_.get() && !auth_identity_.invalid && - auth_handler_->IsFinalRound()) { + // TODO(cbentzel): Since SocketStream only suppports basic authentication + // right now, another challenge is always treated as a rejection. + // Ultimately this should be converted to use HttpAuthController like the + // HttpNetworkTransaction has. + if (auth_handler_.get() && !auth_identity_.invalid) { if (auth_identity_.source != HttpAuth::IDENT_SRC_PATH_LOOKUP) auth_cache_.Remove(auth_origin, auth_handler_->realm(), |