diff options
48 files changed, 1906 insertions, 200 deletions
diff --git a/build/android/pylib/gtest/test_package_apk.py b/build/android/pylib/gtest/test_package_apk.py index 16ef21c..a679b03 100644 --- a/build/android/pylib/gtest/test_package_apk.py +++ b/build/android/pylib/gtest/test_package_apk.py @@ -43,6 +43,11 @@ class TestPackageApk(TestPackage): else: self._package_info = constants.PACKAGE_INFO['gtest'] + if suite_name == 'net_unittests': + self._extras = {'RunInSubThread': ''} + else: + self._extras = [] + def _CreateCommandLineFileOnDevice(self, device, options): device.WriteFile(self._package_info.cmdline_file, self.suite_name + ' ' + options) @@ -74,7 +79,8 @@ class TestPackageApk(TestPackage): device.StartActivity( intent.Intent(package=self._package_info.package, activity=self._package_info.activity, - action='android.intent.action.MAIN'), + action='android.intent.action.MAIN', + extras=self._extras), # No wait since the runner waits for FIFO creation anyway. blocking=False, force_stop=force_stop) diff --git a/chrome/browser/io_thread.cc b/chrome/browser/io_thread.cc index 6d23075..43eb98b 100644 --- a/chrome/browser/io_thread.cc +++ b/chrome/browser/io_thread.cc @@ -505,6 +505,8 @@ IOThread::IOThread( auth_delegate_whitelist_ = local_state->GetString( prefs::kAuthNegotiateDelegateWhitelist); gssapi_library_name_ = local_state->GetString(prefs::kGSSAPILibraryName); + auth_android_negotiate_account_type_ = + local_state->GetString(prefs::kAuthAndroidNegotiateAccountType); pref_proxy_config_tracker_.reset( ProxyServiceFactory::CreatePrefProxyConfigTrackerOfLocalState( local_state)); @@ -1039,6 +1041,8 @@ void IOThread::RegisterPrefs(PrefRegistrySimple* registry) { registry->RegisterStringPref(prefs::kAuthNegotiateDelegateWhitelist, std::string()); registry->RegisterStringPref(prefs::kGSSAPILibraryName, std::string()); + registry->RegisterStringPref(prefs::kAuthAndroidNegotiateAccountType, + std::string()); registry->RegisterStringPref( data_reduction_proxy::prefs::kDataReductionProxy, std::string()); registry->RegisterBooleanPref(prefs::kEnableReferrers, true); @@ -1067,9 +1071,9 @@ net::HttpAuthHandlerFactory* IOThread::CreateDefaultAuthHandlerFactory( scoped_ptr<net::HttpAuthHandlerRegistryFactory> registry_factory( net::HttpAuthHandlerRegistryFactory::Create( - supported_schemes, globals_->url_security_manager.get(), - resolver, gssapi_library_name_, negotiate_disable_cname_lookup_, - negotiate_enable_port_)); + supported_schemes, globals_->url_security_manager.get(), resolver, + gssapi_library_name_, auth_android_negotiate_account_type_, + negotiate_disable_cname_lookup_, negotiate_enable_port_)); return registry_factory.release(); } diff --git a/chrome/browser/io_thread.h b/chrome/browser/io_thread.h index 1066a9b..54a90ff 100644 --- a/chrome/browser/io_thread.h +++ b/chrome/browser/io_thread.h @@ -457,6 +457,7 @@ class IOThread : public content::BrowserThreadDelegate { std::string auth_server_whitelist_; std::string auth_delegate_whitelist_; std::string gssapi_library_name_; + std::string auth_android_negotiate_account_type_; // This is an instance of the default SSLConfigServiceManager for the current // platform and it gets SSL preferences from local_state object. diff --git a/chrome/common/pref_names.cc b/chrome/common/pref_names.cc index 46ec969..716be31 100644 --- a/chrome/common/pref_names.cc +++ b/chrome/common/pref_names.cc @@ -1686,6 +1686,11 @@ const char kAuthNegotiateDelegateWhitelist[] = // String that specifies the name of a custom GSSAPI library to load. const char kGSSAPILibraryName[] = "auth.gssapi_library_name"; +// String that specifies the Android account type to use for Negotiate +// authentication. +const char kAuthAndroidNegotiateAccountType[] = + "auth.android_negotiate_account_type"; + // Boolean that specifies whether to allow basic auth prompting on cross- // domain sub-content requests. const char kAllowCrossOriginAuthPrompt[] = "auth.allow_cross_origin_prompt"; diff --git a/chrome/common/pref_names.h b/chrome/common/pref_names.h index 4b4fa6b..6223df1 100644 --- a/chrome/common/pref_names.h +++ b/chrome/common/pref_names.h @@ -693,6 +693,7 @@ extern const char kEnableAuthNegotiatePort[]; extern const char kAuthServerWhitelist[]; extern const char kAuthNegotiateDelegateWhitelist[]; extern const char kGSSAPILibraryName[]; +extern const char kAuthAndroidNegotiateAccountType[]; extern const char kAllowCrossOriginAuthPrompt[]; extern const char kBuiltInDnsClientEnabled[]; diff --git a/google_apis/gcm/tools/mcs_probe.cc b/google_apis/gcm/tools/mcs_probe.cc index eadd3b5..9d0881b 100644 --- a/google_apis/gcm/tools/mcs_probe.cc +++ b/google_apis/gcm/tools/mcs_probe.cc @@ -377,14 +377,9 @@ void MCSProbe::InitializeNetworkState() { transport_security_state_.reset(new net::TransportSecurityState()); url_security_manager_.reset(net::URLSecurityManager::Create(NULL, NULL)); - http_auth_handler_factory_.reset( - net::HttpAuthHandlerRegistryFactory::Create( - std::vector<std::string>(1, "basic"), - url_security_manager_.get(), - host_resolver_.get(), - std::string(), - false, - false)); + http_auth_handler_factory_.reset(net::HttpAuthHandlerRegistryFactory::Create( + std::vector<std::string>(1, "basic"), url_security_manager_.get(), + host_resolver_.get(), std::string(), std::string(), false, false)); http_server_properties_.reset(new net::HttpServerPropertiesImpl()); host_mapping_rules_.reset(new net::HostMappingRules()); proxy_service_.reset(net::ProxyService::CreateDirectWithNetLog(&net_log_)); diff --git a/net/BUILD.gn b/net/BUILD.gn index 8454d7a..e9ad1d2 100644 --- a/net/BUILD.gn +++ b/net/BUILD.gn @@ -1216,6 +1216,7 @@ if (is_android) { "android/java/src/org/chromium/net/AndroidNetworkLibrary.java", "android/java/src/org/chromium/net/AndroidPrivateKey.java", "android/java/src/org/chromium/net/GURLUtils.java", + "android/java/src/org/chromium/net/HttpNegotiateAuthenticator.java", "android/java/src/org/chromium/net/NetworkChangeNotifier.java", "android/java/src/org/chromium/net/ProxyChangeListener.java", "android/java/src/org/chromium/net/X509Util.java", @@ -1225,6 +1226,7 @@ if (is_android) { generate_jni("net_test_jni_headers") { sources = [ "android/javatests/src/org/chromium/net/AndroidKeyStoreTestUtil.java", + "test/android/javatests/src/org/chromium/net/test/DummySpnegoAuthenticator.java", ] jni_package = "net" } @@ -1408,14 +1410,20 @@ if (!is_android && !is_mac) { if (use_kerberos) { defines += [ "USE_KERBEROS" ] - } else { + } + + # These are excluded on Android, because the actual Kerberos support, which + # these test, is in a separate app on Android. + if (!use_kerberos || is_android) { sources -= [ "http/http_auth_gssapi_posix_unittest.cc", - "http/http_auth_handler_negotiate_unittest.cc", "http/mock_gssapi_library_posix.cc", "http/mock_gssapi_library_posix.h", ] } + if (!use_kerberos) { + sources -= [ "http/http_auth_handler_negotiate_unittest.cc" ] + } if (use_openssl || (!is_desktop_linux && !is_chromeos && !is_ios)) { # Only include this test when on Posix and using NSS for diff --git a/net/android/BUILD.gn b/net/android/BUILD.gn index 358e1f7..abe4654 100644 --- a/net/android/BUILD.gn +++ b/net/android/BUILD.gn @@ -30,6 +30,7 @@ android_library("net_java_test_support") { DEPRECATED_java_in_dir = "../test/android/javatests/src" deps = [ "//base:base_java", + ":net_java", ] srcjar_deps = [ ":net_java_test_support_enums_srcjar" ] } @@ -78,6 +79,17 @@ java_cpp_enum("net_android_java_enums_srcjar") { ] } +junit_binary("net_junit_tests") { + java_files = + [ "junit/src/org/chromium/net/HttpNegotiateAuthenticatorTest.java" ] + deps = [ + ":net_java", + "//base:base_java", + "//base:base_java_test_support", + "//third_party/junit:hamcrest", + ] +} + # TODO(GYP) if (false) { unittest_apk("net_unittests_apk") { diff --git a/net/android/dummy_spnego_authenticator.cc b/net/android/dummy_spnego_authenticator.cc new file mode 100644 index 0000000..d42f64c --- /dev/null +++ b/net/android/dummy_spnego_authenticator.cc @@ -0,0 +1,204 @@ +// Copyright (c) 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +#include "net/android/dummy_spnego_authenticator.h" + +#include "base/android/jni_string.h" +#include "base/base64.h" +#include "net/test/jni/DummySpnegoAuthenticator_jni.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace net { + +// iso.org.dod.internet.security.mechanism.snego (1.3.6.1.5.5.2) +// From RFC 4178, which uses SNEGO not SPNEGO. +static const unsigned char kSpnegoOid[] = {0x2b, 0x06, 0x01, 0x05, 0x05, 0x02}; +gss_OID_desc CHROME_GSS_SPNEGO_MECH_OID_DESC_VAL = { + arraysize(kSpnegoOid), + const_cast<unsigned char*>(kSpnegoOid)}; + +gss_OID CHROME_GSS_SPNEGO_MECH_OID_DESC = &CHROME_GSS_SPNEGO_MECH_OID_DESC_VAL; + +namespace { + +// gss_OID helpers. +// NOTE: gss_OID's do not own the data they point to, which should be static. +void ClearOid(gss_OID dest) { + if (!dest) + return; + dest->length = 0; + dest->elements = NULL; +} + +void SetOid(gss_OID dest, const void* src, size_t length) { + if (!dest) + return; + ClearOid(dest); + if (!src) + return; + dest->length = length; + if (length) + dest->elements = const_cast<void*>(src); +} + +void CopyOid(gss_OID dest, const gss_OID_desc* src) { + if (!dest) + return; + ClearOid(dest); + if (!src) + return; + SetOid(dest, src->elements, src->length); +} + +} // namespace + +namespace test { + +GssContextMockImpl::GssContextMockImpl() + : lifetime_rec(0), ctx_flags(0), locally_initiated(0), open(0) { + ClearOid(&mech_type); +} + +GssContextMockImpl::GssContextMockImpl(const GssContextMockImpl& other) + : src_name(other.src_name), + targ_name(other.targ_name), + lifetime_rec(other.lifetime_rec), + ctx_flags(other.ctx_flags), + locally_initiated(other.locally_initiated), + open(other.open) { + CopyOid(&mech_type, &other.mech_type); +} + +GssContextMockImpl::GssContextMockImpl(const char* src_name_in, + const char* targ_name_in, + uint32_t lifetime_rec_in, + const gss_OID_desc& mech_type_in, + uint32_t ctx_flags_in, + int locally_initiated_in, + int open_in) + : src_name(src_name_in ? src_name_in : ""), + targ_name(targ_name_in ? targ_name_in : ""), + lifetime_rec(lifetime_rec_in), + ctx_flags(ctx_flags_in), + locally_initiated(locally_initiated_in), + open(open_in) { + CopyOid(&mech_type, &mech_type_in); +} + +GssContextMockImpl::~GssContextMockImpl() { + ClearOid(&mech_type); +} + +} // namespace test + +namespace android { + +DummySpnegoAuthenticator::SecurityContextQuery::SecurityContextQuery( + const std::string& in_expected_package, + uint32_t in_response_code, + uint32_t in_minor_response_code, + const test::GssContextMockImpl& in_context_info, + const std::string& in_expected_input_token, + const std::string& in_output_token) + : expected_package(in_expected_package), + response_code(in_response_code), + minor_response_code(in_minor_response_code), + context_info(in_context_info), + expected_input_token(in_expected_input_token), + output_token(in_output_token) { +} + +DummySpnegoAuthenticator::SecurityContextQuery::SecurityContextQuery( + const std::string& in_expected_package, + uint32_t in_response_code, + uint32_t in_minor_response_code, + const test::GssContextMockImpl& in_context_info, + const char* in_expected_input_token, + const char* in_output_token) + : expected_package(in_expected_package), + response_code(in_response_code), + minor_response_code(in_minor_response_code), + context_info(in_context_info) { + if (in_expected_input_token) + expected_input_token = in_expected_input_token; + if (in_output_token) + output_token = in_output_token; +} + +DummySpnegoAuthenticator::SecurityContextQuery::SecurityContextQuery() + : response_code(0), minor_response_code(0) { +} + +DummySpnegoAuthenticator::SecurityContextQuery::~SecurityContextQuery() { +} + +base::android::ScopedJavaLocalRef<jstring> +DummySpnegoAuthenticator::SecurityContextQuery::GetTokenToReturn( + JNIEnv* env, + jobject /*obj*/) { + return base::android::ConvertUTF8ToJavaString(env, output_token.c_str()); +} +int DummySpnegoAuthenticator::SecurityContextQuery::GetResult(JNIEnv* /*env*/, + jobject /*obj*/) { + return response_code; +} + +void DummySpnegoAuthenticator::SecurityContextQuery::CheckGetTokenArguments( + JNIEnv* env, + jobject /*obj*/, + jstring j_incoming_token) { + std::string incoming_token = + base::android::ConvertJavaStringToUTF8(env, j_incoming_token); + EXPECT_EQ(expected_input_token, incoming_token); +} + +// Needed to satisfy "complex class" clang requirements. +DummySpnegoAuthenticator::DummySpnegoAuthenticator() { +} + +DummySpnegoAuthenticator::~DummySpnegoAuthenticator() { +} + +void DummySpnegoAuthenticator::EnsureTestAccountExists() { + Java_DummySpnegoAuthenticator_ensureTestAccountExists( + base::android::AttachCurrentThread()); +} + +void DummySpnegoAuthenticator::RemoveTestAccounts() { + Java_DummySpnegoAuthenticator_removeTestAccounts( + base::android::AttachCurrentThread()); +} + +void DummySpnegoAuthenticator::ExpectSecurityContext( + const std::string& expected_package, + uint32_t response_code, + uint32_t minor_response_code, + const test::GssContextMockImpl& context_info, + const std::string& expected_input_token, + const std::string& output_token) { + SecurityContextQuery query(expected_package, response_code, + minor_response_code, context_info, + expected_input_token, output_token); + expected_security_queries_.push_back(query); + Java_DummySpnegoAuthenticator_setNativeAuthenticator( + base::android::AttachCurrentThread(), reinterpret_cast<intptr_t>(this)); +} + +bool DummySpnegoAuthenticator::RegisterJni(JNIEnv* env) { + return RegisterNativesImpl(env); +} + +long DummySpnegoAuthenticator::GetNextQuery(JNIEnv* /*env*/, + jobject /* obj */) { + CheckQueueNotEmpty(); + current_query_ = expected_security_queries_.front(); + expected_security_queries_.pop_front(); + return reinterpret_cast<intptr_t>(¤t_query_); +} + +void DummySpnegoAuthenticator::CheckQueueNotEmpty() { + ASSERT_FALSE(expected_security_queries_.empty()); +} + +} // namespace android +} // namespace net diff --git a/net/android/dummy_spnego_authenticator.h b/net/android/dummy_spnego_authenticator.h new file mode 100644 index 0000000..a7d4a97 --- /dev/null +++ b/net/android/dummy_spnego_authenticator.h @@ -0,0 +1,140 @@ +// Copyright (c) 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef NET_ANDROID_DUMMY_SPNEGO_AUTHENTICATOR_H_ +#define NET_ANDROID_DUMMY_SPNEGO_AUTHENTICATOR_H_ + +#include <jni.h> +#include <cstdint> +#include <list> +#include <string> + +#include "base/android/scoped_java_ref.h" + +// Provides an interface for controlling the DummySpnegoAuthenticator service. +// This includes a basic stub of the Mock GSSAPI library, so that OS independent +// Negotiate authentication tests can be run on Android. +namespace net { + +// These constant values are arbitrary, and different from the real GSSAPI +// values, but must match those used in DummySpnegoAuthenticator.java +#define GSS_S_COMPLETE 0 +#define GSS_S_CONTINUE_NEEDED 1 +#define GSS_S_FAILURE 2 + +class gss_buffer_desc; + +typedef struct gss_OID_desc_struct { + uint32_t length; + void* elements; +} gss_OID_desc, *gss_OID; + +extern gss_OID CHROME_GSS_SPNEGO_MECH_OID_DESC; + +namespace test { + +// Copy of class in Mock GSSAPI library. +class GssContextMockImpl { + public: + GssContextMockImpl(); + GssContextMockImpl(const GssContextMockImpl& other); + GssContextMockImpl(const char* src_name, + const char* targ_name, + uint32_t lifetime_rec, + const gss_OID_desc& mech_type, + uint32_t ctx_flags, + int locally_initiated, + int open); + ~GssContextMockImpl(); + + void Assign(const GssContextMockImpl& other); + + std::string src_name; + std::string targ_name; + int32_t lifetime_rec; + gss_OID_desc mech_type; + int32_t ctx_flags; + int locally_initiated; + int open; +}; + +} // namespace test + +namespace android { + +// Interface to Java DummySpnegoAuthenticator. +class DummySpnegoAuthenticator { + public: + struct SecurityContextQuery { + SecurityContextQuery(const std::string& expected_package, + uint32_t response_code, + uint32_t minor_response_code, + const test::GssContextMockImpl& context_info, + const std::string& expected_input_token, + const std::string& output_token); + SecurityContextQuery(const std::string& expected_package, + uint32_t response_code, + uint32_t minor_response_code, + const test::GssContextMockImpl& context_info, + const char* expected_input_token, + const char* output_token); + SecurityContextQuery(); + ~SecurityContextQuery(); + + // Note that many of these fields only exist for compatibility with the + // non-Android version of the tests. Only the response_code and tokens are + // used or checked on Android. + std::string expected_package; + uint32_t response_code; + uint32_t minor_response_code; + test::GssContextMockImpl context_info; + std::string expected_input_token; + std::string output_token; + + // Java callable members + base::android::ScopedJavaLocalRef<jstring> GetTokenToReturn(JNIEnv* env, + jobject obj); + int GetResult(JNIEnv* env, jobject obj); + + // Called from Java to check the arguments passed to the GetToken. Has to + // be in C++ since these tests are driven by googletest, and can only report + // failures through the googletest C++ API. + void CheckGetTokenArguments(JNIEnv* env, + jobject obj, + jstring incoming_token); + }; + + DummySpnegoAuthenticator(); + + ~DummySpnegoAuthenticator(); + + void ExpectSecurityContext(const std::string& expected_package, + uint32_t response_code, + uint32_t minor_response_code, + const test::GssContextMockImpl& context_info, + const std::string& expected_input_token, + const std::string& output_token); + + static bool RegisterJni(JNIEnv* env); + + static void EnsureTestAccountExists(); + static void RemoveTestAccounts(); + + long GetNextQuery(JNIEnv* env, jobject obj); + + private: + // Abandon the test if the query queue is empty. Has to be a void function to + // allow use of ASSERT_FALSE. + void CheckQueueNotEmpty(); + + std::list<SecurityContextQuery> expected_security_queries_; + // Needed to keep the current query alive once it has been pulled from the + // queue. This is simpler than transferring its ownership to Java. + SecurityContextQuery current_query_; +}; + +} // namespace android +} // namespace net + +#endif // NET_ANDROID_DUMMY_SPNEGO_AUTHENTICATOR_DRIVER_H diff --git a/net/android/http_auth_negotiate_android.cc b/net/android/http_auth_negotiate_android.cc new file mode 100644 index 0000000..a8e9cc3 --- /dev/null +++ b/net/android/http_auth_negotiate_android.cc @@ -0,0 +1,154 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "net/android/http_auth_negotiate_android.h" + +#include "base/android/jni_string.h" +#include "base/android/scoped_java_ref.h" +#include "base/bind.h" +#include "base/callback_helpers.h" +#include "base/location.h" +#include "base/thread_task_runner_handle.h" +#include "jni/HttpNegotiateAuthenticator_jni.h" +#include "net/base/auth.h" +#include "net/base/net_errors.h" +#include "net/http/http_auth_challenge_tokenizer.h" +#include "net/http/http_auth_multi_round_parse.h" + +using base::android::AttachCurrentThread; +using base::android::ConvertUTF8ToJavaString; +using base::android::ConvertJavaStringToUTF8; +using base::android::ScopedJavaLocalRef; + +namespace net { +namespace android { + +JavaNegotiateResultWrapper::JavaNegotiateResultWrapper( + const scoped_refptr<base::TaskRunner>& callback_task_runner, + const base::Callback<void(int, const std::string&)>& thread_safe_callback) + : callback_task_runner_(callback_task_runner), + thread_safe_callback_(thread_safe_callback) { +} + +JavaNegotiateResultWrapper::~JavaNegotiateResultWrapper() { +} + +void JavaNegotiateResultWrapper::SetResult(JNIEnv* env, + jobject obj, + int result, + jstring token) { + // This will be called on the UI thread, so we have to post a task back to the + // correct thread to actually save the result + std::string raw_token = ConvertJavaStringToUTF8(env, token); + // Always post, even if we are on the same thread. This guarantees that the + // result will be delayed until after the request has completed, which + // simplifies the logic. In practice the result will only ever come back on + // the original thread in an obscure error case. + callback_task_runner_->PostTask( + FROM_HERE, base::Bind(thread_safe_callback_, result, raw_token)); + // We will always get precisely one call to set result for each call to + // getNextAuthToken, so we can now delete the callback object, and must + // do so to avoid a memory leak. + delete this; +} + +HttpAuthNegotiateAndroid::HttpAuthNegotiateAndroid( + const std::string& account_type) + : account_type_(account_type), + can_delegate_(false), + first_challenge_(true), + auth_token_(nullptr), + weak_factory_(this) { + DCHECK(!account_type.empty()); + JNIEnv* env = AttachCurrentThread(); + java_authenticator_.Reset(Java_HttpNegotiateAuthenticator_create( + env, ConvertUTF8ToJavaString(env, account_type).obj())); +} + +HttpAuthNegotiateAndroid::~HttpAuthNegotiateAndroid() { +} + +bool HttpAuthNegotiateAndroid::Register(JNIEnv* env) { + return RegisterNativesImpl(env); +} + +bool HttpAuthNegotiateAndroid::Init() { + return true; +} + +bool HttpAuthNegotiateAndroid::NeedsIdentity() const { + return false; +} + +bool HttpAuthNegotiateAndroid::AllowsExplicitCredentials() const { + return false; +} + +HttpAuth::AuthorizationResult HttpAuthNegotiateAndroid::ParseChallenge( + net::HttpAuthChallengeTokenizer* tok) { + if (first_challenge_) { + first_challenge_ = false; + return net::ParseFirstRoundChallenge("negotiate", tok); + } + std::string decoded_auth_token; + return net::ParseLaterRoundChallenge("negotiate", tok, &server_auth_token_, + &decoded_auth_token); +} + +int HttpAuthNegotiateAndroid::GenerateAuthToken( + const AuthCredentials* credentials, + const std::string& spn, + std::string* auth_token, + const net::CompletionCallback& callback) { + DCHECK(auth_token); + DCHECK(completion_callback_.is_null()); + DCHECK(!callback.is_null()); + + auth_token_ = auth_token; + completion_callback_ = callback; + scoped_refptr<base::SingleThreadTaskRunner> callback_task_runner = + base::ThreadTaskRunnerHandle::Get(); + base::Callback<void(int, const std::string&)> thread_safe_callback = + base::Bind(&HttpAuthNegotiateAndroid::SetResultInternal, + weak_factory_.GetWeakPtr()); + JNIEnv* env = AttachCurrentThread(); + ScopedJavaLocalRef<jstring> java_server_auth_token = + ConvertUTF8ToJavaString(env, server_auth_token_); + ScopedJavaLocalRef<jstring> java_spn = ConvertUTF8ToJavaString(env, spn); + ScopedJavaLocalRef<jstring> java_account_type = + ConvertUTF8ToJavaString(env, account_type_); + + // It is intentional that callback_wrapper is not owned or deleted by the + // HttpAuthNegotiateAndroid object. The Java code will call the callback + // asynchronously on a different thread, and needs an object to call it on. As + // such, the callback_wrapper must not be deleted until the callback has been + // called, whatever happens to the HttpAuthNegotiateAndroid object. + // + // Unfortunately we have no automated way of managing C++ objects owned by + // Java, so the Java code must simply be written to guarantee that the + // callback is, in the end, called. + JavaNegotiateResultWrapper* callback_wrapper = new JavaNegotiateResultWrapper( + callback_task_runner, thread_safe_callback); + Java_HttpNegotiateAuthenticator_getNextAuthToken( + env, java_authenticator_.obj(), + reinterpret_cast<intptr_t>(callback_wrapper), java_spn.obj(), + java_server_auth_token.obj(), can_delegate_); + return ERR_IO_PENDING; +} + +void HttpAuthNegotiateAndroid::Delegate() { + can_delegate_ = true; +} + +void HttpAuthNegotiateAndroid::SetResultInternal(int result, + const std::string& raw_token) { + DCHECK(auth_token_); + DCHECK(!completion_callback_.is_null()); + if (result == OK) + *auth_token_ = "Negotiate " + raw_token; + base::ResetAndReturn(&completion_callback_).Run(result); +} + +} // namespace android +} // namespace net diff --git a/net/android/http_auth_negotiate_android.h b/net/android/http_auth_negotiate_android.h new file mode 100644 index 0000000..56990ce --- /dev/null +++ b/net/android/http_auth_negotiate_android.h @@ -0,0 +1,132 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef NET_ANDROID_HTTP_AUTH_NEGOTIATE_ANDROID_H_ +#define NET_ANDROID_HTTP_AUTH_NEGOTIATE_ANDROID_H_ + +#include <jni.h> +#include <string> + +#include "base/android/jni_android.h" +#include "base/callback.h" +#include "base/macros.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "net/base/completion_callback.h" +#include "net/http/http_auth.h" + +namespace net { + +class HttpAuthChallengeTokenizer; + +namespace android { + +// This class provides a threadsafe wrapper for SetResult, which is called from +// Java. A new instance of this class is needed for each call, and the instance +// destroys itself when the callback is received. It is written to allow +// setResult to be called on any thread, but in practice they will be called +// on the application's main thread. +// +// We cannot use a Callback object here, because there is no way of invoking the +// Run method from Java. +class NET_EXPORT_PRIVATE JavaNegotiateResultWrapper { + public: + scoped_refptr<base::TaskRunner> callback_task_runner_; + base::Callback<void(int, const std::string&)> thread_safe_callback_; + + JavaNegotiateResultWrapper( + const scoped_refptr<base::TaskRunner>& callback_task_runner, + const base::Callback<void(int, const std::string&)>& + thread_safe_callback); + + void SetResult(JNIEnv* env, jobject obj, int result, jstring token); + + private: + // Class is only allowed to delete itself, nobody else is allowed to delete. + ~JavaNegotiateResultWrapper(); +}; + +// Class providing Negotiate (SPNEGO/Kerberos) authentication support on +// Android. The actual authentication is done through an Android authenticator +// provided by third parties who want Kerberos support. This class simply +// provides a bridge to the Java code, and hence to the service. See +// https://drive.google.com/open?id=1G7WAaYEKMzj16PTHT_cIYuKXJG6bBcrQ7QQBQ6ihOcQ&authuser=1 +// for the full details. +class NET_EXPORT_PRIVATE HttpAuthNegotiateAndroid { + public: + // Creates an object for one negotiation session. |account_type| is the + // Android account type, used by Android to find the correct authenticator. + explicit HttpAuthNegotiateAndroid(const std::string& account_type); + ~HttpAuthNegotiateAndroid(); + + // Register the JNI for this class. + static bool Register(JNIEnv* env); + + // Does nothing, but needed for compatibility with the Negotiate + // authenticators for other O.S.. Always returns true. + bool Init(); + + // True if authentication needs the identity of the user from Chrome. + bool NeedsIdentity() const; + + // True authentication can use explicit credentials included in the URL. + bool AllowsExplicitCredentials() const; + + // Parse a received Negotiate challenge. + HttpAuth::AuthorizationResult ParseChallenge( + net::HttpAuthChallengeTokenizer* tok); + + // Generates an authentication token. + // + // The return value is an error code. The authentication token will be + // returned in |*auth_token|. If the result code is not |OK|, the value of + // |*auth_token| is unspecified. + // + // If the operation cannot be completed synchronously, |ERR_IO_PENDING| will + // be returned and the real result code will be passed to the completion + // callback. Otherwise the result code is returned immediately from this + // call. + // + // If the AndroidAuthNegotiate object is deleted before completion then the + // callback will not be called. + // + // If no immediate result is returned then |auth_token| must remain valid + // until the callback has been called. + // + // |spn| is the Service Principal Name of the server that the token is + // being generated for. + // + // If this is the first round of a multiple round scheme, credentials are + // obtained using |*credentials|. If |credentials| is NULL, the default + // credentials are used instead. + int GenerateAuthToken(const AuthCredentials* credentials, + const std::string& spn, + std::string* auth_token, + const net::CompletionCallback& callback); + + // Delegation is allowed on the Kerberos ticket. This allows certain servers + // to act as the user, such as an IIS server retrieving data from a + // Kerberized MSSQL server. + void Delegate(); + + private: + void SetResultInternal(int result, const std::string& token); + + std::string account_type_; + bool can_delegate_; + bool first_challenge_; + std::string server_auth_token_; + std::string* auth_token_; + base::android::ScopedJavaGlobalRef<jobject> java_authenticator_; + net::CompletionCallback completion_callback_; + + base::WeakPtrFactory<HttpAuthNegotiateAndroid> weak_factory_; + + DISALLOW_COPY_AND_ASSIGN(HttpAuthNegotiateAndroid); +}; + +} // namespace android +} // namespace net + +#endif // NET_ANDROID_HTTP_AUTH_NEGOTIATE_ANDROID_H_ diff --git a/net/android/http_auth_negotiate_android_unittest.cc b/net/android/http_auth_negotiate_android_unittest.cc new file mode 100644 index 0000000..b1e178f --- /dev/null +++ b/net/android/http_auth_negotiate_android_unittest.cc @@ -0,0 +1,94 @@ +// Copyright (c) 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/run_loop.h" +#include "net/android/dummy_spnego_authenticator.h" +#include "net/android/http_auth_negotiate_android.h" +#include "net/base/net_errors.h" +#include "net/base/test_completion_callback.h" +#include "net/http/http_auth_challenge_tokenizer.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace net { +namespace android { + +TEST(HttpAuthNegotiateAndroidTest, GenerateAuthToken) { + DummySpnegoAuthenticator::EnsureTestAccountExists(); + + std::string auth_token; + + DummySpnegoAuthenticator authenticator; + net::test::GssContextMockImpl mockContext; + authenticator.ExpectSecurityContext("Negotiate", GSS_S_COMPLETE, 0, + mockContext, "", "DummyToken"); + + HttpAuthNegotiateAndroid auth("org.chromium.test.DummySpnegoAuthenticator"); + EXPECT_TRUE(auth.Init()); + + TestCompletionCallback callback; + EXPECT_EQ(OK, callback.GetResult(auth.GenerateAuthToken( + nullptr, "Dummy", &auth_token, callback.callback()))); + + EXPECT_EQ("Negotiate DummyToken", auth_token); + + DummySpnegoAuthenticator::RemoveTestAccounts(); +} + +TEST(HttpAuthNegotiateAndroidTest, ParseChallenge_FirstRound) { + // The first round should just consist of an unadorned "Negotiate" header. + HttpAuthNegotiateAndroid auth("org.chromium.test.DummySpnegoAuthenticator"); + std::string challenge_text = "Negotiate"; + HttpAuthChallengeTokenizer challenge(challenge_text.begin(), + challenge_text.end()); + EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_ACCEPT, + auth.ParseChallenge(&challenge)); +} + +TEST(HttpAuthNegotiateAndroidTest, ParseChallenge_UnexpectedTokenFirstRound) { + // If the first round challenge has an additional authentication token, it + // should be treated as an invalid challenge from the server. + HttpAuthNegotiateAndroid auth("org.chromium.test.DummySpnegoAuthenticator"); + std::string challenge_text = "Negotiate Zm9vYmFy"; + HttpAuthChallengeTokenizer challenge(challenge_text.begin(), + challenge_text.end()); + EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_INVALID, + auth.ParseChallenge(&challenge)); +} + +TEST(HttpAuthNegotiateAndroidTest, ParseChallenge_TwoRounds) { + // The first round should just have "Negotiate", and the second round should + // have a valid base64 token associated with it. + HttpAuthNegotiateAndroid auth("org.chromium.test.DummySpnegoAuthenticator"); + std::string first_challenge_text = "Negotiate"; + HttpAuthChallengeTokenizer first_challenge(first_challenge_text.begin(), + first_challenge_text.end()); + EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_ACCEPT, + auth.ParseChallenge(&first_challenge)); + + std::string second_challenge_text = "Negotiate Zm9vYmFy"; + HttpAuthChallengeTokenizer second_challenge(second_challenge_text.begin(), + second_challenge_text.end()); + EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_ACCEPT, + auth.ParseChallenge(&second_challenge)); +} + +TEST(HttpAuthNegotiateAndroidTest, ParseChallenge_MissingTokenSecondRound) { + // If a later-round challenge is simply "Negotiate", it should be treated as + // an authentication challenge rejection from the server or proxy. + HttpAuthNegotiateAndroid auth("org.chromium.test.DummySpnegoAuthenticator"); + std::string first_challenge_text = "Negotiate"; + HttpAuthChallengeTokenizer first_challenge(first_challenge_text.begin(), + first_challenge_text.end()); + EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_ACCEPT, + auth.ParseChallenge(&first_challenge)); + + std::string second_challenge_text = "Negotiate"; + HttpAuthChallengeTokenizer second_challenge(second_challenge_text.begin(), + second_challenge_text.end()); + EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_REJECT, + auth.ParseChallenge(&second_challenge)); +} + +} // namespace android +} // namespace net diff --git a/net/android/java/src/org/chromium/net/HttpNegotiateAuthenticator.java b/net/android/java/src/org/chromium/net/HttpNegotiateAuthenticator.java new file mode 100644 index 0000000..38dba5a --- /dev/null +++ b/net/android/java/src/org/chromium/net/HttpNegotiateAuthenticator.java @@ -0,0 +1,139 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.net; + +import android.accounts.AccountManager; +import android.accounts.AccountManagerCallback; +import android.accounts.AccountManagerFuture; +import android.accounts.AuthenticatorException; +import android.accounts.OperationCanceledException; +import android.app.Activity; +import android.os.Bundle; +import android.os.Handler; + +import org.chromium.base.ApplicationStatus; +import org.chromium.base.CalledByNative; +import org.chromium.base.JNINamespace; +import org.chromium.base.ThreadUtils; +import org.chromium.base.VisibleForTesting; + +import java.io.IOException; + +/** + * Class to get Auth Tokens for HTTP Negotiate authentication (typically used for Kerberos) An + * instance of this class is created for each separate negotiation. + */ +@JNINamespace("net::android") +public class HttpNegotiateAuthenticator { + private Bundle mSpnegoContext = null; + private final String mAccountType; + private AccountManagerFuture<Bundle> mFuture; + + private HttpNegotiateAuthenticator(String accountType) { + assert !android.text.TextUtils.isEmpty(accountType); + mAccountType = accountType; + } + + /** + * @param accountType The Android account type to use. + */ + @VisibleForTesting + @CalledByNative + static HttpNegotiateAuthenticator create(String accountType) { + return new HttpNegotiateAuthenticator(accountType); + } + + /** + * @param nativeResultObject The C++ object used to return the result. For correct C++ memory + * management we must call nativeSetResult precisely once with this object. + * @param principal The principal (must be host based). + * @param authToken The incoming auth token. + * @param canDelegate True if we can delegate. + */ + @VisibleForTesting + @CalledByNative + void getNextAuthToken(final long nativeResultObject, final String principal, String authToken, + boolean canDelegate) { + assert principal != null; + String authTokenType = HttpNegotiateConstants.SPNEGO_TOKEN_TYPE_BASE + principal; + Activity activity = ApplicationStatus.getLastTrackedFocusedActivity(); + if (activity == null) { + nativeSetResult(nativeResultObject, NetError.ERR_UNEXPECTED, null); + return; + } + AccountManager am = AccountManager.get(activity); + String features[] = {HttpNegotiateConstants.SPNEGO_FEATURE}; + + Bundle options = new Bundle(); + + if (authToken != null) { + options.putString(HttpNegotiateConstants.KEY_INCOMING_AUTH_TOKEN, authToken); + } + if (mSpnegoContext != null) { + options.putBundle(HttpNegotiateConstants.KEY_SPNEGO_CONTEXT, mSpnegoContext); + } + options.putBoolean(HttpNegotiateConstants.KEY_CAN_DELEGATE, canDelegate); + + mFuture = am.getAuthTokenByFeatures(mAccountType, authTokenType, features, activity, null, + options, new AccountManagerCallback<Bundle>() { + + @Override + public void run(AccountManagerFuture<Bundle> future) { + try { + Bundle result = future.getResult(); + mSpnegoContext = + result.getBundle(HttpNegotiateConstants.KEY_SPNEGO_CONTEXT); + int status; + switch (result.getInt(HttpNegotiateConstants.KEY_SPNEGO_RESULT, + HttpNegotiateConstants.ERR_UNEXPECTED)) { + case HttpNegotiateConstants.OK: + status = 0; + break; + case HttpNegotiateConstants.ERR_UNEXPECTED: + status = NetError.ERR_UNEXPECTED; + break; + case HttpNegotiateConstants.ERR_ABORTED: + status = NetError.ERR_ABORTED; + break; + case HttpNegotiateConstants.ERR_UNEXPECTED_SECURITY_LIBRARY_STATUS: + status = NetError.ERR_UNEXPECTED_SECURITY_LIBRARY_STATUS; + break; + case HttpNegotiateConstants.ERR_INVALID_RESPONSE: + status = NetError.ERR_INVALID_RESPONSE; + break; + case HttpNegotiateConstants.ERR_INVALID_AUTH_CREDENTIALS: + status = NetError.ERR_INVALID_AUTH_CREDENTIALS; + break; + case HttpNegotiateConstants.ERR_UNSUPPORTED_AUTH_SCHEME: + status = NetError.ERR_UNSUPPORTED_AUTH_SCHEME; + break; + case HttpNegotiateConstants.ERR_MISSING_AUTH_CREDENTIALS: + status = NetError.ERR_MISSING_AUTH_CREDENTIALS; + break; + case HttpNegotiateConstants + .ERR_UNDOCUMENTED_SECURITY_LIBRARY_STATUS: + status = NetError.ERR_UNDOCUMENTED_SECURITY_LIBRARY_STATUS; + break; + case HttpNegotiateConstants.ERR_MALFORMED_IDENTITY: + status = NetError.ERR_MALFORMED_IDENTITY; + break; + default: + status = NetError.ERR_UNEXPECTED; + } + nativeSetResult(nativeResultObject, status, + result.getString(AccountManager.KEY_AUTHTOKEN)); + } catch (OperationCanceledException | AuthenticatorException + | IOException e) { + nativeSetResult(nativeResultObject, NetError.ERR_ABORTED, null); + } + } + + }, new Handler(ThreadUtils.getUiThreadLooper())); + } + + @VisibleForTesting + native void nativeSetResult( + long nativeJavaNegotiateResultWrapper, int status, String authToken); +} diff --git a/net/android/java/src/org/chromium/net/HttpNegotiateConstants.java b/net/android/java/src/org/chromium/net/HttpNegotiateConstants.java new file mode 100644 index 0000000..fa17647 --- /dev/null +++ b/net/android/java/src/org/chromium/net/HttpNegotiateConstants.java @@ -0,0 +1,51 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.net; + +/** + * Constants used by Chrome in SPNEGO authentication requests to the Android Account Manager. + */ +public class HttpNegotiateConstants { + // Option bundle keys + // + // The token provided by in the HTTP 401 response (Base64 encoded string) + public static final String KEY_INCOMING_AUTH_TOKEN = "incomingAuthToken"; + // The SPNEGO Context from the previous transaction (Bundle) - also used in the response bundle + public static final String KEY_SPNEGO_CONTEXT = "spnegoContext"; + // True if delegation is allowed + public static final String KEY_CAN_DELEGATE = "canDelegate"; + + // Response bundle keys + // + // The returned status from the authenticator. + public static final String KEY_SPNEGO_RESULT = "spnegoResult"; + + // Name of SPNEGO feature + public static final String SPNEGO_FEATURE = "SPNEGO"; + // Prefix of token type. Full token type is "SPNEGO:HOSTBASED:<spn>" + public static final String SPNEGO_TOKEN_TYPE_BASE = "SPNEGO:HOSTBASED:"; + + // Returned status codes + // All OK. Returned token is valid. + public static final int OK = 0; + // An unexpected error. This may be caused by a programming mistake or an invalid assumption. + public static final int ERR_UNEXPECTED = 1; + // Request aborted due to user action. + public static final int ERR_ABORTED = 2; + // An unexpected, but documented, SSPI or GSSAPI status code was returned. + public static final int ERR_UNEXPECTED_SECURITY_LIBRARY_STATUS = 3; + // The server's response was invalid. + public static final int ERR_INVALID_RESPONSE = 4; + // Credentials could not be established during HTTP Authentication. + public static final int ERR_INVALID_AUTH_CREDENTIALS = 5; + // An HTTP Authentication scheme was tried which is not supported on this machine. + public static final int ERR_UNSUPPORTED_AUTH_SCHEME = 6; + // (GSSAPI) No Kerberos credentials were available during HTTP Authentication. + public static final int ERR_MISSING_AUTH_CREDENTIALS = 7; + // An undocumented SSPI or GSSAPI status code was returned. + public static final int ERR_UNDOCUMENTED_SECURITY_LIBRARY_STATUS = 8; + // The identity used for authentication is invalid. + public static final int ERR_MALFORMED_IDENTITY = 9; +} diff --git a/net/android/junit/src/org/chromium/net/HttpNegotiateAuthenticatorTest.java b/net/android/junit/src/org/chromium/net/HttpNegotiateAuthenticatorTest.java new file mode 100644 index 0000000..8647407 --- /dev/null +++ b/net/android/junit/src/org/chromium/net/HttpNegotiateAuthenticatorTest.java @@ -0,0 +1,222 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.net; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.accounts.AccountManager; +import android.accounts.AccountManagerCallback; +import android.accounts.AccountManagerFuture; +import android.accounts.AuthenticatorException; +import android.accounts.OperationCanceledException; +import android.app.Activity; +import android.os.Bundle; +import android.os.Handler; + +import org.chromium.base.BaseChromiumApplication; +import org.chromium.testing.local.LocalRobolectricTestRunner; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.shadows.ShadowAccountManager; + +import java.io.IOException; + +/** + * Robolectric tests for HttpNegotiateAuthenticator + */ +@RunWith(LocalRobolectricTestRunner.class) +@Config(manifest = Config.NONE, + shadows = HttpNegotiateAuthenticatorTest.ExtendedShadowAccountManager.class, + application = BaseChromiumApplication.class) +public class HttpNegotiateAuthenticatorTest { + static int sCallCount = 0; + static String sAccountTypeReceived; + static String sAuthTokenTypeReceived; + static String sFeaturesReceived[]; + static Bundle sAddAccountOptionsReceived; + static Bundle sAuthTokenOptionsReceived; + static AccountManagerCallback<Bundle> sCallbackReceived; + static Handler sHandlerReceived; + + /** + * Robolectic's ShadowAccountManager doesn't implement getAccountsByTypeAndFeature so extend it. + * We simply check the call is correct, and don't try to emulate it Note: Shadow classes need to + * be public and static. + */ + @Implements(AccountManager.class) + public static class ExtendedShadowAccountManager extends ShadowAccountManager { + @Implementation + public AccountManagerFuture<Bundle> getAuthTokenByFeatures(String accountType, + String authTokenType, String[] features, Activity activity, + Bundle addAccountOptions, Bundle getAuthTokenOptions, + AccountManagerCallback<Bundle> callback, Handler handler) { + sCallCount++; + sAccountTypeReceived = accountType; + sAuthTokenTypeReceived = authTokenType; + sFeaturesReceived = features; + sAddAccountOptionsReceived = addAccountOptions; + sAuthTokenOptionsReceived = getAuthTokenOptions; + sCallbackReceived = callback; + sHandlerReceived = handler; + + return null; + } + } + + /** + * Test of {@link HttpNegotiateAuthenticator#getNextAuthToken} + */ + @Test + public void testGetNextAuthToken() { + HttpNegotiateAuthenticator authenticator = + HttpNegotiateAuthenticator.create("Dummy_Account"); + Robolectric.buildActivity(Activity.class).create().start().resume().visible(); + authenticator.getNextAuthToken(0, "test_principal", "", true); + assertThat("getAuthTokenByFeatures called precisely once", sCallCount, equalTo(1)); + assertThat("Received account type matches input", sAccountTypeReceived, + equalTo("Dummy_Account")); + assertThat("AuthTokenType is \"SPNEGO:HOSTBASED:test_principal\"", sAuthTokenTypeReceived, + equalTo("SPNEGO:HOSTBASED:test_principal")); + assertThat("Features are precisely {\"SPNEGO\"}", sFeaturesReceived, + equalTo(new String[] {"SPNEGO"})); + assertThat("No account options requested", sAddAccountOptionsReceived, nullValue()); + assertThat("There is no existing context", + sAuthTokenOptionsReceived.get(HttpNegotiateConstants.KEY_SPNEGO_CONTEXT), + nullValue()); + assertThat("The existing token is empty", + sAuthTokenOptionsReceived.getString(HttpNegotiateConstants.KEY_INCOMING_AUTH_TOKEN), + equalTo("")); + assertThat("Delegation is allowed", + sAuthTokenOptionsReceived.getBoolean(HttpNegotiateConstants.KEY_CAN_DELEGATE), + equalTo(true)); + assertThat("getAuthTokenByFeatures was called with a callback", sCallbackReceived, + notNullValue()); + assertThat("getAuthTokenByFeatures was called with a handler", sHandlerReceived, + notNullValue()); + } + + /** + * Test of callback called when getting the auth token completes. + */ + @Test + public void testAccountManagerCallbackRun() { + // Spy on the authenticator so that we can override and intercept the native method call. + HttpNegotiateAuthenticator authenticator = + spy(HttpNegotiateAuthenticator.create("Dummy_Account")); + doNothing().when(authenticator).nativeSetResult(anyLong(), anyInt(), anyString()); + + Robolectric.buildActivity(Activity.class).create().start().resume().visible(); + + // Call getNextAuthToken to get the callback + authenticator.getNextAuthToken(1234, "test_principal", "", true); + + // Avoid warning when creating mock accountManagerFuture, can't take .class of an + // instantiated generic type, yet compiler complains if I leave it uninstantiated. + @SuppressWarnings("unchecked") + AccountManagerFuture<Bundle> accountManagerFuture = mock(AccountManagerFuture.class); + Bundle resultBundle = new Bundle(); + Bundle context = new Bundle(); + context.putString("String", "test_context"); + resultBundle.putInt(HttpNegotiateConstants.KEY_SPNEGO_RESULT, HttpNegotiateConstants.OK); + resultBundle.putBundle(HttpNegotiateConstants.KEY_SPNEGO_CONTEXT, context); + resultBundle.putString(AccountManager.KEY_AUTHTOKEN, "output_token"); + try { + when(accountManagerFuture.getResult()).thenReturn(resultBundle); + } catch (OperationCanceledException | AuthenticatorException | IOException e) { + // Can never happen - artifact of Mockito. + fail(); + } + sCallbackReceived.run(accountManagerFuture); + verify(authenticator).nativeSetResult(1234, 0, "output_token"); + + // Check that the next call to getNextAuthToken uses the correct context + authenticator.getNextAuthToken(5678, "test_principal", "", true); + assertThat("The spnego context is preserved between calls", + sAuthTokenOptionsReceived.getBundle(HttpNegotiateConstants.KEY_SPNEGO_CONTEXT), + equalTo(context)); + + // Test exception path + try { + when(accountManagerFuture.getResult()).thenThrow(new OperationCanceledException()); + } catch (OperationCanceledException | AuthenticatorException | IOException e) { + // Can never happen - artifact of Mockito. + fail(); + } + sCallbackReceived.run(accountManagerFuture); + verify(authenticator).nativeSetResult(5678, NetError.ERR_ABORTED, null); + } + + private void checkErrorReturn(Integer spnegoError, int expectedError) { + // Spy on the authenticator so that we can override and intercept the native method call. + HttpNegotiateAuthenticator authenticator = + spy(HttpNegotiateAuthenticator.create("Dummy_Account")); + doNothing().when(authenticator).nativeSetResult(anyLong(), anyInt(), anyString()); + + Robolectric.buildActivity(Activity.class).create().start().resume().visible(); + + // Call getNextAuthToken to get the callback + authenticator.getNextAuthToken(1234, "test_principal", "", true); + + // Avoid warning when creating mock accountManagerFuture, can't take .class of an + // instantiated generic type, yet compiler complains if I leave it uninstantiated. + @SuppressWarnings("unchecked") + AccountManagerFuture<Bundle> accountManagerFuture = mock(AccountManagerFuture.class); + Bundle resultBundle = new Bundle(); + if (spnegoError != null) { + resultBundle.putInt(HttpNegotiateConstants.KEY_SPNEGO_RESULT, spnegoError); + } + try { + when(accountManagerFuture.getResult()).thenReturn(resultBundle); + } catch (OperationCanceledException | AuthenticatorException | IOException e) { + // Can never happen - artifact of Mockito. + fail(); + } + sCallbackReceived.run(accountManagerFuture); + verify(authenticator).nativeSetResult(anyLong(), eq(expectedError), anyString()); + } + + /** + * Test of callback error returns when getting the auth token completes. + */ + @Test + public void testAccountManagerCallbackErrorReturns() { + checkErrorReturn(null, NetError.ERR_UNEXPECTED); + checkErrorReturn(HttpNegotiateConstants.ERR_UNEXPECTED, NetError.ERR_UNEXPECTED); + checkErrorReturn(HttpNegotiateConstants.ERR_ABORTED, NetError.ERR_ABORTED); + checkErrorReturn(HttpNegotiateConstants.ERR_UNEXPECTED_SECURITY_LIBRARY_STATUS, + NetError.ERR_UNEXPECTED_SECURITY_LIBRARY_STATUS); + checkErrorReturn( + HttpNegotiateConstants.ERR_INVALID_RESPONSE, NetError.ERR_INVALID_RESPONSE); + checkErrorReturn(HttpNegotiateConstants.ERR_INVALID_AUTH_CREDENTIALS, + NetError.ERR_INVALID_AUTH_CREDENTIALS); + checkErrorReturn(HttpNegotiateConstants.ERR_UNSUPPORTED_AUTH_SCHEME, + NetError.ERR_UNSUPPORTED_AUTH_SCHEME); + checkErrorReturn(HttpNegotiateConstants.ERR_MISSING_AUTH_CREDENTIALS, + NetError.ERR_MISSING_AUTH_CREDENTIALS); + checkErrorReturn(HttpNegotiateConstants.ERR_UNDOCUMENTED_SECURITY_LIBRARY_STATUS, + NetError.ERR_UNDOCUMENTED_SECURITY_LIBRARY_STATUS); + checkErrorReturn( + HttpNegotiateConstants.ERR_MALFORMED_IDENTITY, NetError.ERR_MALFORMED_IDENTITY); + // 9999 is not a valid return value + checkErrorReturn(9999, NetError.ERR_UNEXPECTED); + } +} diff --git a/net/android/net_jni_registrar.cc b/net/android/net_jni_registrar.cc index 1175d96..dec74ec 100644 --- a/net/android/net_jni_registrar.cc +++ b/net/android/net_jni_registrar.cc @@ -8,6 +8,7 @@ #include "base/android/jni_registrar.h" #include "net/android/android_private_key.h" #include "net/android/gurl_utils.h" +#include "net/android/http_auth_negotiate_android.h" #include "net/android/keystore.h" #include "net/android/network_change_notifier_android.h" #include "net/android/network_library.h" @@ -27,6 +28,7 @@ static base::android::RegistrationMethod kNetRegisteredMethods[] = { {"AndroidKeyStore", RegisterKeyStore}, {"AndroidNetworkLibrary", RegisterNetworkLibrary}, {"GURLUtils", RegisterGURLUtils}, + {"HttpAuthNegotiateAndroid", HttpAuthNegotiateAndroid::Register}, {"NetworkChangeNotifierAndroid", NetworkChangeNotifierAndroid::Register}, {"ProxyConfigService", ProxyConfigServiceAndroid::Register}, {"X509Util", RegisterX509Util}, diff --git a/net/android/unittest_support/AndroidManifest.xml b/net/android/unittest_support/AndroidManifest.xml new file mode 100644 index 0000000..d7493de --- /dev/null +++ b/net/android/unittest_support/AndroidManifest.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright 2014 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="org.chromium.native_test" + android:versionCode="1" + android:versionName="1.0"> + + <uses-sdk android:minSdkVersion="16" android:targetSdkVersion="22" /> + <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/> + <uses-permission android:name="android.permission.BLUETOOTH"/> + <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> + <uses-permission android:name="android.permission.CAMERA" /> + <uses-permission android:name="android.permission.GET_ACCOUNTS"/> + <uses-permission android:name="android.permission.INTERNET"/> + <uses-permission android:name="android.permission.MANAGE_ACCOUNTS"/> + <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/> + <uses-permission android:name="android.permission.RECORD_AUDIO"/> + <uses-permission android:name="android.permission.USE_CREDENTIALS"/> + <uses-permission android:name="android.permission.WAKE_LOCK"/> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> + + <application android:label="NativeTests" + android:name="org.chromium.base.BaseChromiumApplication"> + <activity android:name=".NativeUnitTestActivity" + android:label="NativeTest" + android:configChanges="orientation|keyboardHidden"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + <service android:name="org.chromium.net.test.DummySpnegoAuthenticatorService" + android:exported = "false"> + <intent-filter> + <action android:name="android.accounts.AccountAuthenticator" /> + </intent-filter> + <meta-data android:name="android.accounts.AccountAuthenticator" + android:resource="@xml/dummy_spnego_authenticator" /> + </service> + </application> + + <instrumentation android:name="org.chromium.native_test.NativeTestInstrumentationTestRunner" + android:targetPackage="org.chromium.native_test" + android:label="Instrumentation entry point for org.chromium.native_test"/> + +</manifest> diff --git a/net/android/unittest_support/res/mipmap-hdpi/app_icon.png b/net/android/unittest_support/res/mipmap-hdpi/app_icon.png Binary files differnew file mode 100644 index 0000000..da88148 --- /dev/null +++ b/net/android/unittest_support/res/mipmap-hdpi/app_icon.png diff --git a/net/android/unittest_support/res/mipmap-mdpi/app_icon.png b/net/android/unittest_support/res/mipmap-mdpi/app_icon.png Binary files differnew file mode 100644 index 0000000..24611de --- /dev/null +++ b/net/android/unittest_support/res/mipmap-mdpi/app_icon.png diff --git a/net/android/unittest_support/res/mipmap-xhdpi/app_icon.png b/net/android/unittest_support/res/mipmap-xhdpi/app_icon.png Binary files differnew file mode 100644 index 0000000..98c9189 --- /dev/null +++ b/net/android/unittest_support/res/mipmap-xhdpi/app_icon.png diff --git a/net/android/unittest_support/res/mipmap-xxhdpi/app_icon.png b/net/android/unittest_support/res/mipmap-xxhdpi/app_icon.png Binary files differnew file mode 100644 index 0000000..1fbc0e7 --- /dev/null +++ b/net/android/unittest_support/res/mipmap-xxhdpi/app_icon.png diff --git a/net/android/unittest_support/res/mipmap-xxxhdpi/app_icon.png b/net/android/unittest_support/res/mipmap-xxxhdpi/app_icon.png Binary files differnew file mode 100644 index 0000000..5b09d42 --- /dev/null +++ b/net/android/unittest_support/res/mipmap-xxxhdpi/app_icon.png diff --git a/net/android/unittest_support/res/xml/dummy_spnego_account_preferences.xml b/net/android/unittest_support/res/xml/dummy_spnego_account_preferences.xml new file mode 100644 index 0000000..38901dc --- /dev/null +++ b/net/android/unittest_support/res/xml/dummy_spnego_account_preferences.xml @@ -0,0 +1 @@ +<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"/> diff --git a/net/android/unittest_support/res/xml/dummy_spnego_authenticator.xml b/net/android/unittest_support/res/xml/dummy_spnego_authenticator.xml new file mode 100644 index 0000000..aab1b93 --- /dev/null +++ b/net/android/unittest_support/res/xml/dummy_spnego_authenticator.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android" + android:accountType="org.chromium.test.DummySpnegoAuthenticator" + android:label="DummySpengoAuthenticator" + android:icon="@mipmap/app_icon" + android:smallIcon="@mipmap/app_icon" + android:customTokens="true" + android:accountPreferences="@xml/dummy_spnego_account_preferences"/> diff --git a/net/http/http_auth_gssapi_posix.cc b/net/http/http_auth_gssapi_posix.cc index 388cc64..0b87b33 100644 --- a/net/http/http_auth_gssapi_posix.cc +++ b/net/http/http_auth_gssapi_posix.cc @@ -16,7 +16,7 @@ #include "base/threading/thread_restrictions.h" #include "net/base/net_errors.h" #include "net/base/net_util.h" -#include "net/http/http_auth_challenge_tokenizer.h" +#include "net/http/http_auth_multi_round_parse.h" // These are defined for the GSSAPI library: // Paraphrasing the comments from gssapi.h: @@ -687,39 +687,18 @@ void HttpAuthGSSAPI::Delegate() { HttpAuth::AuthorizationResult HttpAuthGSSAPI::ParseChallenge( HttpAuthChallengeTokenizer* tok) { - // Verify the challenge's auth-scheme. - if (!base::LowerCaseEqualsASCII(tok->scheme(), - base::StringToLowerASCII(scheme_).c_str())) - return HttpAuth::AUTHORIZATION_RESULT_INVALID; - - std::string encoded_auth_token = tok->base64_param(); - - if (encoded_auth_token.empty()) { - // 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; + if (scoped_sec_context_.get() == GSS_C_NO_CONTEXT) { + return net::ParseFirstRoundChallenge(scheme_, tok); } - - // Make sure the additional token is base64 encoded. - std::string decoded_auth_token; - bool base64_rv = base::Base64Decode(encoded_auth_token, &decoded_auth_token); - if (!base64_rv) - return HttpAuth::AUTHORIZATION_RESULT_INVALID; - decoded_server_auth_token_ = decoded_auth_token; - return HttpAuth::AUTHORIZATION_RESULT_ACCEPT; + std::string encoded_auth_token; + return net::ParseLaterRoundChallenge(scheme_, tok, &encoded_auth_token, + &decoded_server_auth_token_); } int HttpAuthGSSAPI::GenerateAuthToken(const AuthCredentials* credentials, const std::string& spn, - std::string* auth_token) { + std::string* auth_token, + const CompletionCallback& /*callback*/) { DCHECK(auth_token); gss_buffer_desc input_token = GSS_C_EMPTY_BUFFER; diff --git a/net/http/http_auth_gssapi_posix.h b/net/http/http_auth_gssapi_posix.h index ab967c96..73502c9 100644 --- a/net/http/http_auth_gssapi_posix.h +++ b/net/http/http_auth_gssapi_posix.h @@ -9,6 +9,7 @@ #include "base/gtest_prod_util.h" #include "base/native_library.h" +#include "net/base/completion_callback.h" #include "net/base/net_export.h" #include "net/http/http_auth.h" @@ -22,7 +23,7 @@ // Chrome supports OSX 10.6, which doesn't have access to GSS.framework. Chrome // always dlopens libgssapi_krb5.dylib, which is provided by -// Kerberos.framework. On OSX 10.7+ this is an ABI comptabile shim that loads +// Kerberos.framework. On OSX 10.7+ this is an ABI compatible shim that loads // GSS.framework. #include <Kerberos/gssapi.h> #elif defined(OS_FREEBSD) @@ -246,19 +247,35 @@ class NET_EXPORT_PRIVATE HttpAuthGSSAPI { HttpAuthChallengeTokenizer* tok); // Generates an authentication token. - // The return value is an error code. If it's not |OK|, the value of + // + // The return value is an error code. The authentication token will be + // returned in |*auth_token|. If the result code is not |OK|, the value of // |*auth_token| is unspecified. + // + // If the operation cannot be completed synchronously, |ERR_IO_PENDING| will + // be returned and the real result code will be passed to the completion + // callback. Otherwise the result code is returned immediately from this + // call. + // + // If the HttpAuthGSSAPI object is deleted before completion then the callback + // will not be called. + // + // If no immediate result is returned then |auth_token| must remain valid + // until the callback has been called. + // // |spn| is the Service Principal Name of the server that the token is // being generated for. + // // If this is the first round of a multiple round scheme, credentials are // obtained using |*credentials|. If |credentials| is NULL, the default // credentials are used instead. int GenerateAuthToken(const AuthCredentials* credentials, const std::string& spn, - std::string* auth_token); + std::string* auth_token, + const CompletionCallback& callback); // Delegation is allowed on the Kerberos ticket. This allows certain servers - // to act as the user, such as an IIS server retrieiving data from a + // to act as the user, such as an IIS server retrieving data from a // Kerberized MSSQL server. void Delegate(); diff --git a/net/http/http_auth_gssapi_posix_unittest.cc b/net/http/http_auth_gssapi_posix_unittest.cc index 6f93334..79a248f 100644 --- a/net/http/http_auth_gssapi_posix_unittest.cc +++ b/net/http/http_auth_gssapi_posix_unittest.cc @@ -71,6 +71,12 @@ void EstablishInitialContext(test::MockGSSAPILibrary* library) { out_buffer); } +void UnexpectedCallback(int result) { + // At present getting tokens from gssapi is fully synchronous, so the callback + // should never be called. + ADD_FAILURE(); +} + } // namespace TEST(HttpAuthGSSAPIPOSIXTest, GSSAPIStartup) { @@ -204,7 +210,8 @@ TEST(HttpAuthGSSAPITest, ParseChallenge_TwoRounds) { EstablishInitialContext(&mock_library); std::string auth_token; EXPECT_EQ(OK, auth_gssapi.GenerateAuthToken(NULL, "HTTP/intranet.google.com", - &auth_token)); + &auth_token, + base::Bind(&UnexpectedCallback))); std::string second_challenge_text = "Negotiate Zm9vYmFy"; HttpAuthChallengeTokenizer second_challenge(second_challenge_text.begin(), @@ -241,7 +248,8 @@ TEST(HttpAuthGSSAPITest, ParseChallenge_MissingTokenSecondRound) { EstablishInitialContext(&mock_library); std::string auth_token; EXPECT_EQ(OK, auth_gssapi.GenerateAuthToken(NULL, "HTTP/intranet.google.com", - &auth_token)); + &auth_token, + base::Bind(&UnexpectedCallback))); std::string second_challenge_text = "Negotiate"; HttpAuthChallengeTokenizer second_challenge(second_challenge_text.begin(), second_challenge_text.end()); @@ -264,7 +272,8 @@ TEST(HttpAuthGSSAPITest, ParseChallenge_NonBase64EncodedToken) { EstablishInitialContext(&mock_library); std::string auth_token; EXPECT_EQ(OK, auth_gssapi.GenerateAuthToken(NULL, "HTTP/intranet.google.com", - &auth_token)); + &auth_token, + base::Bind(&UnexpectedCallback))); std::string second_challenge_text = "Negotiate =happyjoy="; HttpAuthChallengeTokenizer second_challenge(second_challenge_text.begin(), second_challenge_text.end()); diff --git a/net/http/http_auth_handler_factory.cc b/net/http/http_auth_handler_factory.cc index decb20e..4f09c9f 100644 --- a/net/http/http_auth_handler_factory.cc +++ b/net/http/http_auth_handler_factory.cc @@ -53,7 +53,9 @@ HttpAuthHandlerRegistryFactory* HttpAuthHandlerFactory::CreateDefault( registry_factory->RegisterSchemeFactory( "digest", new HttpAuthHandlerDigest::Factory()); -#if defined(USE_KERBEROS) +// On Android Chrome needs an account type configured to enable Kerberos, +// so the default factory should not include Kerberos. +#if defined(USE_KERBEROS) && !defined(OS_ANDROID) HttpAuthHandlerNegotiate::Factory* negotiate_factory = new HttpAuthHandlerNegotiate::Factory(); #if defined(OS_POSIX) @@ -63,7 +65,7 @@ HttpAuthHandlerRegistryFactory* HttpAuthHandlerFactory::CreateDefault( #endif negotiate_factory->set_host_resolver(host_resolver); registry_factory->RegisterSchemeFactory("negotiate", negotiate_factory); -#endif // defined(USE_KERBEROS) +#endif // defined(USE_KERBEROS) && !defined(OS_ANDROID) HttpAuthHandlerNTLM::Factory* ntlm_factory = new HttpAuthHandlerNTLM::Factory(); @@ -131,6 +133,7 @@ HttpAuthHandlerRegistryFactory* HttpAuthHandlerRegistryFactory::Create( URLSecurityManager* security_manager, HostResolver* host_resolver, const std::string& gssapi_library_name, + const std::string& auth_android_negotiate_account_type, bool negotiate_disable_cname_lookup, bool negotiate_enable_port) { HttpAuthHandlerRegistryFactory* registry_factory = @@ -154,7 +157,9 @@ HttpAuthHandlerRegistryFactory* HttpAuthHandlerRegistryFactory::Create( if (IsSupportedScheme(supported_schemes, "negotiate")) { HttpAuthHandlerNegotiate::Factory* negotiate_factory = new HttpAuthHandlerNegotiate::Factory(); -#if defined(OS_POSIX) +#if defined(OS_ANDROID) + negotiate_factory->set_library(&auth_android_negotiate_account_type); +#elif defined(OS_POSIX) negotiate_factory->set_library( new GSSAPISharedLibrary(gssapi_library_name)); #elif defined(OS_WIN) diff --git a/net/http/http_auth_handler_factory.h b/net/http/http_auth_handler_factory.h index 06f7f34..30b1896c 100644 --- a/net/http/http_auth_handler_factory.h +++ b/net/http/http_auth_handler_factory.h @@ -169,7 +169,12 @@ class NET_EXPORT HttpAuthHandlerRegistryFactory // |host_resolver| must not be NULL. // // |gssapi_library_name| specifies the name of the GSSAPI library that will - // be loaded on all platforms except Windows. + // be loaded on Posix platforms other than Android. |gssapi_library_name| is + // ignored on Android and Windows. + // + // |auth_android_negotiate_account_type| is an Android account type, used to + // find the appropriate authenticator service on Android. It is ignored on + // non-Android platforms. // // |negotiate_disable_cname_lookup| and |negotiate_enable_port| both control // how Negotiate does SPN generation, by default these should be false. @@ -178,6 +183,7 @@ class NET_EXPORT HttpAuthHandlerRegistryFactory URLSecurityManager* security_manager, HostResolver* host_resolver, const std::string& gssapi_library_name, + const std::string& auth_android_negotiate_account_type, bool negotiate_disable_cname_lookup, bool negotiate_enable_port); diff --git a/net/http/http_auth_handler_factory_unittest.cc b/net/http/http_auth_handler_factory_unittest.cc index 2aa7958..be0c3b0 100644 --- a/net/http/http_auth_handler_factory_unittest.cc +++ b/net/http/http_auth_handler_factory_unittest.cc @@ -172,7 +172,8 @@ TEST(HttpAuthHandlerFactoryTest, DefaultFactory) { server_origin, BoundNetLog(), &handler); -#if defined(USE_KERBEROS) +// Note the default factory doesn't support Kerberos on Android +#if defined(USE_KERBEROS) && !defined(OS_ANDROID) EXPECT_EQ(OK, rv); ASSERT_FALSE(handler.get() == NULL); EXPECT_EQ(HttpAuth::AUTH_SCHEME_NEGOTIATE, handler->auth_scheme()); @@ -183,7 +184,7 @@ TEST(HttpAuthHandlerFactoryTest, DefaultFactory) { #else EXPECT_EQ(ERR_UNSUPPORTED_AUTH_SCHEME, rv); EXPECT_TRUE(handler.get() == NULL); -#endif // defined(USE_KERBEROS) +#endif // defined(USE_KERBEROS) && !defined(OS_ANDROID) } } diff --git a/net/http/http_auth_handler_negotiate.cc b/net/http/http_auth_handler_negotiate.cc index 422ddd7..b02913b 100644 --- a/net/http/http_auth_handler_negotiate.cc +++ b/net/http/http_auth_handler_negotiate.cc @@ -61,10 +61,14 @@ int HttpAuthHandlerNegotiate::Factory::CreateAuthHandler( new HttpAuthHandlerNegotiate(auth_library_.get(), max_token_length_, url_security_manager(), resolver_, disable_cname_lookup_, use_port_)); - if (!tmp_handler->InitFromChallenge(challenge, target, origin, net_log)) - return ERR_INVALID_RESPONSE; - handler->swap(tmp_handler); - return OK; +#elif defined(OS_ANDROID) + if (is_unsupported_ || auth_library_->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. + scoped_ptr<HttpAuthHandler> tmp_handler(new HttpAuthHandlerNegotiate( + auth_library_.get(), url_security_manager(), resolver_, + disable_cname_lookup_, use_port_)); #elif defined(OS_POSIX) if (is_unsupported_) return ERR_UNSUPPORTED_AUTH_SCHEME; @@ -78,11 +82,11 @@ int HttpAuthHandlerNegotiate::Factory::CreateAuthHandler( new HttpAuthHandlerNegotiate(auth_library_.get(), url_security_manager(), resolver_, disable_cname_lookup_, use_port_)); +#endif if (!tmp_handler->InitFromChallenge(challenge, target, origin, net_log)) return ERR_INVALID_RESPONSE; handler->swap(tmp_handler); return OK; -#endif } HttpAuthHandlerNegotiate::HttpAuthHandlerNegotiate( @@ -94,7 +98,9 @@ HttpAuthHandlerNegotiate::HttpAuthHandlerNegotiate( HostResolver* resolver, bool disable_cname_lookup, bool use_port) -#if defined(OS_WIN) +#if defined(OS_ANDROID) + : auth_system_(*auth_library), +#elif defined(OS_WIN) : auth_system_(auth_library, "Negotiate", NEGOSSP_NAME, max_token_length), #elif defined(OS_POSIX) : auth_system_(auth_library, "Negotiate", CHROME_GSS_SPNEGO_MECH_OID_DESC), @@ -315,8 +321,10 @@ int HttpAuthHandlerNegotiate::DoResolveCanonicalNameComplete(int rv) { int HttpAuthHandlerNegotiate::DoGenerateAuthToken() { next_state_ = STATE_GENERATE_AUTH_TOKEN_COMPLETE; AuthCredentials* credentials = has_credentials_ ? &credentials_ : NULL; - // TODO(cbentzel): This should possibly be done async. - return auth_system_.GenerateAuthToken(credentials, spn_, auth_token_); + return auth_system_.GenerateAuthToken( + credentials, spn_, auth_token_, + base::Bind(&HttpAuthHandlerNegotiate::OnIOComplete, + base::Unretained(this))); } int HttpAuthHandlerNegotiate::DoGenerateAuthTokenComplete(int rv) { diff --git a/net/http/http_auth_handler_negotiate.h b/net/http/http_auth_handler_negotiate.h index fc596fa..d8b9d9d 100644 --- a/net/http/http_auth_handler_negotiate.h +++ b/net/http/http_auth_handler_negotiate.h @@ -13,7 +13,9 @@ #include "net/http/http_auth_handler.h" #include "net/http/http_auth_handler_factory.h" -#if defined(OS_WIN) +#if defined(OS_ANDROID) +#include "net/android/http_auth_negotiate_android.h" +#elif defined(OS_WIN) #include "net/http/http_auth_sspi_win.h" #elif defined(OS_POSIX) #include "net/http/http_auth_gssapi_posix.h" @@ -32,7 +34,12 @@ class URLSecurityManager; class NET_EXPORT_PRIVATE HttpAuthHandlerNegotiate : public HttpAuthHandler { public: -#if defined(OS_WIN) +#if defined(OS_ANDROID) + typedef net::android::HttpAuthNegotiateAndroid AuthSystem; + // For Android this isn't a library, but for the Android Account type, which + // indirectly identifies the Kerberos/SPNEGO authentication app. + typedef const std::string AuthLibrary; +#elif defined(OS_WIN) typedef SSPILibrary AuthLibrary; typedef HttpAuthSSPI AuthSystem; #elif defined(OS_POSIX) @@ -65,8 +72,8 @@ class NET_EXPORT_PRIVATE HttpAuthHandlerNegotiate : public HttpAuthHandler { // Sets the system library to use, thereby assuming ownership of // |auth_library|. - void set_library(AuthLibrary* auth_library) { - auth_library_.reset(auth_library); + void set_library(AuthLibrary* auth_provider) { + auth_library_.reset(auth_provider); } int CreateAuthHandler(HttpAuthChallengeTokenizer* challenge, @@ -89,7 +96,7 @@ class NET_EXPORT_PRIVATE HttpAuthHandlerNegotiate : public HttpAuthHandler { scoped_ptr<AuthLibrary> auth_library_; }; - HttpAuthHandlerNegotiate(AuthLibrary* sspi_library, + HttpAuthHandlerNegotiate(AuthLibrary* auth_library, #if defined(OS_WIN) ULONG max_token_length, #endif diff --git a/net/http/http_auth_handler_negotiate_unittest.cc b/net/http/http_auth_handler_negotiate_unittest.cc index eaee8e5..49ebdad6 100644 --- a/net/http/http_auth_handler_negotiate_unittest.cc +++ b/net/http/http_auth_handler_negotiate_unittest.cc @@ -4,6 +4,8 @@ #include "net/http/http_auth_handler_negotiate.h" +#include <string> + #include "base/strings/string_util.h" #include "base/strings/utf_string_conversions.h" #include "net/base/net_errors.h" @@ -11,7 +13,9 @@ #include "net/dns/mock_host_resolver.h" #include "net/http/http_request_info.h" #include "net/http/mock_allow_url_security_manager.h" -#if defined(OS_WIN) +#if defined(OS_ANDROID) +#include "net/android/dummy_spnego_authenticator.h" +#elif defined(OS_WIN) #include "net/http/mock_sspi_library_win.h" #elif defined(OS_POSIX) #include "net/http/mock_gssapi_library_posix.h" @@ -21,7 +25,9 @@ namespace net { -#if defined(OS_WIN) +#if defined(OS_ANDROID) +typedef net::android::DummySpnegoAuthenticator MockAuthLibrary; +#elif defined(OS_WIN) typedef MockSSPILibrary MockAuthLibrary; #elif defined(OS_POSIX) typedef test::MockGSSAPILibrary MockAuthLibrary; @@ -38,10 +44,22 @@ class HttpAuthHandlerNegotiateTest : public PlatformTest { url_security_manager_.reset(new MockAllowURLSecurityManager()); factory_.reset(new HttpAuthHandlerNegotiate::Factory()); factory_->set_url_security_manager(url_security_manager_.get()); +#if defined(OS_ANDROID) + std::string* authenticator = + new std::string("org.chromium.test.DummySpnegoAuthenticator"); + factory_->set_library(authenticator); + MockAuthLibrary::EnsureTestAccountExists(); +#endif +#if defined(OS_WIN) || (defined(OS_POSIX) && !defined(OS_ANDROID)) factory_->set_library(auth_library_); +#endif factory_->set_host_resolver(resolver_.get()); } +#if defined(OS_ANDROID) + void TearDown() override { MockAuthLibrary::RemoveTestAccounts(); } +#endif + void SetupMocks(MockAuthLibrary* mock_library) { #if defined(OS_WIN) security_package_.reset(new SecPkgInfoW); @@ -113,21 +131,21 @@ class HttpAuthHandlerNegotiateTest : public PlatformTest { 0, // Context flags 1, // Locally initiated 1); // Open - test::MockGSSAPILibrary::SecurityContextQuery queries[] = { - test::MockGSSAPILibrary::SecurityContextQuery( - "Negotiate", // Package name - GSS_S_CONTINUE_NEEDED, // Major response code - 0, // Minor response code - context1, // Context - NULL, // Expected input token - kAuthResponse), // Output token - test::MockGSSAPILibrary::SecurityContextQuery( - "Negotiate", // Package name - GSS_S_COMPLETE, // Major response code - 0, // Minor response code - context2, // Context - kAuthResponse, // Expected input token - kAuthResponse) // Output token + MockAuthLibrary::SecurityContextQuery queries[] = { + MockAuthLibrary::SecurityContextQuery( + "Negotiate", // Package name + GSS_S_CONTINUE_NEEDED, // Major response code + 0, // Minor response code + context1, // Context + NULL, // Expected input token + kAuthResponse), // Output token + MockAuthLibrary::SecurityContextQuery( + "Negotiate", // Package name + GSS_S_COMPLETE, // Major response code + 0, // Minor response code + context2, // Context + kAuthResponse, // Expected input token + kAuthResponse) // Output token }; for (size_t i = 0; i < arraysize(queries); ++i) { @@ -154,13 +172,13 @@ class HttpAuthHandlerNegotiateTest : public PlatformTest { 0, // Context flags 1, // Locally initiated 0); // Open - test::MockGSSAPILibrary::SecurityContextQuery query( - "Negotiate", // Package name - major_status, // Major response code - minor_status, // Minor response code - context, // Context - NULL, // Expected input token - NULL); // Output token + MockAuthLibrary::SecurityContextQuery query( + "Negotiate", // Package name + major_status, // Major response code + minor_status, // Minor response code + context, // Context + NULL, // Expected input token + NULL); // Output token mock_library->ExpectSecurityContext(query.expected_package, query.response_code, @@ -223,8 +241,8 @@ TEST_F(HttpAuthHandlerNegotiateTest, DisableCname) { TestCompletionCallback callback; HttpRequestInfo request_info; std::string token; - EXPECT_EQ(OK, auth_handler->GenerateAuthToken(NULL, &request_info, - callback.callback(), &token)); + EXPECT_EQ(OK, callback.GetResult(auth_handler->GenerateAuthToken( + NULL, &request_info, callback.callback(), &token))); #if defined(OS_WIN) EXPECT_EQ("HTTP/alias", auth_handler->spn()); #elif defined(OS_POSIX) @@ -241,8 +259,8 @@ TEST_F(HttpAuthHandlerNegotiateTest, DisableCnameStandardPort) { TestCompletionCallback callback; HttpRequestInfo request_info; std::string token; - EXPECT_EQ(OK, auth_handler->GenerateAuthToken(NULL, &request_info, - callback.callback(), &token)); + EXPECT_EQ(OK, callback.GetResult(auth_handler->GenerateAuthToken( + NULL, &request_info, callback.callback(), &token))); #if defined(OS_WIN) EXPECT_EQ("HTTP/alias", auth_handler->spn()); #elif defined(OS_POSIX) @@ -259,8 +277,8 @@ TEST_F(HttpAuthHandlerNegotiateTest, DisableCnameNonstandardPort) { TestCompletionCallback callback; HttpRequestInfo request_info; std::string token; - EXPECT_EQ(OK, auth_handler->GenerateAuthToken(NULL, &request_info, - callback.callback(), &token)); + EXPECT_EQ(OK, callback.GetResult(auth_handler->GenerateAuthToken( + NULL, &request_info, callback.callback(), &token))); #if defined(OS_WIN) EXPECT_EQ("HTTP/alias:500", auth_handler->spn()); #elif defined(OS_POSIX) @@ -277,8 +295,8 @@ TEST_F(HttpAuthHandlerNegotiateTest, CnameSync) { TestCompletionCallback callback; HttpRequestInfo request_info; std::string token; - EXPECT_EQ(OK, auth_handler->GenerateAuthToken(NULL, &request_info, - callback.callback(), &token)); + EXPECT_EQ(OK, callback.GetResult(auth_handler->GenerateAuthToken( + NULL, &request_info, callback.callback(), &token))); #if defined(OS_WIN) EXPECT_EQ("HTTP/canonical.example.com", auth_handler->spn()); #elif defined(OS_POSIX) diff --git a/net/http/http_auth_handler_ntlm.cc b/net/http/http_auth_handler_ntlm.cc index 51a3232..0bf7260 100644 --- a/net/http/http_auth_handler_ntlm.cc +++ b/net/http/http_auth_handler_ntlm.cc @@ -33,10 +33,8 @@ int HttpAuthHandlerNTLM::GenerateAuthTokenImpl( const AuthCredentials* credentials, const HttpRequestInfo* request, const CompletionCallback& callback, std::string* auth_token) { #if defined(NTLM_SSPI) - return auth_sspi_.GenerateAuthToken( - credentials, - CreateSPN(origin_), - auth_token); + return auth_sspi_.GenerateAuthToken(credentials, CreateSPN(origin_), + auth_token, callback); #else // !defined(NTLM_SSPI) // TODO(cbentzel): Shouldn't be hitting this case. if (!credentials) { diff --git a/net/http/http_auth_multi_round_parse.cc b/net/http/http_auth_multi_round_parse.cc new file mode 100644 index 0000000..efee2bb --- /dev/null +++ b/net/http/http_auth_multi_round_parse.cc @@ -0,0 +1,58 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/base64.h" +#include "base/strings/string_util.h" +#include "net/http/http_auth_challenge_tokenizer.h" +#include "net/http/http_auth_multi_round_parse.h" + +namespace net { + +namespace { + +// Check that the scheme in the challenge matches the expected scheme +bool SchemeIsValid(const std::string& scheme, + HttpAuthChallengeTokenizer* challenge) { + // There is no guarantee that challenge->scheme() is valid ASCII, but + // LowerCaseEqualsASCII will do the right thing even if it isn't. + return base::LowerCaseEqualsASCII(challenge->scheme(), + base::StringToLowerASCII(scheme).c_str()); +} + +} // namespace + +HttpAuth::AuthorizationResult ParseFirstRoundChallenge( + const std::string& scheme, + HttpAuthChallengeTokenizer* challenge) { + // Verify the challenge's auth-scheme. + if (!SchemeIsValid(scheme, challenge)) + return HttpAuth::AUTHORIZATION_RESULT_INVALID; + + std::string encoded_auth_token = challenge->base64_param(); + if (!encoded_auth_token.empty()) { + return HttpAuth::AUTHORIZATION_RESULT_INVALID; + } + return HttpAuth::AUTHORIZATION_RESULT_ACCEPT; +} + +HttpAuth::AuthorizationResult ParseLaterRoundChallenge( + const std::string& scheme, + HttpAuthChallengeTokenizer* challenge, + std::string* encoded_token, + std::string* decoded_token) { + // Verify the challenge's auth-scheme. + if (!SchemeIsValid(scheme, challenge)) + return HttpAuth::AUTHORIZATION_RESULT_INVALID; + + *encoded_token = challenge->base64_param(); + if (encoded_token->empty()) + return HttpAuth::AUTHORIZATION_RESULT_REJECT; + + // Make sure the additional token is base64 encoded. + if (!base::Base64Decode(*encoded_token, decoded_token)) + return HttpAuth::AUTHORIZATION_RESULT_INVALID; + return HttpAuth::AUTHORIZATION_RESULT_ACCEPT; +} + +} // namespace net diff --git a/net/http/http_auth_multi_round_parse.h b/net/http/http_auth_multi_round_parse.h new file mode 100644 index 0000000..2fb6347 --- /dev/null +++ b/net/http/http_auth_multi_round_parse.h @@ -0,0 +1,29 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef NET_HTTP_HTTP_AUTH_MULTI_ROUND_PARSE_H_ +#define NET_HTTP_HTTP_AUTH_MULTI_ROUND_PARSE_H_ + +#include <string> + +#include "net/base/net_export.h" +#include "net/http/http_auth.h" + +namespace net { + +class HttpAuthChallengeTokenizer; + +NET_EXPORT_PRIVATE HttpAuth::AuthorizationResult ParseFirstRoundChallenge( + const std::string& scheme, + HttpAuthChallengeTokenizer* challenge); + +NET_EXPORT_PRIVATE HttpAuth::AuthorizationResult ParseLaterRoundChallenge( + const std::string& scheme, + HttpAuthChallengeTokenizer* challenge, + std::string* encoded_token, + std::string* decoded_token); + +} // namespace net + +#endif // NET_HTTP_HTTP_AUTH_MULTI_ROUND_PARSE_H_ diff --git a/net/http/http_auth_multi_round_parse_unittest.cc b/net/http/http_auth_multi_round_parse_unittest.cc new file mode 100644 index 0000000..1be5ad1 --- /dev/null +++ b/net/http/http_auth_multi_round_parse_unittest.cc @@ -0,0 +1,78 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// 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_challenge_tokenizer.h" +#include "net/http/http_auth_multi_round_parse.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace net { + +TEST(HttpAuthHandlerNegotiateParseTest, ParseFirstRoundChallenge) { + // The first round should just consist of an unadorned header with the scheme + // name. + std::string challenge_text = "DummyScheme"; + HttpAuthChallengeTokenizer challenge(challenge_text.begin(), + challenge_text.end()); + EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_ACCEPT, + ParseFirstRoundChallenge("dummyscheme", &challenge)); +} + +TEST(HttpAuthHandlerNegotiateParseTest, + ParseFirstNegotiateChallenge_UnexpectedToken) { + // If the first round challenge has an additional authentication token, it + // should be treated as an invalid challenge from the server. + std::string challenge_text = "Negotiate Zm9vYmFy"; + HttpAuthChallengeTokenizer challenge(challenge_text.begin(), + challenge_text.end()); + EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_INVALID, + ParseFirstRoundChallenge("negotiate", &challenge)); +} + +TEST(HttpAuthHandlerNegotiateParseTest, + ParseFirstNegotiateChallenge_BadScheme) { + std::string challenge_text = "DummyScheme"; + HttpAuthChallengeTokenizer challenge(challenge_text.begin(), + challenge_text.end()); + EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_INVALID, + ParseFirstRoundChallenge("negotiate", &challenge)); +} + +TEST(HttpAuthHandlerNegotiateParseTest, ParseLaterRoundChallenge) { + // Later rounds should always have a Base64 encoded token. + std::string challenge_text = "Negotiate Zm9vYmFy"; + HttpAuthChallengeTokenizer challenge(challenge_text.begin(), + challenge_text.end()); + std::string encoded_token; + std::string decoded_token; + EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_ACCEPT, + ParseLaterRoundChallenge("negotiate", &challenge, &encoded_token, + &decoded_token)); + EXPECT_EQ("Zm9vYmFy", encoded_token); + EXPECT_EQ("foobar", decoded_token); +} + +TEST(HttpAuthHandlerNegotiateParseTest, + ParseAnotherNegotiateChallenge_MissingToken) { + std::string challenge_text = "Negotiate"; + HttpAuthChallengeTokenizer challenge(challenge_text.begin(), + challenge_text.end()); + std::string encoded_token; + std::string decoded_token; + EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_REJECT, + ParseLaterRoundChallenge("negotiate", &challenge, &encoded_token, + &decoded_token)); +} + +TEST(HttpAuthHandlerNegotiateParseTest, + ParseAnotherNegotiateChallenge_InvalidToken) { + std::string challenge_text = "Negotiate ***"; + HttpAuthChallengeTokenizer challenge(challenge_text.begin(), + challenge_text.end()); + std::string encoded_token; + std::string decoded_token; + EXPECT_EQ(HttpAuth::AUTHORIZATION_RESULT_INVALID, + ParseLaterRoundChallenge("negotiate", &challenge, &encoded_token, + &decoded_token)); +} +} diff --git a/net/http/http_auth_sspi_win.cc b/net/http/http_auth_sspi_win.cc index c935d33..f59ce4d 100644 --- a/net/http/http_auth_sspi_win.cc +++ b/net/http/http_auth_sspi_win.cc @@ -13,7 +13,7 @@ #include "base/strings/utf_string_conversions.h" #include "net/base/net_errors.h" #include "net/http/http_auth.h" -#include "net/http/http_auth_challenge_tokenizer.h" +#include "net/http/http_auth_multi_round_parse.h" namespace net { @@ -282,37 +282,18 @@ void HttpAuthSSPI::ResetSecurityContext() { HttpAuth::AuthorizationResult HttpAuthSSPI::ParseChallenge( HttpAuthChallengeTokenizer* tok) { - // Verify the challenge's auth-scheme. - if (!base::LowerCaseEqualsASCII(tok->scheme(), - base::StringToLowerASCII(scheme_).c_str())) - return HttpAuth::AUTHORIZATION_RESULT_INVALID; - - std::string encoded_auth_token = tok->base64_param(); - if (encoded_auth_token.empty()) { - // 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; + if (!SecIsValidHandle(&ctxt_)) { + return net::ParseFirstRoundChallenge(scheme_, tok); } - - std::string decoded_auth_token; - bool base64_rv = base::Base64Decode(encoded_auth_token, &decoded_auth_token); - if (!base64_rv) - return HttpAuth::AUTHORIZATION_RESULT_INVALID; - decoded_server_auth_token_ = decoded_auth_token; - return HttpAuth::AUTHORIZATION_RESULT_ACCEPT; + std::string encoded_auth_token; + return net::ParseLaterRoundChallenge(scheme_, tok, &encoded_auth_token, + &decoded_server_auth_token_); } int HttpAuthSSPI::GenerateAuthToken(const AuthCredentials* credentials, const std::string& spn, - std::string* auth_token) { + std::string* auth_token, + const CompletionCallback& /*callback*/) { // Initial challenge. if (!SecIsValidHandle(&cred_)) { int rv = OnFirstRound(credentials); diff --git a/net/http/http_auth_sspi_win.h b/net/http/http_auth_sspi_win.h index befc1bf..1d524fa 100644 --- a/net/http/http_auth_sspi_win.h +++ b/net/http/http_auth_sspi_win.h @@ -17,6 +17,7 @@ #include <string> #include "base/strings/string16.h" +#include "net/base/completion_callback.h" #include "net/base/net_export.h" #include "net/http/http_auth.h" @@ -120,19 +121,36 @@ class NET_EXPORT_PRIVATE HttpAuthSSPI { HttpAuth::AuthorizationResult ParseChallenge( HttpAuthChallengeTokenizer* tok); - // Generates an authentication token for the service specified by the - // Service Principal Name |spn| and stores the value in |*auth_token|. - // If the return value is not |OK|, then the value of |*auth_token| is - // unspecified. ERR_IO_PENDING is not a valid return code. + // Generates an authentication token. + // + // The return value is an error code. The authentication token will be + // returned in |*auth_token|. If the result code is not |OK|, the value of + // |*auth_token| is unspecified. + // + // If the operation cannot be completed synchronously, |ERR_IO_PENDING| will + // be returned and the real result code will be passed to the completion + // callback. Otherwise the result code is returned immediately from this + // call. + // + // If the HttpAuthSPPI object is deleted before completion then the callback + // will not be called. + // + // If no immediate result is returned then |auth_token| must remain valid + // until the callback has been called. + // + // |spn| is the Service Principal Name of the server that the token is + // being generated for. + // // If this is the first round of a multiple round scheme, credentials are - // obtained using |*credentials|. If |credentials| is NULL, the credentials - // for the currently logged in user are used instead. + // obtained using |*credentials|. If |credentials| is NULL, the default + // credentials are used instead. int GenerateAuthToken(const AuthCredentials* credentials, const std::string& spn, - std::string* auth_token); + std::string* auth_token, + const CompletionCallback& callback); // Delegation is allowed on the Kerberos ticket. This allows certain servers - // to act as the user, such as an IIS server retrieiving data from a + // to act as the user, such as an IIS server retrieving data from a // Kerberized MSSQL server. void Delegate(); diff --git a/net/http/http_auth_sspi_win_unittest.cc b/net/http/http_auth_sspi_win_unittest.cc index 586822d..feebaf0 100644 --- a/net/http/http_auth_sspi_win_unittest.cc +++ b/net/http/http_auth_sspi_win_unittest.cc @@ -25,6 +25,12 @@ void MatchDomainUserAfterSplit(const std::wstring& combined, const ULONG kMaxTokenLength = 100; +void UnexpectedCallback(int result) { + // At present getting tokens from gssapi is fully synchronous, so the callback + // should never be called. + ADD_FAILURE(); +} + } // namespace TEST(HttpAuthSSPITest, SplitUserAndDomain) { @@ -84,7 +90,8 @@ TEST(HttpAuthSSPITest, ParseChallenge_TwoRounds) { // Generate an auth token and create another thing. std::string auth_token; EXPECT_EQ(OK, auth_sspi.GenerateAuthToken(NULL, "HTTP/intranet.google.com", - &auth_token)); + &auth_token, + base::Bind(&UnexpectedCallback))); std::string second_challenge_text = "Negotiate Zm9vYmFy"; HttpAuthChallengeTokenizer second_challenge(second_challenge_text.begin(), @@ -120,7 +127,8 @@ TEST(HttpAuthSSPITest, ParseChallenge_MissingTokenSecondRound) { std::string auth_token; EXPECT_EQ(OK, auth_sspi.GenerateAuthToken(NULL, "HTTP/intranet.google.com", - &auth_token)); + &auth_token, + base::Bind(&UnexpectedCallback))); std::string second_challenge_text = "Negotiate"; HttpAuthChallengeTokenizer second_challenge(second_challenge_text.begin(), second_challenge_text.end()); @@ -142,7 +150,8 @@ TEST(HttpAuthSSPITest, ParseChallenge_NonBase64EncodedToken) { std::string auth_token; EXPECT_EQ(OK, auth_sspi.GenerateAuthToken(NULL, "HTTP/intranet.google.com", - &auth_token)); + &auth_token, + base::Bind(&UnexpectedCallback))); std::string second_challenge_text = "Negotiate =happyjoy="; HttpAuthChallengeTokenizer second_challenge(second_challenge_text.begin(), second_challenge_text.end()); diff --git a/net/http/http_auth_unittest.cc b/net/http/http_auth_unittest.cc index 52e8294..a81a409 100644 --- a/net/http/http_auth_unittest.cc +++ b/net/http/http_auth_unittest.cc @@ -69,57 +69,56 @@ TEST(HttpAuthTest, ChooseBestChallenge) { HttpAuth::Scheme challenge_scheme; const char* challenge_realm; } tests[] = { - { - // Basic is the only challenge type, pick it. - "Y: Digest realm=\"X\", nonce=\"aaaaaaaaaa\"\n" - "www-authenticate: Basic realm=\"BasicRealm\"\n", - - HttpAuth::AUTH_SCHEME_BASIC, - "BasicRealm", - }, - { - // Fake is the only challenge type, but it is unsupported. - "Y: Digest realm=\"FooBar\", nonce=\"aaaaaaaaaa\"\n" - "www-authenticate: Fake realm=\"FooBar\"\n", - - HttpAuth::AUTH_SCHEME_MAX, - "", - }, - { - // Pick Digest over Basic. - "www-authenticate: Basic realm=\"FooBar\"\n" - "www-authenticate: Fake realm=\"FooBar\"\n" - "www-authenticate: nonce=\"aaaaaaaaaa\"\n" - "www-authenticate: Digest realm=\"DigestRealm\", nonce=\"aaaaaaaaaa\"\n", - - HttpAuth::AUTH_SCHEME_DIGEST, - "DigestRealm", - }, - { - // Handle an empty header correctly. - "Y: Digest realm=\"X\", nonce=\"aaaaaaaaaa\"\n" - "www-authenticate:\n", - - HttpAuth::AUTH_SCHEME_MAX, - "", - }, - { - "WWW-Authenticate: Negotiate\n" - "WWW-Authenticate: NTLM\n", - -#if defined(USE_KERBEROS) - // Choose Negotiate over NTLM on all platforms. - // TODO(ahendrickson): This may be flaky on Linux and OSX as it - // relies on being able to load one of the known .so files - // for gssapi. - HttpAuth::AUTH_SCHEME_NEGOTIATE, + { + // Basic is the only challenge type, pick it. + "Y: Digest realm=\"X\", nonce=\"aaaaaaaaaa\"\n" + "www-authenticate: Basic realm=\"BasicRealm\"\n", + + HttpAuth::AUTH_SCHEME_BASIC, + "BasicRealm", + }, + { + // Fake is the only challenge type, but it is unsupported. + "Y: Digest realm=\"FooBar\", nonce=\"aaaaaaaaaa\"\n" + "www-authenticate: Fake realm=\"FooBar\"\n", + + HttpAuth::AUTH_SCHEME_MAX, + "", + }, + { + // Pick Digest over Basic. + "www-authenticate: Basic realm=\"FooBar\"\n" + "www-authenticate: Fake realm=\"FooBar\"\n" + "www-authenticate: nonce=\"aaaaaaaaaa\"\n" + "www-authenticate: Digest realm=\"DigestRealm\", nonce=\"aaaaaaaaaa\"\n", + + HttpAuth::AUTH_SCHEME_DIGEST, + "DigestRealm", + }, + { + // Handle an empty header correctly. + "Y: Digest realm=\"X\", nonce=\"aaaaaaaaaa\"\n" + "www-authenticate:\n", + + HttpAuth::AUTH_SCHEME_MAX, + "", + }, + { + "WWW-Authenticate: Negotiate\n" + "WWW-Authenticate: NTLM\n", + +#if defined(USE_KERBEROS) && !defined(OS_ANDROID) + // Choose Negotiate over NTLM on all platforms. + // TODO(ahendrickson): This may be flaky on Linux and OSX as it + // relies on being able to load one of the known .so files + // for gssapi. + HttpAuth::AUTH_SCHEME_NEGOTIATE, #else - // On systems that don't use Kerberos fall back to NTLM. - HttpAuth::AUTH_SCHEME_NTLM, + // On systems that don't use Kerberos fall back to NTLM. + HttpAuth::AUTH_SCHEME_NTLM, #endif // defined(USE_KERBEROS) - "", - } - }; + "", + }}; GURL origin("http://www.example.com"); std::set<HttpAuth::Scheme> disabled_schemes; MockAllowURLSecurityManager url_security_manager; diff --git a/net/net.gyp b/net/net.gyp index b231499..a997712 100644 --- a/net/net.gyp +++ b/net/net.gyp @@ -10,11 +10,11 @@ 'net_test_extra_libs': [], 'linux_link_kerberos%': 0, 'conditions': [ - ['chromeos==1 or embedded==1 or OS=="android" or OS=="ios"', { - # Disable Kerberos on ChromeOS, Android and iOS, at least for now. + ['chromeos==1 or embedded==1 or OS=="ios"', { + # Disable Kerberos on ChromeOS and iOS, at least for now. # It needs configuration (krb5.conf and so on). 'use_kerberos%': 0, - }, { # chromeos == 0 and embedded==0 and OS!="android" and OS!="ios" + }, { # chromeos == 0 and embedded==0 and OS!="ios" 'use_kerberos%': 1, }], ['OS=="android" and target_arch != "ia32"', { @@ -222,14 +222,21 @@ 'defines': [ 'USE_KERBEROS', ], - }, { # use_kerberos == 0 + }], + [ 'use_kerberos==0 or OS == "android"', { + # These are excluded on Android, because the actual Kerberos support, + # which these test, is in a separate app on Android. 'sources!': [ 'http/http_auth_gssapi_posix_unittest.cc', - 'http/http_auth_handler_negotiate_unittest.cc', 'http/mock_gssapi_library_posix.cc', 'http/mock_gssapi_library_posix.h', ], }], + [ 'use_kerberos==0', { + 'sources!': [ + 'http/http_auth_handler_negotiate_unittest.cc', + ], + }], [ 'use_openssl == 1 or (desktop_linux == 0 and chromeos == 0 and OS != "ios")', { # Only include this test when on Posix and using NSS for # cert verification or on iOS (which also uses NSS for certs). @@ -1355,6 +1362,7 @@ 'android/java/src/org/chromium/net/AndroidNetworkLibrary.java', 'android/java/src/org/chromium/net/AndroidPrivateKey.java', 'android/java/src/org/chromium/net/GURLUtils.java', + 'android/java/src/org/chromium/net/HttpNegotiateAuthenticator.java', 'android/java/src/org/chromium/net/NetStringUtil.java', 'android/java/src/org/chromium/net/NetworkChangeNotifier.java', 'android/java/src/org/chromium/net/ProxyChangeListener.java', @@ -1371,6 +1379,7 @@ 'sources': [ 'android/javatests/src/org/chromium/net/AndroidKeyStoreTestUtil.java', 'test/android/javatests/src/org/chromium/net/test/EmbeddedTestServer.java', + 'test/android/javatests/src/org/chromium/net/test/DummySpnegoAuthenticator.java', ], 'variables': { 'jni_gen_package': 'net/test', @@ -1418,6 +1427,7 @@ 'net_test_support', 'url_request_failed_job_java', '../base/base.gyp:base_java', + 'net_java', '<@(net_test_extra_libs)', ], 'includes': [ '../build/java.gypi' ], @@ -1493,6 +1503,7 @@ 'dependencies': [ 'net_java', 'net_javatests', + 'net_java_test_support', 'net_unittests', ], 'conditions': [ @@ -1514,6 +1525,8 @@ 'variables': { 'test_suite_name': 'net_unittests', 'isolate_file': 'net_unittests.isolate', + 'android_manifest_path': 'android/unittest_support/AndroidManifest.xml', + 'resource_dir': 'android/unittest_support/res', 'conditions': [ ['v8_use_external_startup_data==1', { 'asset_location': '<(PRODUCT_DIR)/net_unittests_apk/assets', @@ -1530,6 +1543,26 @@ }, 'includes': [ '../build/apk_test.gypi' ], }, + { + 'target_name': 'net_junit_tests', + 'type': 'none', + 'dependencies': [ + 'net_java', + '../base/base.gyp:base', + '../base/base.gyp:base_java_test_support', + '../testing/android/junit/junit_test.gyp:junit_test_support', + ], + 'variables': { + 'main_class': 'org.chromium.testing.local.JunitTestMain', + 'src_paths': [ + 'android/junit/', + ], + }, + 'includes': [ + '../build/host_jar.gypi', + ], + }, + ], }], ['OS == "android" or OS == "linux"', { diff --git a/net/net.gypi b/net/net.gypi index 19f5e90..630be11 100644 --- a/net/net.gypi +++ b/net/net.gypi @@ -184,6 +184,8 @@ 'android/cert_verify_result_android.h', 'android/gurl_utils.cc', 'android/gurl_utils.h', + 'android/http_auth_negotiate_android.cc', + 'android/http_auth_negotiate_android.h', 'android/keystore.cc', 'android/keystore.h', 'android/keystore_openssl.cc', @@ -657,6 +659,8 @@ 'http/http_auth_handler_ntlm.h', 'http/http_auth_handler_ntlm_portable.cc', 'http/http_auth_handler_ntlm_win.cc', + 'http/http_auth_multi_round_parse.cc', + 'http/http_auth_multi_round_parse.h', 'http/http_auth_sspi_win.cc', 'http/http_auth_sspi_win.h', 'http/http_basic_state.cc', @@ -1289,6 +1293,9 @@ 'extras/sqlite/sqlite_persistent_cookie_store.h', ], 'net_test_sources': [ + 'android/dummy_spnego_authenticator.cc', + 'android/dummy_spnego_authenticator.h', + 'android/http_auth_negotiate_android_unittest.cc', 'android/keystore_unittest.cc', 'android/network_change_notifier_android_unittest.cc', 'base/address_list_unittest.cc', @@ -1439,6 +1446,7 @@ 'http/http_auth_handler_mock.h', 'http/http_auth_handler_negotiate_unittest.cc', 'http/http_auth_handler_unittest.cc', + 'http/http_auth_multi_round_parse_unittest.cc', 'http/http_auth_sspi_win_unittest.cc', 'http/http_auth_unittest.cc', 'http/http_basic_state_unittest.cc', diff --git a/net/net_common.gypi b/net/net_common.gypi index 7a888a8..18d4f03 100644 --- a/net/net_common.gypi +++ b/net/net_common.gypi @@ -390,6 +390,8 @@ 'cert/cert_database_openssl.cc', 'cert/cert_verify_proc_openssl.cc', 'cert/test_root_certs_openssl.cc', + 'http/http_auth_gssapi_posix.cc', + 'http/http_auth_gssapi_posix.h', ], }, ], diff --git a/net/test/android/javatests/src/org/chromium/net/test/DummySpnegoAuthenticator.java b/net/test/android/javatests/src/org/chromium/net/test/DummySpnegoAuthenticator.java new file mode 100644 index 0000000..1c8b0c0 --- /dev/null +++ b/net/test/android/javatests/src/org/chromium/net/test/DummySpnegoAuthenticator.java @@ -0,0 +1,186 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.net.test; + +import android.accounts.AbstractAccountAuthenticator; +import android.accounts.Account; +import android.accounts.AccountAuthenticatorResponse; +import android.accounts.AccountManager; +import android.accounts.AuthenticatorException; +import android.accounts.NetworkErrorException; +import android.accounts.OperationCanceledException; +import android.app.Activity; +import android.content.Context; +import android.os.Bundle; + +import org.chromium.base.ApplicationStatus; +import org.chromium.base.CalledByNative; +import org.chromium.base.JNINamespace; +import org.chromium.base.NativeClassQualifiedName; +import org.chromium.net.HttpNegotiateConstants; + +import java.io.IOException; + +/** + * Dummy Android authenticator, to test SPNEGO/Keberos support on Android. This is deliberately + * minimal, and is not intended as an example of how to write a real SPNEGO Authenticator. + */ +@JNINamespace("net::android") +public class DummySpnegoAuthenticator extends AbstractAccountAuthenticator { + private static final String ACCOUNT_TYPE = "org.chromium.test.DummySpnegoAuthenticator"; + private static final String ACCOUNT_NAME = "DummySpnegoAccount"; + private static int sResult; + private static String sToken; + private static boolean sCheckArguments; + private static long sNativeDummySpnegoAuthenticator; + private static final int GSS_S_COMPLETE = 0; + private static final int GSS_S_CONTINUE_NEEDED = 1; + private static final int GSS_S_FAILURE = 2; + + /** + * @param context + */ + public DummySpnegoAuthenticator(Context context) { + super(context); + } + + @Override + public Bundle addAccount(AccountAuthenticatorResponse arg0, String accountType, String arg2, + String[] arg3, Bundle arg4) throws NetworkErrorException { + Bundle result = new Bundle(); + result.putInt(AccountManager.KEY_ERROR_CODE, AccountManager.ERROR_CODE_BAD_REQUEST); + result.putString(AccountManager.KEY_ERROR_MESSAGE, "Can't add new SPNEGO accounts"); + return result; + } + + @Override + public Bundle confirmCredentials(AccountAuthenticatorResponse arg0, Account arg1, Bundle arg2) + throws NetworkErrorException { + Bundle result = new Bundle(); + result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, true); + return result; + } + + @Override + public Bundle editProperties(AccountAuthenticatorResponse arg0, String arg1) { + return new Bundle(); + } + + @Override + public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, + String authTokenType, Bundle options) throws NetworkErrorException { + long nativeQuery = nativeGetNextQuery(sNativeDummySpnegoAuthenticator); + String incomingToken = options.getString(HttpNegotiateConstants.KEY_INCOMING_AUTH_TOKEN); + nativeCheckGetTokenArguments(nativeQuery, incomingToken); + Bundle result = new Bundle(); + result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name); + result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type); + result.putString(AccountManager.KEY_AUTHTOKEN, nativeGetTokenToReturn(nativeQuery)); + result.putInt(HttpNegotiateConstants.KEY_SPNEGO_RESULT, + decodeResult(nativeGetResult(nativeQuery))); + return result; + } + + /** + * @param nativeGetResult + * @return + */ + private int decodeResult(int gssApiResult) { + // This only handles the result values currently used in the tests. + switch (gssApiResult) { + case GSS_S_COMPLETE: + case GSS_S_CONTINUE_NEEDED: + return 0; + case GSS_S_FAILURE: + return HttpNegotiateConstants.ERR_MISSING_AUTH_CREDENTIALS; + default: + return HttpNegotiateConstants.ERR_UNEXPECTED; + } + } + + @Override + public String getAuthTokenLabel(String arg0) { + return "Spnego " + arg0; + } + + @Override + public Bundle hasFeatures(AccountAuthenticatorResponse arg0, Account arg1, String[] features) + throws NetworkErrorException { + Bundle result = new Bundle(); + for (String feature : features) { + if (!feature.equals(HttpNegotiateConstants.SPNEGO_FEATURE)) { + result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false); + return result; + } + } + result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, true); + return result; + } + + @Override + public Bundle updateCredentials(AccountAuthenticatorResponse arg0, Account arg1, String arg2, + Bundle arg3) throws NetworkErrorException { + Bundle result = new Bundle(); + result.putInt(AccountManager.KEY_ERROR_CODE, AccountManager.ERROR_CODE_BAD_REQUEST); + result.putString(AccountManager.KEY_ERROR_MESSAGE, "Can't add new SPNEGO accounts"); + return result; + } + + /** + * Called from tests, sets up the test account, if it doesn't already exist + */ + @CalledByNative + private static void ensureTestAccountExists() { + Activity activity = ApplicationStatus.getLastTrackedFocusedActivity(); + AccountManager am = AccountManager.get(activity); + Account account = new Account(ACCOUNT_NAME, ACCOUNT_TYPE); + am.addAccountExplicitly(account, null, null); + } + + /** + * Called from tests to tidy up test accounts. + */ + @SuppressWarnings("deprecation") + @CalledByNative + private static void removeTestAccounts() { + Activity activity = ApplicationStatus.getLastTrackedFocusedActivity(); + AccountManager am = AccountManager.get(activity); + String features[] = {HttpNegotiateConstants.SPNEGO_FEATURE}; + try { + Account accounts[] = + am.getAccountsByTypeAndFeatures(ACCOUNT_TYPE, features, null, null).getResult(); + for (Account account : accounts) { + // Deprecated, but the replacement not available on Android JB. + am.removeAccount(account, null, null).getResult(); + } + } catch (OperationCanceledException | AuthenticatorException | IOException e) { + // Should never happen. This is tidy-up after the tests. Ignore. + } + } + + @CalledByNative + private static void setNativeAuthenticator(long nativeDummySpnegoAuthenticator) { + sNativeDummySpnegoAuthenticator = nativeDummySpnegoAuthenticator; + } + + /** + * Send the relevant decoded arguments of getAuthToken to C++ for checking by googletest checks + * If the checks fail then the C++ unit test using this authenticator will fail. + * + * @param authTokenType + * @param spn + * @param incomingToken + */ + @NativeClassQualifiedName("DummySpnegoAuthenticator::SecurityContextQuery") + private native void nativeCheckGetTokenArguments(long nativeQuery, String incomingToken); + + @NativeClassQualifiedName("DummySpnegoAuthenticator::SecurityContextQuery") + private native String nativeGetTokenToReturn(long nativeQuery); + + @NativeClassQualifiedName("DummySpnegoAuthenticator::SecurityContextQuery") + private native int nativeGetResult(long nativeQuery); + + private native long nativeGetNextQuery(long nativeDummySpnegoAuthenticator); +} diff --git a/net/test/android/javatests/src/org/chromium/net/test/DummySpnegoAuthenticatorService.java b/net/test/android/javatests/src/org/chromium/net/test/DummySpnegoAuthenticatorService.java new file mode 100644 index 0000000..cc732a6 --- /dev/null +++ b/net/test/android/javatests/src/org/chromium/net/test/DummySpnegoAuthenticatorService.java @@ -0,0 +1,22 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.net.test; + +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; + +/** + * Authenticator service for testing SPNEGO (Kerberos) support. + */ +public class DummySpnegoAuthenticatorService extends Service { + private static DummySpnegoAuthenticator sAuthenticator = null; + + @Override + public IBinder onBind(Intent arg0) { + if (sAuthenticator == null) sAuthenticator = new DummySpnegoAuthenticator(this); + return sAuthenticator.getIBinder(); + } +} diff --git a/net/test/run_all_unittests.cc b/net/test/run_all_unittests.cc index 25016ca..7da7991 100644 --- a/net/test/run_all_unittests.cc +++ b/net/test/run_all_unittests.cc @@ -15,6 +15,8 @@ #include "base/android/jni_android.h" #include "base/android/jni_registrar.h" #include "base/test/test_file_util.h" +#include "base/test/test_ui_thread_android.h" +#include "net/android/dummy_spnego_authenticator.h" #include "net/android/net_jni_registrar.h" #include "url/android/url_jni_registrar.h" #endif @@ -32,9 +34,12 @@ int main(int argc, char** argv) { #if defined(OS_ANDROID) const base::android::RegistrationMethod kNetTestRegisteredMethods[] = { - {"NetAndroid", net::android::RegisterJni}, - {"TestFileUtil", base::RegisterContentUriTestUtils}, - {"UrlAndroid", url::android::RegisterJni}, + {"DummySpnegoAuthenticator", + net::android::DummySpnegoAuthenticator::RegisterJni}, + {"NetAndroid", net::android::RegisterJni}, + {"TestFileUtil", base::RegisterContentUriTestUtils}, + {"TestUiThreadAndroid", base::RegisterTestUiThreadAndroid}, + {"UrlAndroid", url::android::RegisterJni}, }; // Register JNI bindings for android. Doing it early as the test suite setup |
