diff options
author | Zeno Albisser <zeno.albisser@digia.com> | 2013-08-15 21:46:11 +0200 |
---|---|---|
committer | Zeno Albisser <zeno.albisser@digia.com> | 2013-08-15 21:46:11 +0200 |
commit | 679147eead574d186ebf3069647b4c23e8ccace6 (patch) | |
tree | fc247a0ac8ff119f7c8550879ebb6d3dd8d1ff69 /chromium/google_apis | |
download | qtwebengine-chromium-679147eead574d186ebf3069647b4c23e8ccace6.tar.gz |
Initial import.
Diffstat (limited to 'chromium/google_apis')
51 files changed, 9050 insertions, 0 deletions
diff --git a/chromium/google_apis/DEPS b/chromium/google_apis/DEPS new file mode 100644 index 00000000000..367bfb8ac5c --- /dev/null +++ b/chromium/google_apis/DEPS @@ -0,0 +1,6 @@ +include_rules = [ + "-chrome", + "-content", + "+crypto", + "+net", +] diff --git a/chromium/google_apis/OWNERS b/chromium/google_apis/OWNERS new file mode 100644 index 00000000000..4975363007f --- /dev/null +++ b/chromium/google_apis/OWNERS @@ -0,0 +1 @@ +joi@chromium.org diff --git a/chromium/google_apis/build/check_internal.py b/chromium/google_apis/build/check_internal.py new file mode 100755 index 00000000000..da0ddae8052 --- /dev/null +++ b/chromium/google_apis/build/check_internal.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +# Copyright (c) 2012 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. + +"""google_api's auto-internal gyp integration. + +Takes one argument, a path. Prints 1 if the path exists, 0 if not. +""" + + +import os +import sys + + +if __name__ == '__main__': + if os.path.exists(sys.argv[1]): + print 1 + else: + print 0 diff --git a/chromium/google_apis/cup/OWNERS b/chromium/google_apis/cup/OWNERS new file mode 100644 index 00000000000..00f97260494 --- /dev/null +++ b/chromium/google_apis/cup/OWNERS @@ -0,0 +1 @@ +ryanmyers@chromium.org diff --git a/chromium/google_apis/cup/client_update_protocol.cc b/chromium/google_apis/cup/client_update_protocol.cc new file mode 100644 index 00000000000..e730557164f --- /dev/null +++ b/chromium/google_apis/cup/client_update_protocol.cc @@ -0,0 +1,305 @@ +// Copyright 2013 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 "google_apis/cup/client_update_protocol.h" + +#include "base/base64.h" +#include "base/logging.h" +#include "base/memory/scoped_ptr.h" +#include "base/sha1.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "crypto/hmac.h" +#include "crypto/random.h" + +namespace { + +base::StringPiece ByteVectorToSP(const std::vector<uint8>& vec) { + if (vec.empty()) + return base::StringPiece(); + + return base::StringPiece(reinterpret_cast<const char*>(&vec[0]), vec.size()); +} + +// This class needs to implement the same hashing and signing functions as the +// Google Update server; for now, this is SHA-1 and HMAC-SHA1, but this may +// change to SHA-256 in the near future. For this reason, all primitives are +// wrapped. The name "SymSign" is used to mirror the CUP specification. +size_t HashDigestSize() { + return base::kSHA1Length; +} + +std::vector<uint8> Hash(const std::vector<uint8>& data) { + std::vector<uint8> result(HashDigestSize()); + base::SHA1HashBytes(data.empty() ? NULL : &data[0], + data.size(), + &result[0]); + return result; +} + +std::vector<uint8> Hash(const base::StringPiece& sdata) { + std::vector<uint8> result(HashDigestSize()); + base::SHA1HashBytes(sdata.empty() ? + NULL : + reinterpret_cast<const unsigned char*>(sdata.data()), + sdata.length(), + &result[0]); + return result; +} + +std::vector<uint8> SymConcat(uint8 id, + const std::vector<uint8>* h1, + const std::vector<uint8>* h2, + const std::vector<uint8>* h3) { + std::vector<uint8> result; + result.push_back(id); + const std::vector<uint8>* args[] = { h1, h2, h3 }; + for (size_t i = 0; i != arraysize(args); ++i) { + if (args[i]) { + DCHECK_EQ(args[i]->size(), HashDigestSize()); + result.insert(result.end(), args[i]->begin(), args[i]->end()); + } + } + + return result; +} + +std::vector<uint8> SymSign(const std::vector<uint8>& key, + const std::vector<uint8>& hashes) { + DCHECK(!key.empty()); + DCHECK(!hashes.empty()); + + crypto::HMAC hmac(crypto::HMAC::SHA1); + if (!hmac.Init(&key[0], key.size())) + return std::vector<uint8>(); + + std::vector<uint8> result(hmac.DigestLength()); + if (!hmac.Sign(ByteVectorToSP(hashes), &result[0], result.size())) + return std::vector<uint8>(); + + return result; +} + +bool SymSignVerify(const std::vector<uint8>& key, + const std::vector<uint8>& hashes, + const std::vector<uint8>& server_proof) { + DCHECK(!key.empty()); + DCHECK(!hashes.empty()); + DCHECK(!server_proof.empty()); + + crypto::HMAC hmac(crypto::HMAC::SHA1); + if (!hmac.Init(&key[0], key.size())) + return false; + + return hmac.Verify(ByteVectorToSP(hashes), ByteVectorToSP(server_proof)); +} + +// RsaPad() is implemented as described in the CUP spec. It is NOT a general +// purpose padding algorithm. +std::vector<uint8> RsaPad(size_t rsa_key_size, + const std::vector<uint8>& entropy) { + DCHECK_GE(rsa_key_size, HashDigestSize()); + + // The result gets padded with zeros if the result size is greater than + // the size of the buffer provided by the caller. + std::vector<uint8> result(entropy); + result.resize(rsa_key_size - HashDigestSize()); + + // For use with RSA, the input needs to be smaller than the RSA modulus, + // which has always the msb set. + result[0] &= 127; // Reset msb + result[0] |= 64; // Set second highest bit. + + std::vector<uint8> digest = Hash(result); + result.insert(result.end(), digest.begin(), digest.end()); + DCHECK_EQ(result.size(), rsa_key_size); + return result; +} + +// CUP passes the versioned secret in the query portion of the URL for the +// update check service -- and that means that a URL-safe variant of Base64 is +// needed. Call the standard Base64 encoder/decoder and then apply fixups. +std::string UrlSafeB64Encode(const std::vector<uint8>& data) { + std::string result; + if (!base::Base64Encode(ByteVectorToSP(data), &result)) + return std::string(); + + // Do an tr|+/|-_| on the output, and strip any '=' padding. + for (std::string::iterator it = result.begin(); it != result.end(); ++it) { + switch (*it) { + case '+': + *it = '-'; + break; + case '/': + *it = '_'; + break; + default: + break; + } + } + TrimString(result, "=", &result); + + return result; +} + +std::vector<uint8> UrlSafeB64Decode(const base::StringPiece& input) { + std::string unsafe(input.begin(), input.end()); + for (std::string::iterator it = unsafe.begin(); it != unsafe.end(); ++it) { + switch (*it) { + case '-': + *it = '+'; + break; + case '_': + *it = '/'; + break; + default: + break; + } + } + if (unsafe.length() % 4) + unsafe.append(4 - (unsafe.length() % 4), '='); + + std::string decoded; + if (!base::Base64Decode(unsafe, &decoded)) + return std::vector<uint8>(); + + return std::vector<uint8>(decoded.begin(), decoded.end()); +} + +} // end namespace + +ClientUpdateProtocol::ClientUpdateProtocol(int key_version) + : pub_key_version_(key_version) { +} + +scoped_ptr<ClientUpdateProtocol> ClientUpdateProtocol::Create( + int key_version, + const base::StringPiece& public_key) { + DCHECK_GT(key_version, 0); + DCHECK(!public_key.empty()); + + scoped_ptr<ClientUpdateProtocol> result( + new ClientUpdateProtocol(key_version)); + if (!result) + return scoped_ptr<ClientUpdateProtocol>(); + + if (!result->LoadPublicKey(public_key)) + return scoped_ptr<ClientUpdateProtocol>(); + + if (!result->BuildRandomSharedKey()) + return scoped_ptr<ClientUpdateProtocol>(); + + return result.Pass(); +} + +std::string ClientUpdateProtocol::GetVersionedSecret() const { + return base::StringPrintf("%d:%s", + pub_key_version_, + UrlSafeB64Encode(encrypted_key_source_).c_str()); +} + +bool ClientUpdateProtocol::SignRequest(const base::StringPiece& url, + const base::StringPiece& request_body, + std::string* client_proof) { + DCHECK(!encrypted_key_source_.empty()); + DCHECK(!url.empty()); + DCHECK(!request_body.empty()); + DCHECK(client_proof); + + // Compute the challenge hash: + // hw = HASH(HASH(v|w)|HASH(request_url)|HASH(body)). + // Keep the challenge hash for later to validate the server's response. + std::vector<uint8> internal_hashes; + + std::vector<uint8> h; + h = Hash(GetVersionedSecret()); + internal_hashes.insert(internal_hashes.end(), h.begin(), h.end()); + h = Hash(url); + internal_hashes.insert(internal_hashes.end(), h.begin(), h.end()); + h = Hash(request_body); + internal_hashes.insert(internal_hashes.end(), h.begin(), h.end()); + DCHECK_EQ(internal_hashes.size(), 3 * HashDigestSize()); + + client_challenge_hash_ = Hash(internal_hashes); + + // Sign the challenge hash (hw) using the shared key (sk) to produce the + // client proof (cp). + std::vector<uint8> raw_client_proof = + SymSign(shared_key_, SymConcat(3, &client_challenge_hash_, NULL, NULL)); + if (raw_client_proof.empty()) { + client_challenge_hash_.clear(); + return false; + } + + *client_proof = UrlSafeB64Encode(raw_client_proof); + return true; +} + +bool ClientUpdateProtocol::ValidateResponse( + const base::StringPiece& response_body, + const base::StringPiece& server_cookie, + const base::StringPiece& server_proof) { + DCHECK(!client_challenge_hash_.empty()); + + if (response_body.empty() || server_cookie.empty() || server_proof.empty()) + return false; + + // Decode the server proof from URL-safe Base64 to a binary HMAC for the + // response. + std::vector<uint8> sp_decoded = UrlSafeB64Decode(server_proof); + if (sp_decoded.empty()) + return false; + + // If the request was received by the server, the server will use its + // private key to decrypt |w_|, yielding the original contents of |r_|. + // The server can then recreate |sk_|, compute |hw_|, and SymSign(3|hw) + // to ensure that the cp matches the contents. It will then use |sk_| + // to sign its response, producing the server proof |sp|. + std::vector<uint8> hm = Hash(response_body); + std::vector<uint8> hc = Hash(server_cookie); + return SymSignVerify(shared_key_, + SymConcat(1, &client_challenge_hash_, &hm, &hc), + sp_decoded); +} + +bool ClientUpdateProtocol::BuildRandomSharedKey() { + DCHECK_GE(PublicKeyLength(), HashDigestSize()); + + // Start by generating some random bytes that are suitable to be encrypted; + // this will be the source of the shared HMAC key that client and server use. + // (CUP specification calls this "r".) + std::vector<uint8> key_source; + std::vector<uint8> entropy(PublicKeyLength() - HashDigestSize()); + crypto::RandBytes(&entropy[0], entropy.size()); + key_source = RsaPad(PublicKeyLength(), entropy); + + return DeriveSharedKey(key_source); +} + +bool ClientUpdateProtocol::SetSharedKeyForTesting( + const base::StringPiece& key_source) { + DCHECK_EQ(key_source.length(), PublicKeyLength()); + + return DeriveSharedKey(std::vector<uint8>(key_source.begin(), + key_source.end())); +} + +bool ClientUpdateProtocol::DeriveSharedKey(const std::vector<uint8>& source) { + DCHECK(!source.empty()); + DCHECK_GE(source.size(), HashDigestSize()); + DCHECK_EQ(source.size(), PublicKeyLength()); + + // Hash the key source (r) to generate a new shared HMAC key (sk'). + shared_key_ = Hash(source); + + // Encrypt the key source (r) using the public key (pk[v]) to generate the + // encrypted key source (w). + if (!EncryptKeySource(source)) + return false; + if (encrypted_key_source_.size() != PublicKeyLength()) + return false; + + return true; +} + diff --git a/chromium/google_apis/cup/client_update_protocol.h b/chromium/google_apis/cup/client_update_protocol.h new file mode 100644 index 00000000000..786c01f42b1 --- /dev/null +++ b/chromium/google_apis/cup/client_update_protocol.h @@ -0,0 +1,138 @@ +// Copyright 2013 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 GOOGLE_APIS_CUP_CLIENT_UPDATE_PROTOCOL_H_ +#define GOOGLE_APIS_CUP_CLIENT_UPDATE_PROTOCOL_H_ + +#include <string> +#include <vector> + +#include "base/basictypes.h" +#include "base/memory/scoped_ptr.h" +#include "base/strings/string_piece.h" + +// Forward declare types for NSS. +#if defined(USE_NSS) || defined(OS_WIN) || defined(OS_MACOSX) +typedef struct SECKEYPublicKeyStr SECKEYPublicKey; +#endif + +// Client Update Protocol, or CUP, is used by Google Update (Omaha) servers to +// ensure freshness and authenticity of update checks over HTTP, without the +// overhead of HTTPS -- namely, no PKI, no guarantee of privacy, and no request +// replay protection (since update checks are idempotent). +// +// http://omaha.googlecode.com/svn/wiki/cup.html +// +// Each ClientUpdateProtocol represents a single update check in flight -- a +// call to SignRequest() generates internal state used by ValidateResponse(). +// +// This implementation does not persist client proofs by design. +class ClientUpdateProtocol { + public: + ~ClientUpdateProtocol(); + + // Initializes this instance of CUP with a versioned public key. |key_version| + // must be non-negative. |public_key| is expected to be a DER-encoded ASN.1 + // Subject Public Key Info. Returns a NULL pointer on failure. + static scoped_ptr<ClientUpdateProtocol> Create( + int key_version, const base::StringPiece& public_key); + + // Returns a versioned encrypted secret (v|w) in a URL-safe Base64 encoding. + // Add to your URL before calling SignRequest(). + std::string GetVersionedSecret() const; + + // Generates freshness/authentication data for an outgoing update check. + // |url| contains the the URL that the request will be sent to; it should + // include GetVersionedSecret() in its query string. This needs to be + // formatted in the way that the Omaha server expects: omit the scheme and + // any port number. (e.g. "//tools.google.com/service/update2?w=1:abcdef") + // |request_body| contains the body of the update check request in UTF-8. + // + // On success, returns true, and |client_proof| receives a Base64-encoded + // client proof, which should be sent in the If-Match HTTP header. On + // failure, returns false, and |client_proof| is not modified. + // + // This method will store internal state in this instance used by calls to + // ValidateResponse(); if you need to have multiple update checks in flight, + // initialize a separate CUP instance for each one. + bool SignRequest(const base::StringPiece& url, + const base::StringPiece& request_body, + std::string* client_proof); + + // Validates a response given to a update check request previously signed + // with SignRequest(). |request_body| contains the body of the response in + // UTF-8. |server_cookie| contains the persisted credential cookie provided + // by the server. |server_proof| contains the Base64-encoded server proof, + // which is passed in the ETag HTTP header. Returns true if the response is + // valid. + // This method uses internal state that is set by a prior SignRequest() call. + bool ValidateResponse(const base::StringPiece& response_body, + const base::StringPiece& server_cookie, + const base::StringPiece& server_proof); + + private: + friend class CupTest; + + explicit ClientUpdateProtocol(int key_version); + + // Decodes |public_key| into the appropriate internal structures. Returns + // the length of the public key (modulus) in bytes, or 0 on failure. + bool LoadPublicKey(const base::StringPiece& public_key); + + // Returns the size of the public key in bytes, or 0 on failure. + size_t PublicKeyLength(); + + // Helper function for BuildSharedKey() -- encrypts |key_source| (r) using + // the loaded public key, filling out |encrypted_key_source_| (w). + // Returns true on success. + bool EncryptKeySource(const std::vector<uint8>& key_source); + + // Generates a random key source and passes it to DeriveSharedKey(). + // Returns true on success. + bool BuildRandomSharedKey(); + + // Sets a fixed key source from a character string and passes it to + // DeriveSharedKey(). Used for unit testing only. Returns true on success. + bool SetSharedKeyForTesting(const base::StringPiece& fixed_key_source); + + // Given a key source (r), derives the values of |shared_key_| (sk') and + // encrypted_key_source_ (w). Returns true on success. + bool DeriveSharedKey(const std::vector<uint8>& source); + + // The server keeps multiple private keys; a version must be sent so that + // the right private key is used to decode the versioned secret. (The CUP + // specification calls this "v".) + int pub_key_version_; + + // Holds the shared key, which is used to generate an HMAC signature for both + // the update check request and the update response. The client builds it + // locally, but sends the server an encrypted copy of the key source to + // synthesize it on its own. (The CUP specification calls this "sk'".) + std::vector<uint8> shared_key_; + + // Holds the original contents of key_source_ that have been encrypted with + // the server's public key. The client sends this, along with the version of + // the keypair that was used, to the server. The server decrypts it using its + // private key to get the contents of key_source_, from which it recreates the + // shared key. (The CUP specification calls this "w".) + std::vector<uint8> encrypted_key_source_; + + // Holds the hash of the update check request, the URL that it was sent to, + // and the versioned secret. This is filled out by a successful call to + // SignRequest(), and used by ValidateResponse() to confirm that the server + // has successfully decoded the versioned secret and signed the response using + // the same shared key as our own. (The CUP specification calls this "hw".) + std::vector<uint8> client_challenge_hash_; + + // The public key used to encrypt the key source. (The CUP specification + // calls this "pk[v]".) +#if defined(USE_NSS) || defined(OS_WIN) || defined(OS_MACOSX) + SECKEYPublicKey* public_key_; +#endif + + DISALLOW_IMPLICIT_CONSTRUCTORS(ClientUpdateProtocol); +}; + +#endif // GOOGLE_APIS_CUP_CLIENT_UPDATE_PROTOCOL_H_ + diff --git a/chromium/google_apis/cup/client_update_protocol_nss.cc b/chromium/google_apis/cup/client_update_protocol_nss.cc new file mode 100644 index 00000000000..b369c13d154 --- /dev/null +++ b/chromium/google_apis/cup/client_update_protocol_nss.cc @@ -0,0 +1,81 @@ +// Copyright 2013 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 "google_apis/cup/client_update_protocol.h" + +#include <keyhi.h> +#include <pk11pub.h> +#include <seccomon.h> + +#include "base/logging.h" +#include "crypto/nss_util.h" +#include "crypto/scoped_nss_types.h" + +typedef scoped_ptr_malloc< + CERTSubjectPublicKeyInfo, + crypto::NSSDestroyer<CERTSubjectPublicKeyInfo, + SECKEY_DestroySubjectPublicKeyInfo> > + ScopedCERTSubjectPublicKeyInfo; + +ClientUpdateProtocol::~ClientUpdateProtocol() { + if (public_key_) + SECKEY_DestroyPublicKey(public_key_); +} + +bool ClientUpdateProtocol::LoadPublicKey(const base::StringPiece& public_key) { + crypto::EnsureNSSInit(); + + // The binary blob |public_key| is expected to be a DER-encoded ASN.1 + // Subject Public Key Info. + SECItem spki_item; + spki_item.type = siBuffer; + spki_item.data = + reinterpret_cast<unsigned char*>(const_cast<char*>(public_key.data())); + spki_item.len = static_cast<unsigned int>(public_key.size()); + + ScopedCERTSubjectPublicKeyInfo spki( + SECKEY_DecodeDERSubjectPublicKeyInfo(&spki_item)); + if (!spki.get()) + return false; + + public_key_ = SECKEY_ExtractPublicKey(spki.get()); + if (!public_key_) + return false; + + if (!PublicKeyLength()) + return false; + + return true; +} + +size_t ClientUpdateProtocol::PublicKeyLength() { + if (!public_key_) + return 0; + + return SECKEY_PublicKeyStrength(public_key_); +} + +bool ClientUpdateProtocol::EncryptKeySource( + const std::vector<uint8>& key_source) { + // WARNING: This call bypasses the usual PKCS #1 padding and does direct RSA + // exponentiation. This is not secure without taking measures to ensure that + // the contents of r are suitable. This is done to remain compatible with + // the implementation on the Google Update servers; don't copy-paste this + // code arbitrarily and expect it to work and/or remain secure! + if (!public_key_) + return false; + + size_t keysize = SECKEY_PublicKeyStrength(public_key_); + if (key_source.size() != keysize) + return false; + + encrypted_key_source_.resize(keysize); + return SECSuccess == PK11_PubEncryptRaw( + public_key_, + &encrypted_key_source_[0], + const_cast<unsigned char*>(&key_source[0]), + key_source.size(), + NULL); +} + diff --git a/chromium/google_apis/cup/client_update_protocol_openssl.cc b/chromium/google_apis/cup/client_update_protocol_openssl.cc new file mode 100644 index 00000000000..74f99ec145c --- /dev/null +++ b/chromium/google_apis/cup/client_update_protocol_openssl.cc @@ -0,0 +1,27 @@ +// Copyright 2013 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 "google_apis/cup/client_update_protocol.h" + +#include "base/logging.h" + +ClientUpdateProtocol::~ClientUpdateProtocol() {} + +bool ClientUpdateProtocol::LoadPublicKey(const base::StringPiece& public_key) { + NOTIMPLEMENTED(); + return false; +} + +size_t ClientUpdateProtocol::PublicKeyLength() { + NOTIMPLEMENTED(); + return 0; +} + +bool ClientUpdateProtocol::EncryptKeySource( + const std::vector<uint8>& key_source) { + NOTIMPLEMENTED(); + return false; +} + + diff --git a/chromium/google_apis/cup/client_update_protocol_unittest.cc b/chromium/google_apis/cup/client_update_protocol_unittest.cc new file mode 100644 index 00000000000..6a2abedce44 --- /dev/null +++ b/chromium/google_apis/cup/client_update_protocol_unittest.cc @@ -0,0 +1,165 @@ +// Copyright 2013 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 <limits> +#include <vector> + +#include "base/base64.h" +#include "base/memory/scoped_ptr.h" +#include "crypto/random.h" +#include "crypto/secure_util.h" +#include "google_apis/cup/client_update_protocol.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +std::string GetPublicKeyForTesting() { + // How to generate this key: + // openssl genpkey -out cr.pem -outform PEM -algorithm RSA + // -pkeyopt rsa_keygen_pubexp:3 + // openssl rsa -in cr.pem -pubout -out cr_pub.pem + + static const char kCupTestKey1024_Base64[] = + "MIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQC7ct1JhLSol2DkBcJdNjR3KkEA" + "ZfXpF22lDD2WZu5JAZ4NiZqnHsKGJNPUbCH4AhFsXmuW5wEHhUVNhsMP6F9mQ06D" + "i+ygwZ8aXlklmW4S0Et+SNg3i73fnYn0KDQzrzJnMu46s/CFPhjr4f0TH9b7oHkU" + "XbqNZtG6gwaN1bmzFwIBAw=="; + + std::string result; + if (!base::Base64Decode(std::string(kCupTestKey1024_Base64), &result)) + return ""; + + return result; +} + +} // end namespace + +#if defined(USE_OPENSSL) + +// Once CUP is implemented for OpenSSL, remove this #if block. +TEST(CupTest, OpenSSLStub) { + scoped_ptr<ClientUpdateProtocol> cup = + ClientUpdateProtocol::Create(8, GetPublicKeyForTesting()); + ASSERT_FALSE(cup.get()); +} + +#else + +class CupTest : public testing::Test { + protected: + virtual void SetUp() { + cup_ = ClientUpdateProtocol::Create(8, GetPublicKeyForTesting()); + ASSERT_TRUE(cup_.get()); + } + + void OverrideRAndRebuildKeys() { + // This must be the same length as the modulus of the public key being + // used in the unit test. + static const size_t kPublicKeyLength = 1024 / 8; + static const char kFixedR[] = + "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do " + "eiusmod tempor incididunt ut labore et dolore magna aliqua. "; + + ASSERT_EQ(kPublicKeyLength, strlen(kFixedR)); + ASSERT_TRUE(cup_->SetSharedKeyForTesting(kFixedR)); + } + + ClientUpdateProtocol& CUP() { + return *cup_.get(); + } + + private: + scoped_ptr<ClientUpdateProtocol> cup_; +}; + +TEST_F(CupTest, GetVersionedSecret) { + // Given a fixed public key set in the test fixture, if the key source |r| + // is filled with known data, |w| can be tested against an expected output, + // and the signing/verification for an given request becomes fixed. + // + // The expected output can be generated using this command line, where + // plaintext.bin is the contents of kFixedR[] in OverrideRAndRebuildKeys(): + // + // openssl rsautl -inkey cr2_pub.pem -pubin -encrypt -raw + // -in plaintext.bin | base64 + // + // Remember to prepend the key version number, and fix up the Base64 + // afterwards to be URL-safe. + + static const char kExpectedVW[] = + "8:lMmNR3mVbOitbq8ceYGStFBwrJcpvY-sauFSbMVe6VONS9x42xTOLY_KdqsWCy" + "KuiJBiQziQLOybPUyA9vk0N5kMnC90LIh2nP2FgFG0M0Z22qjB3drsdJPi7TQZbb" + "Xhqm587M8vjc6VlM_eoC0qYwCPaXBqXjsyiHnXetcn5X0"; + + EXPECT_NE(kExpectedVW, CUP().GetVersionedSecret()); + OverrideRAndRebuildKeys(); + EXPECT_EQ(kExpectedVW, CUP().GetVersionedSecret()); +} + +TEST_F(CupTest, SignRequest) { + static const char kUrl[] = "//testserver.chromium.org/update"; + static const char kUrlQuery[] = "?junk=present"; + static const char kRequest[] = "testbody"; + + static const char kExpectedCP[] = "tfjmVMDAbU0-Kye4PjrCuyQIDCU"; + + OverrideRAndRebuildKeys(); + + // Check the case with no query string other than v|w. + std::string url(kUrl); + url.append("?w="); + url.append(CUP().GetVersionedSecret()); + + std::string cp; + ASSERT_TRUE(CUP().SignRequest(url, kRequest, &cp)); + + // Check the case with a pre-existing query string. + std::string url2(kUrl); + url2.append(kUrlQuery); + url2.append("&w="); + url2.append(CUP().GetVersionedSecret()); + + std::string cp2; + ASSERT_TRUE(CUP().SignRequest(url2, kRequest, &cp2)); + + // Changes in the URL should result in changes in the client proof. + EXPECT_EQ(kExpectedCP, cp2); + EXPECT_NE(cp, cp2); +} + +TEST_F(CupTest, ValidateResponse) { + static const char kUrl[] = "//testserver.chromium.orgupdate?junk=present&w="; + static const char kRequest[] = "testbody"; + + static const char kGoodResponse[] = "intact_response"; + static const char kGoodC[] = "c=EncryptedDataFromTheUpdateServer"; + static const char kGoodSP[] = "5rMFMPL9Hgqb-2J8kL3scsHeNgg"; + + static const char kBadResponse[] = "tampered_response"; + static const char kBadC[] = "c=TotalJunkThatAnAttackerCouldSend"; + static const char kBadSP[] = "Base64TamperedShaOneHash"; + + OverrideRAndRebuildKeys(); + + std::string url(kUrl); + url.append(CUP().GetVersionedSecret()); + + std::string client_proof; + ASSERT_TRUE(CUP().SignRequest(url, kRequest, &client_proof)); + + // Return true on a valid response and server proof. + EXPECT_TRUE(CUP().ValidateResponse(kGoodResponse, kGoodC, kGoodSP)); + + // Return false on anything invalid. + EXPECT_FALSE(CUP().ValidateResponse(kBadResponse, kGoodC, kGoodSP)); + EXPECT_FALSE(CUP().ValidateResponse(kGoodResponse, kBadC, kGoodSP)); + EXPECT_FALSE(CUP().ValidateResponse(kGoodResponse, kGoodC, kBadSP)); + EXPECT_FALSE(CUP().ValidateResponse(kGoodResponse, kBadC, kBadSP)); + EXPECT_FALSE(CUP().ValidateResponse(kBadResponse, kGoodC, kBadSP)); + EXPECT_FALSE(CUP().ValidateResponse(kBadResponse, kBadC, kGoodSP)); + EXPECT_FALSE(CUP().ValidateResponse(kBadResponse, kBadC, kBadSP)); +} + +#endif // !defined(USE_OPENSSL) + diff --git a/chromium/google_apis/determine_use_official_keys.gypi b/chromium/google_apis/determine_use_official_keys.gypi new file mode 100644 index 00000000000..d9f11164af7 --- /dev/null +++ b/chromium/google_apis/determine_use_official_keys.gypi @@ -0,0 +1,37 @@ +# Copyright (c) 2013 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 this .gypi in your target to dynamically set the +# use_official_google_api_keys variable (unless it is already +# explicitly set) and the associated preprocessor define. + +{ + 'variables': { + 'variables': { + # See documentation of this variable in //build/common.gypi. + 'use_official_google_api_keys%': 2, + }, + + # Copy conditionally-set variables out one scope. + 'use_official_google_api_keys%': '<(use_official_google_api_keys)', + + 'conditions': [ + # If use_official_google_api_keys is already set (to 0 or 1), we + # do none of the implicit checking. If it is set to 1 and the + # internal keys file is missing, the build will fail at compile + # time. If it is set to 0 and keys are not provided by other + # means, a warning will be printed at compile time. + ['use_official_google_api_keys==2', { + 'use_official_google_api_keys%': + '<!(python <(DEPTH)/google_apis/build/check_internal.py <(DEPTH)/google_apis/internal/google_chrome_api_keys.h)', + }], + ] + }, + + 'conditions': [ + ['use_official_google_api_keys==1', { + 'defines': ['USE_OFFICIAL_GOOGLE_API_KEYS=1'], + }], + ], +} diff --git a/chromium/google_apis/gaia/DEPS b/chromium/google_apis/gaia/DEPS new file mode 100644 index 00000000000..f445dee28bf --- /dev/null +++ b/chromium/google_apis/gaia/DEPS @@ -0,0 +1,5 @@ +specific_include_rules = { + ".*_[a-z]*test\.cc": [ + "+content/public/test/test_browser_thread_bundle.h", + ] +} diff --git a/chromium/google_apis/gaia/OWNERS b/chromium/google_apis/gaia/OWNERS new file mode 100644 index 00000000000..654a183990f --- /dev/null +++ b/chromium/google_apis/gaia/OWNERS @@ -0,0 +1,3 @@ +rogerta@chromium.org +tim@chromium.org +zelidrag@chromium.org diff --git a/chromium/google_apis/gaia/gaia_auth_consumer.cc b/chromium/google_apis/gaia/gaia_auth_consumer.cc new file mode 100644 index 00000000000..860b66ec85f --- /dev/null +++ b/chromium/google_apis/gaia/gaia_auth_consumer.cc @@ -0,0 +1,51 @@ +// Copyright (c) 2012 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 "google_apis/gaia/gaia_auth_consumer.h" + +GaiaAuthConsumer::ClientLoginResult::ClientLoginResult() + : two_factor(false) { +} + +GaiaAuthConsumer::ClientLoginResult::ClientLoginResult( + const std::string& new_sid, + const std::string& new_lsid, + const std::string& new_token, + const std::string& new_data) + : sid(new_sid), + lsid(new_lsid), + token(new_token), + data(new_data), + two_factor(false) {} + +GaiaAuthConsumer::ClientLoginResult::~ClientLoginResult() {} + +bool GaiaAuthConsumer::ClientLoginResult::operator==( + const ClientLoginResult &b) const { + return sid == b.sid && + lsid == b.lsid && + token == b.token && + data == b.data && + two_factor == b.two_factor; +} + +GaiaAuthConsumer::ClientOAuthResult::ClientOAuthResult() + : expires_in_secs(0) {} + +GaiaAuthConsumer::ClientOAuthResult::ClientOAuthResult( + const std::string& new_refresh_token, + const std::string& new_access_token, + int new_expires_in_secs) + : refresh_token(new_refresh_token), + access_token(new_access_token), + expires_in_secs(new_expires_in_secs) {} + +GaiaAuthConsumer::ClientOAuthResult::~ClientOAuthResult() {} + +bool GaiaAuthConsumer::ClientOAuthResult::operator==( + const ClientOAuthResult &b) const { + return refresh_token == b.refresh_token && + access_token == b.access_token && + expires_in_secs == b.expires_in_secs; +} diff --git a/chromium/google_apis/gaia/gaia_auth_consumer.h b/chromium/google_apis/gaia/gaia_auth_consumer.h new file mode 100644 index 00000000000..403033622b2 --- /dev/null +++ b/chromium/google_apis/gaia/gaia_auth_consumer.h @@ -0,0 +1,87 @@ +// Copyright (c) 2012 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 GOOGLE_APIS_GAIA_GAIA_AUTH_CONSUMER_H_ +#define GOOGLE_APIS_GAIA_GAIA_AUTH_CONSUMER_H_ + +#include <map> +#include <string> +#include <vector> + +class GoogleServiceAuthError; + +namespace net { +typedef std::vector<std::string> ResponseCookies; +} + +typedef std::map<std::string, std::string> UserInfoMap; + +// An interface that defines the callbacks for objects that +// GaiaAuthFetcher can return data to. +class GaiaAuthConsumer { + public: + struct ClientLoginResult { + ClientLoginResult(); + ClientLoginResult(const std::string& new_sid, + const std::string& new_lsid, + const std::string& new_token, + const std::string& new_data); + ~ClientLoginResult(); + + bool operator==(const ClientLoginResult &b) const; + + std::string sid; + std::string lsid; + std::string token; + // TODO(chron): Remove the data field later. Don't use it if possible. + std::string data; // Full contents of ClientLogin return. + bool two_factor; // set to true if there was a TWO_FACTOR "failure". + }; + + struct ClientOAuthResult { + ClientOAuthResult(); + ClientOAuthResult(const std::string& new_refresh_token, + const std::string& new_access_token, + int new_expires_in_secs); + ~ClientOAuthResult(); + + bool operator==(const ClientOAuthResult &b) const; + + // OAuth2 refresh token. Used to mint new access tokens when needed. + std::string refresh_token; + + // OAuth2 access token. Token to pass to endpoints that require oauth2 + // authentication. + std::string access_token; + + // The lifespan of |access_token| in seconds. + int expires_in_secs; + }; + + virtual ~GaiaAuthConsumer() {} + + virtual void OnClientLoginSuccess(const ClientLoginResult& result) {} + virtual void OnClientLoginFailure(const GoogleServiceAuthError& error) {} + + virtual void OnIssueAuthTokenSuccess(const std::string& service, + const std::string& auth_token) {} + virtual void OnIssueAuthTokenFailure(const std::string& service, + const GoogleServiceAuthError& error) {} + + virtual void OnClientOAuthSuccess(const ClientOAuthResult& result) {} + virtual void OnClientOAuthFailure(const GoogleServiceAuthError& error) {} + + virtual void OnOAuth2RevokeTokenCompleted() {} + + virtual void OnGetUserInfoSuccess(const UserInfoMap& data) {} + virtual void OnGetUserInfoFailure(const GoogleServiceAuthError& error) {} + + virtual void OnUberAuthTokenSuccess(const std::string& token) {} + virtual void OnUberAuthTokenFailure(const GoogleServiceAuthError& error) {} + + virtual void OnMergeSessionSuccess(const std::string& data) {} + virtual void OnMergeSessionFailure(const GoogleServiceAuthError& error) {} +}; + +#endif // GOOGLE_APIS_GAIA_GAIA_AUTH_CONSUMER_H_ diff --git a/chromium/google_apis/gaia/gaia_auth_fetcher.cc b/chromium/google_apis/gaia/gaia_auth_fetcher.cc new file mode 100644 index 00000000000..67756deb99b --- /dev/null +++ b/chromium/google_apis/gaia/gaia_auth_fetcher.cc @@ -0,0 +1,949 @@ +// Copyright (c) 2012 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 "google_apis/gaia/gaia_auth_fetcher.h" + +#include <algorithm> +#include <string> +#include <utility> +#include <vector> + +#include "base/json/json_reader.h" +#include "base/json/json_writer.h" +#include "base/strings/string_split.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "base/values.h" +#include "google_apis/gaia/gaia_auth_consumer.h" +#include "google_apis/gaia/gaia_constants.h" +#include "google_apis/gaia/gaia_urls.h" +#include "google_apis/gaia/google_service_auth_error.h" +#include "net/base/escape.h" +#include "net/base/load_flags.h" +#include "net/http/http_response_headers.h" +#include "net/http/http_status_code.h" +#include "net/url_request/url_fetcher.h" +#include "net/url_request/url_request_context_getter.h" +#include "net/url_request/url_request_status.h" + +namespace { +const int kLoadFlagsIgnoreCookies = net::LOAD_DO_NOT_SEND_COOKIES | + net::LOAD_DO_NOT_SAVE_COOKIES; + +static bool CookiePartsContains(const std::vector<std::string>& parts, + const char* part) { + return std::find(parts.begin(), parts.end(), part) != parts.end(); +} + +bool ExtractOAuth2TokenPairResponse(base::DictionaryValue* dict, + std::string* refresh_token, + std::string* access_token, + int* expires_in_secs) { + DCHECK(refresh_token); + DCHECK(access_token); + DCHECK(expires_in_secs); + + if (!dict->GetStringWithoutPathExpansion("refresh_token", refresh_token) || + !dict->GetStringWithoutPathExpansion("access_token", access_token) || + !dict->GetIntegerWithoutPathExpansion("expires_in", expires_in_secs)) { + return false; + } + + return true; +} + +} // namespace + +// TODO(chron): Add sourceless version of this formatter. +// static +const char GaiaAuthFetcher::kClientLoginFormat[] = + "Email=%s&" + "Passwd=%s&" + "PersistentCookie=%s&" + "accountType=%s&" + "source=%s&" + "service=%s"; +// static +const char GaiaAuthFetcher::kClientLoginCaptchaFormat[] = + "Email=%s&" + "Passwd=%s&" + "PersistentCookie=%s&" + "accountType=%s&" + "source=%s&" + "service=%s&" + "logintoken=%s&" + "logincaptcha=%s"; +// static +const char GaiaAuthFetcher::kIssueAuthTokenFormat[] = + "SID=%s&" + "LSID=%s&" + "service=%s&" + "Session=%s"; +// static +const char GaiaAuthFetcher::kClientLoginToOAuth2BodyFormat[] = + "scope=%s&client_id=%s"; +// static +const char GaiaAuthFetcher::kOAuth2CodeToTokenPairBodyFormat[] = + "scope=%s&" + "grant_type=authorization_code&" + "client_id=%s&" + "client_secret=%s&" + "code=%s"; +// static +const char GaiaAuthFetcher::kOAuth2RevokeTokenBodyFormat[] = + "token=%s"; +// static +const char GaiaAuthFetcher::kGetUserInfoFormat[] = + "LSID=%s"; +// static +const char GaiaAuthFetcher::kMergeSessionFormat[] = + "uberauth=%s&" + "continue=%s&" + "source=%s"; +// static +const char GaiaAuthFetcher::kUberAuthTokenURLFormat[] = + "%s?source=%s&" + "issueuberauth=1"; + +const char GaiaAuthFetcher::kOAuthLoginFormat[] = "service=%s&source=%s"; + +// static +const char GaiaAuthFetcher::kAccountDeletedError[] = "AccountDeleted"; +const char GaiaAuthFetcher::kAccountDeletedErrorCode[] = "adel"; +// static +const char GaiaAuthFetcher::kAccountDisabledError[] = "AccountDisabled"; +const char GaiaAuthFetcher::kAccountDisabledErrorCode[] = "adis"; +// static +const char GaiaAuthFetcher::kBadAuthenticationError[] = "BadAuthentication"; +const char GaiaAuthFetcher::kBadAuthenticationErrorCode[] = "badauth"; +// static +const char GaiaAuthFetcher::kCaptchaError[] = "CaptchaRequired"; +const char GaiaAuthFetcher::kCaptchaErrorCode[] = "cr"; +// static +const char GaiaAuthFetcher::kServiceUnavailableError[] = + "ServiceUnavailable"; +const char GaiaAuthFetcher::kServiceUnavailableErrorCode[] = + "ire"; +// static +const char GaiaAuthFetcher::kErrorParam[] = "Error"; +// static +const char GaiaAuthFetcher::kErrorUrlParam[] = "Url"; +// static +const char GaiaAuthFetcher::kCaptchaUrlParam[] = "CaptchaUrl"; +// static +const char GaiaAuthFetcher::kCaptchaTokenParam[] = "CaptchaToken"; + +// static +const char GaiaAuthFetcher::kCookiePersistence[] = "true"; +// static +// TODO(johnnyg): When hosted accounts are supported by sync, +// we can always use "HOSTED_OR_GOOGLE" +const char GaiaAuthFetcher::kAccountTypeHostedOrGoogle[] = + "HOSTED_OR_GOOGLE"; +const char GaiaAuthFetcher::kAccountTypeGoogle[] = + "GOOGLE"; + +// static +const char GaiaAuthFetcher::kSecondFactor[] = "Info=InvalidSecondFactor"; + +// static +const char GaiaAuthFetcher::kAuthHeaderFormat[] = + "Authorization: GoogleLogin auth=%s"; +// static +const char GaiaAuthFetcher::kOAuthHeaderFormat[] = "Authorization: OAuth %s"; +// static +const char GaiaAuthFetcher::kOAuth2BearerHeaderFormat[] = + "Authorization: Bearer %s"; +// static +const char GaiaAuthFetcher::kClientLoginToOAuth2CookiePartSecure[] = "Secure"; +// static +const char GaiaAuthFetcher::kClientLoginToOAuth2CookiePartHttpOnly[] = + "HttpOnly"; +// static +const char GaiaAuthFetcher::kClientLoginToOAuth2CookiePartCodePrefix[] = + "oauth_code="; +// static +const int GaiaAuthFetcher::kClientLoginToOAuth2CookiePartCodePrefixLength = + arraysize(GaiaAuthFetcher::kClientLoginToOAuth2CookiePartCodePrefix) - 1; + +GaiaAuthFetcher::GaiaAuthFetcher(GaiaAuthConsumer* consumer, + const std::string& source, + net::URLRequestContextGetter* getter) + : consumer_(consumer), + getter_(getter), + source_(source), + client_login_gurl_(GaiaUrls::GetInstance()->client_login_url()), + issue_auth_token_gurl_(GaiaUrls::GetInstance()->issue_auth_token_url()), + oauth2_token_gurl_(GaiaUrls::GetInstance()->oauth2_token_url()), + oauth2_revoke_gurl_(GaiaUrls::GetInstance()->oauth2_revoke_url()), + get_user_info_gurl_(GaiaUrls::GetInstance()->get_user_info_url()), + merge_session_gurl_(GaiaUrls::GetInstance()->merge_session_url()), + uberauth_token_gurl_(base::StringPrintf(kUberAuthTokenURLFormat, + GaiaUrls::GetInstance()->oauth1_login_url().c_str(), source.c_str())), + oauth_login_gurl_(GaiaUrls::GetInstance()->oauth1_login_url()), + client_login_to_oauth2_gurl_( + GaiaUrls::GetInstance()->client_login_to_oauth2_url()), + fetch_pending_(false) {} + +GaiaAuthFetcher::~GaiaAuthFetcher() {} + +bool GaiaAuthFetcher::HasPendingFetch() { + return fetch_pending_; +} + +void GaiaAuthFetcher::CancelRequest() { + fetcher_.reset(); + fetch_pending_ = false; +} + +// static +net::URLFetcher* GaiaAuthFetcher::CreateGaiaFetcher( + net::URLRequestContextGetter* getter, + const std::string& body, + const std::string& headers, + const GURL& gaia_gurl, + int load_flags, + net::URLFetcherDelegate* delegate) { + net::URLFetcher* to_return = net::URLFetcher::Create( + 0, gaia_gurl, + body == "" ? net::URLFetcher::GET : net::URLFetcher::POST, + delegate); + to_return->SetRequestContext(getter); + to_return->SetUploadData("application/x-www-form-urlencoded", body); + + DVLOG(2) << "Gaia fetcher URL: " << gaia_gurl.spec(); + DVLOG(2) << "Gaia fetcher headers: " << headers; + DVLOG(2) << "Gaia fetcher body: " << body; + + // The Gaia token exchange requests do not require any cookie-based + // identification as part of requests. We suppress sending any cookies to + // maintain a separation between the user's browsing and Chrome's internal + // services. Where such mixing is desired (MergeSession), it will be done + // explicitly. + to_return->SetLoadFlags(load_flags); + + // Fetchers are sometimes cancelled because a network change was detected, + // especially at startup and after sign-in on ChromeOS. Retrying once should + // be enough in those cases; let the fetcher retry up to 3 times just in case. + // http://crbug.com/163710 + to_return->SetAutomaticallyRetryOnNetworkChanges(3); + + if (!headers.empty()) + to_return->SetExtraRequestHeaders(headers); + + return to_return; +} + +// static +std::string GaiaAuthFetcher::MakeClientLoginBody( + const std::string& username, + const std::string& password, + const std::string& source, + const char* service, + const std::string& login_token, + const std::string& login_captcha, + HostedAccountsSetting allow_hosted_accounts) { + std::string encoded_username = net::EscapeUrlEncodedData(username, true); + std::string encoded_password = net::EscapeUrlEncodedData(password, true); + std::string encoded_login_token = net::EscapeUrlEncodedData(login_token, + true); + std::string encoded_login_captcha = net::EscapeUrlEncodedData(login_captcha, + true); + + const char* account_type = allow_hosted_accounts == HostedAccountsAllowed ? + kAccountTypeHostedOrGoogle : + kAccountTypeGoogle; + + if (login_token.empty() || login_captcha.empty()) { + return base::StringPrintf(kClientLoginFormat, + encoded_username.c_str(), + encoded_password.c_str(), + kCookiePersistence, + account_type, + source.c_str(), + service); + } + + return base::StringPrintf(kClientLoginCaptchaFormat, + encoded_username.c_str(), + encoded_password.c_str(), + kCookiePersistence, + account_type, + source.c_str(), + service, + encoded_login_token.c_str(), + encoded_login_captcha.c_str()); +} + +// static +std::string GaiaAuthFetcher::MakeIssueAuthTokenBody( + const std::string& sid, + const std::string& lsid, + const char* const service) { + std::string encoded_sid = net::EscapeUrlEncodedData(sid, true); + std::string encoded_lsid = net::EscapeUrlEncodedData(lsid, true); + + // All tokens should be session tokens except the gaia auth token. + bool session = true; + if (!strcmp(service, GaiaConstants::kGaiaService)) + session = false; + + return base::StringPrintf(kIssueAuthTokenFormat, + encoded_sid.c_str(), + encoded_lsid.c_str(), + service, + session ? "true" : "false"); +} + +// static +std::string GaiaAuthFetcher::MakeGetAuthCodeBody() { + std::string encoded_scope = net::EscapeUrlEncodedData( + GaiaUrls::GetInstance()->oauth1_login_scope(), true); + std::string encoded_client_id = net::EscapeUrlEncodedData( + GaiaUrls::GetInstance()->oauth2_chrome_client_id(), true); + return base::StringPrintf(kClientLoginToOAuth2BodyFormat, + encoded_scope.c_str(), + encoded_client_id.c_str()); +} + +// static +std::string GaiaAuthFetcher::MakeGetTokenPairBody( + const std::string& auth_code) { + std::string encoded_scope = net::EscapeUrlEncodedData( + GaiaUrls::GetInstance()->oauth1_login_scope(), true); + std::string encoded_client_id = net::EscapeUrlEncodedData( + GaiaUrls::GetInstance()->oauth2_chrome_client_id(), true); + std::string encoded_client_secret = net::EscapeUrlEncodedData( + GaiaUrls::GetInstance()->oauth2_chrome_client_secret(), true); + std::string encoded_auth_code = net::EscapeUrlEncodedData(auth_code, true); + return base::StringPrintf(kOAuth2CodeToTokenPairBodyFormat, + encoded_scope.c_str(), + encoded_client_id.c_str(), + encoded_client_secret.c_str(), + encoded_auth_code.c_str()); +} + +// static +std::string GaiaAuthFetcher::MakeRevokeTokenBody( + const std::string& auth_token) { + return base::StringPrintf(kOAuth2RevokeTokenBodyFormat, auth_token.c_str()); +} + +// static +std::string GaiaAuthFetcher::MakeGetUserInfoBody(const std::string& lsid) { + std::string encoded_lsid = net::EscapeUrlEncodedData(lsid, true); + return base::StringPrintf(kGetUserInfoFormat, encoded_lsid.c_str()); +} + +// static +std::string GaiaAuthFetcher::MakeMergeSessionBody( + const std::string& auth_token, + const std::string& continue_url, + const std::string& source) { + std::string encoded_auth_token = net::EscapeUrlEncodedData(auth_token, true); + std::string encoded_continue_url = net::EscapeUrlEncodedData(continue_url, + true); + std::string encoded_source = net::EscapeUrlEncodedData(source, true); + return base::StringPrintf(kMergeSessionFormat, + encoded_auth_token.c_str(), + encoded_continue_url.c_str(), + encoded_source.c_str()); +} + +// static +std::string GaiaAuthFetcher::MakeGetAuthCodeHeader( + const std::string& auth_token) { + return base::StringPrintf(kAuthHeaderFormat, auth_token.c_str()); +} + +// Helper method that extracts tokens from a successful reply. +// static +void GaiaAuthFetcher::ParseClientLoginResponse(const std::string& data, + std::string* sid, + std::string* lsid, + std::string* token) { + using std::vector; + using std::pair; + using std::string; + sid->clear(); + lsid->clear(); + token->clear(); + vector<pair<string, string> > tokens; + base::SplitStringIntoKeyValuePairs(data, '=', '\n', &tokens); + for (vector<pair<string, string> >::iterator i = tokens.begin(); + i != tokens.end(); ++i) { + if (i->first == "SID") { + sid->assign(i->second); + } else if (i->first == "LSID") { + lsid->assign(i->second); + } else if (i->first == "Auth") { + token->assign(i->second); + } + } + // If this was a request for uberauth token, then that's all we've got in + // data. + if (sid->empty() && lsid->empty() && token->empty()) + token->assign(data); +} + +// static +std::string GaiaAuthFetcher::MakeOAuthLoginBody(const std::string& service, + const std::string& source) { + std::string encoded_service = net::EscapeUrlEncodedData(service, true); + std::string encoded_source = net::EscapeUrlEncodedData(source, true); + return base::StringPrintf(kOAuthLoginFormat, + encoded_service.c_str(), + encoded_source.c_str()); +} + +// static +void GaiaAuthFetcher::ParseClientLoginFailure(const std::string& data, + std::string* error, + std::string* error_url, + std::string* captcha_url, + std::string* captcha_token) { + using std::vector; + using std::pair; + using std::string; + + vector<pair<string, string> > tokens; + base::SplitStringIntoKeyValuePairs(data, '=', '\n', &tokens); + for (vector<pair<string, string> >::iterator i = tokens.begin(); + i != tokens.end(); ++i) { + if (i->first == kErrorParam) { + error->assign(i->second); + } else if (i->first == kErrorUrlParam) { + error_url->assign(i->second); + } else if (i->first == kCaptchaUrlParam) { + captcha_url->assign(i->second); + } else if (i->first == kCaptchaTokenParam) { + captcha_token->assign(i->second); + } + } +} + +// static +bool GaiaAuthFetcher::ParseClientLoginToOAuth2Response( + const net::ResponseCookies& cookies, + std::string* auth_code) { + DCHECK(auth_code); + net::ResponseCookies::const_iterator iter; + for (iter = cookies.begin(); iter != cookies.end(); ++iter) { + if (ParseClientLoginToOAuth2Cookie(*iter, auth_code)) + return true; + } + return false; +} + +// static +bool GaiaAuthFetcher::ParseClientLoginToOAuth2Cookie(const std::string& cookie, + std::string* auth_code) { + std::vector<std::string> parts; + base::SplitString(cookie, ';', &parts); + // Per documentation, the cookie should have Secure and HttpOnly. + if (!CookiePartsContains(parts, kClientLoginToOAuth2CookiePartSecure) || + !CookiePartsContains(parts, kClientLoginToOAuth2CookiePartHttpOnly)) { + return false; + } + + std::vector<std::string>::const_iterator iter; + for (iter = parts.begin(); iter != parts.end(); ++iter) { + const std::string& part = *iter; + if (StartsWithASCII( + part, kClientLoginToOAuth2CookiePartCodePrefix, false)) { + auth_code->assign(part.substr( + kClientLoginToOAuth2CookiePartCodePrefixLength)); + return true; + } + } + return false; +} + +void GaiaAuthFetcher::StartClientLogin( + const std::string& username, + const std::string& password, + const char* const service, + const std::string& login_token, + const std::string& login_captcha, + HostedAccountsSetting allow_hosted_accounts) { + + DCHECK(!fetch_pending_) << "Tried to fetch two things at once!"; + + // This class is thread agnostic, so be sure to call this only on the + // same thread each time. + DVLOG(1) << "Starting new ClientLogin fetch for:" << username; + + // Must outlive fetcher_. + request_body_ = MakeClientLoginBody(username, + password, + source_, + service, + login_token, + login_captcha, + allow_hosted_accounts); + fetcher_.reset(CreateGaiaFetcher(getter_, + request_body_, + std::string(), + client_login_gurl_, + kLoadFlagsIgnoreCookies, + this)); + fetch_pending_ = true; + fetcher_->Start(); +} + +void GaiaAuthFetcher::StartIssueAuthToken(const std::string& sid, + const std::string& lsid, + const char* const service) { + DCHECK(!fetch_pending_) << "Tried to fetch two things at once!"; + + DVLOG(1) << "Starting IssueAuthToken for: " << service; + requested_service_ = service; + request_body_ = MakeIssueAuthTokenBody(sid, lsid, service); + fetcher_.reset(CreateGaiaFetcher(getter_, + request_body_, + std::string(), + issue_auth_token_gurl_, + kLoadFlagsIgnoreCookies, + this)); + fetch_pending_ = true; + fetcher_->Start(); +} + +void GaiaAuthFetcher::StartLsoForOAuthLoginTokenExchange( + const std::string& auth_token) { + DCHECK(!fetch_pending_) << "Tried to fetch two things at once!"; + + DVLOG(1) << "Starting OAuth login token exchange with auth_token"; + request_body_ = MakeGetAuthCodeBody(); + client_login_to_oauth2_gurl_ = + GURL(GaiaUrls::GetInstance()->client_login_to_oauth2_url()); + + fetcher_.reset(CreateGaiaFetcher(getter_, + request_body_, + MakeGetAuthCodeHeader(auth_token), + client_login_to_oauth2_gurl_, + kLoadFlagsIgnoreCookies, + this)); + fetch_pending_ = true; + fetcher_->Start(); +} + +void GaiaAuthFetcher::StartRevokeOAuth2Token(const std::string& auth_token) { + DCHECK(!fetch_pending_) << "Tried to fetch two things at once!"; + + DVLOG(1) << "Starting OAuth2 token revocation"; + request_body_ = MakeRevokeTokenBody(auth_token); + fetcher_.reset(CreateGaiaFetcher(getter_, + request_body_, + std::string(), + oauth2_revoke_gurl_, + kLoadFlagsIgnoreCookies, + this)); + fetch_pending_ = true; + fetcher_->Start(); +} + +void GaiaAuthFetcher::StartCookieForOAuthLoginTokenExchange( + const std::string& session_index) { + DCHECK(!fetch_pending_) << "Tried to fetch two things at once!"; + + DVLOG(1) << "Starting OAuth login token fetch with cookie jar"; + request_body_ = MakeGetAuthCodeBody(); + + std::string url = GaiaUrls::GetInstance()->client_login_to_oauth2_url(); + if (!session_index.empty()) + url += "?authuser=" + session_index; + + client_login_to_oauth2_gurl_ = GURL(url); + + fetcher_.reset(CreateGaiaFetcher(getter_, + request_body_, + std::string(), + client_login_to_oauth2_gurl_, + net::LOAD_NORMAL, + this)); + fetch_pending_ = true; + fetcher_->Start(); +} + +void GaiaAuthFetcher::StartAuthCodeForOAuth2TokenExchange( + const std::string& auth_code) { + DCHECK(!fetch_pending_) << "Tried to fetch two things at once!"; + + DVLOG(1) << "Starting OAuth token pair fetch"; + request_body_ = MakeGetTokenPairBody(auth_code); + fetcher_.reset(CreateGaiaFetcher(getter_, + request_body_, + std::string(), + oauth2_token_gurl_, + kLoadFlagsIgnoreCookies, + this)); + fetch_pending_ = true; + fetcher_->Start(); +} + +void GaiaAuthFetcher::StartGetUserInfo(const std::string& lsid) { + DCHECK(!fetch_pending_) << "Tried to fetch two things at once!"; + + DVLOG(1) << "Starting GetUserInfo for lsid=" << lsid; + request_body_ = MakeGetUserInfoBody(lsid); + fetcher_.reset(CreateGaiaFetcher(getter_, + request_body_, + std::string(), + get_user_info_gurl_, + kLoadFlagsIgnoreCookies, + this)); + fetch_pending_ = true; + fetcher_->Start(); +} + +void GaiaAuthFetcher::StartMergeSession(const std::string& uber_token) { + DCHECK(!fetch_pending_) << "Tried to fetch two things at once!"; + + DVLOG(1) << "Starting MergeSession with uber_token=" << uber_token; + + // The continue URL is a required parameter of the MergeSession API, but in + // this case we don't actually need or want to navigate to it. Setting it to + // an arbitrary Google URL. + // + // In order for the new session to be merged correctly, the server needs to + // know what sessions already exist in the browser. The fetcher needs to be + // created such that it sends the cookies with the request, which is + // different from all other requests the fetcher can make. + std::string continue_url("http://www.google.com"); + request_body_ = MakeMergeSessionBody(uber_token, continue_url, source_); + fetcher_.reset(CreateGaiaFetcher(getter_, + request_body_, + std::string(), + merge_session_gurl_, + net::LOAD_NORMAL, + this)); + fetch_pending_ = true; + fetcher_->Start(); +} + +void GaiaAuthFetcher::StartTokenFetchForUberAuthExchange( + const std::string& access_token) { + DCHECK(!fetch_pending_) << "Tried to fetch two things at once!"; + + DVLOG(1) << "Starting StartTokenFetchForUberAuthExchange with access_token=" + << access_token; + std::string authentication_header = + base::StringPrintf(kOAuthHeaderFormat, access_token.c_str()); + fetcher_.reset(CreateGaiaFetcher(getter_, + std::string(), + authentication_header, + uberauth_token_gurl_, + kLoadFlagsIgnoreCookies, + this)); + fetch_pending_ = true; + fetcher_->Start(); +} + +void GaiaAuthFetcher::StartOAuthLogin(const std::string& access_token, + const std::string& service) { + DCHECK(!fetch_pending_) << "Tried to fetch two things at once!"; + + request_body_ = MakeOAuthLoginBody(service, source_); + std::string authentication_header = + base::StringPrintf(kOAuth2BearerHeaderFormat, access_token.c_str()); + fetcher_.reset(CreateGaiaFetcher(getter_, + request_body_, + authentication_header, + oauth_login_gurl_, + kLoadFlagsIgnoreCookies, + this)); + fetch_pending_ = true; + fetcher_->Start(); +} + +// static +GoogleServiceAuthError GaiaAuthFetcher::GenerateAuthError( + const std::string& data, + const net::URLRequestStatus& status) { + if (!status.is_success()) { + if (status.status() == net::URLRequestStatus::CANCELED) { + return GoogleServiceAuthError(GoogleServiceAuthError::REQUEST_CANCELED); + } else { + DLOG(WARNING) << "Could not reach Google Accounts servers: errno " + << status.error(); + return GoogleServiceAuthError::FromConnectionError(status.error()); + } + } else { + if (IsSecondFactorSuccess(data)) { + return GoogleServiceAuthError(GoogleServiceAuthError::TWO_FACTOR); + } + + std::string error; + std::string url; + std::string captcha_url; + std::string captcha_token; + ParseClientLoginFailure(data, &error, &url, &captcha_url, &captcha_token); + DLOG(WARNING) << "ClientLogin failed with " << error; + + if (error == kCaptchaError) { + GURL image_url( + GaiaUrls::GetInstance()->captcha_url_prefix() + captcha_url); + GURL unlock_url(url); + return GoogleServiceAuthError::FromClientLoginCaptchaChallenge( + captcha_token, image_url, unlock_url); + } + if (error == kAccountDeletedError) + return GoogleServiceAuthError(GoogleServiceAuthError::ACCOUNT_DELETED); + if (error == kAccountDisabledError) + return GoogleServiceAuthError(GoogleServiceAuthError::ACCOUNT_DISABLED); + if (error == kBadAuthenticationError) { + return GoogleServiceAuthError( + GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS); + } + if (error == kServiceUnavailableError) { + return GoogleServiceAuthError( + GoogleServiceAuthError::SERVICE_UNAVAILABLE); + } + + DLOG(WARNING) << "Incomprehensible response from Google Accounts servers."; + return GoogleServiceAuthError( + GoogleServiceAuthError::SERVICE_UNAVAILABLE); + } + + NOTREACHED(); + return GoogleServiceAuthError(GoogleServiceAuthError::SERVICE_UNAVAILABLE); +} + +// static +GoogleServiceAuthError GaiaAuthFetcher::GenerateOAuthLoginError( + const std::string& data, + const net::URLRequestStatus& status) { + if (!status.is_success()) { + if (status.status() == net::URLRequestStatus::CANCELED) { + return GoogleServiceAuthError(GoogleServiceAuthError::REQUEST_CANCELED); + } else { + DLOG(WARNING) << "Could not reach Google Accounts servers: errno " + << status.error(); + return GoogleServiceAuthError::FromConnectionError(status.error()); + } + } else { + if (IsSecondFactorSuccess(data)) { + return GoogleServiceAuthError(GoogleServiceAuthError::TWO_FACTOR); + } + + std::string error; + std::string url; + std::string captcha_url; + std::string captcha_token; + ParseClientLoginFailure(data, &error, &url, &captcha_url, &captcha_token); + LOG(WARNING) << "OAuthLogin failed with " << error; + + if (error == kCaptchaErrorCode) { + GURL image_url( + GaiaUrls::GetInstance()->captcha_url_prefix() + captcha_url); + GURL unlock_url(url); + return GoogleServiceAuthError::FromClientLoginCaptchaChallenge( + captcha_token, image_url, unlock_url); + } + if (error == kAccountDeletedErrorCode) + return GoogleServiceAuthError(GoogleServiceAuthError::ACCOUNT_DELETED); + if (error == kAccountDisabledErrorCode) + return GoogleServiceAuthError(GoogleServiceAuthError::ACCOUNT_DISABLED); + if (error == kBadAuthenticationErrorCode) { + return GoogleServiceAuthError( + GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS); + } + if (error == kServiceUnavailableErrorCode) { + return GoogleServiceAuthError( + GoogleServiceAuthError::SERVICE_UNAVAILABLE); + } + + DLOG(WARNING) << "Incomprehensible response from Google Accounts servers."; + return GoogleServiceAuthError( + GoogleServiceAuthError::SERVICE_UNAVAILABLE); + } + + NOTREACHED(); + return GoogleServiceAuthError(GoogleServiceAuthError::SERVICE_UNAVAILABLE); +} + +void GaiaAuthFetcher::OnClientLoginFetched(const std::string& data, + const net::URLRequestStatus& status, + int response_code) { + if (status.is_success() && response_code == net::HTTP_OK) { + DVLOG(1) << "ClientLogin successful!"; + std::string sid; + std::string lsid; + std::string token; + ParseClientLoginResponse(data, &sid, &lsid, &token); + consumer_->OnClientLoginSuccess( + GaiaAuthConsumer::ClientLoginResult(sid, lsid, token, data)); + } else { + consumer_->OnClientLoginFailure(GenerateAuthError(data, status)); + } +} + +void GaiaAuthFetcher::OnIssueAuthTokenFetched( + const std::string& data, + const net::URLRequestStatus& status, + int response_code) { + if (status.is_success() && response_code == net::HTTP_OK) { + // Only the bare token is returned in the body of this Gaia call + // without any padding. + consumer_->OnIssueAuthTokenSuccess(requested_service_, data); + } else { + consumer_->OnIssueAuthTokenFailure(requested_service_, + GenerateAuthError(data, status)); + } +} + +void GaiaAuthFetcher::OnClientLoginToOAuth2Fetched( + const std::string& data, + const net::ResponseCookies& cookies, + const net::URLRequestStatus& status, + int response_code) { + if (status.is_success() && response_code == net::HTTP_OK) { + std::string auth_code; + ParseClientLoginToOAuth2Response(cookies, &auth_code); + StartAuthCodeForOAuth2TokenExchange(auth_code); + } else { + consumer_->OnClientOAuthFailure(GenerateAuthError(data, status)); + } +} + +void GaiaAuthFetcher::OnOAuth2TokenPairFetched( + const std::string& data, + const net::URLRequestStatus& status, + int response_code) { + std::string refresh_token; + std::string access_token; + int expires_in_secs = 0; + + bool success = false; + if (status.is_success() && response_code == net::HTTP_OK) { + scoped_ptr<base::Value> value(base::JSONReader::Read(data)); + if (value.get() && value->GetType() == base::Value::TYPE_DICTIONARY) { + base::DictionaryValue* dict = + static_cast<base::DictionaryValue*>(value.get()); + success = ExtractOAuth2TokenPairResponse(dict, &refresh_token, + &access_token, &expires_in_secs); + } + } + + if (success) { + consumer_->OnClientOAuthSuccess( + GaiaAuthConsumer::ClientOAuthResult(refresh_token, access_token, + expires_in_secs)); + } else { + consumer_->OnClientOAuthFailure(GenerateAuthError(data, status)); + } +} + +void GaiaAuthFetcher::OnOAuth2RevokeTokenFetched( + const std::string& data, + const net::URLRequestStatus& status, + int response_code) { + consumer_->OnOAuth2RevokeTokenCompleted(); +} + +void GaiaAuthFetcher::OnGetUserInfoFetched( + const std::string& data, + const net::URLRequestStatus& status, + int response_code) { + if (status.is_success() && response_code == net::HTTP_OK) { + std::vector<std::pair<std::string, std::string> > tokens; + UserInfoMap matches; + base::SplitStringIntoKeyValuePairs(data, '=', '\n', &tokens); + std::vector<std::pair<std::string, std::string> >::iterator i; + for (i = tokens.begin(); i != tokens.end(); ++i) { + matches[i->first] = i->second; + } + consumer_->OnGetUserInfoSuccess(matches); + } else { + consumer_->OnGetUserInfoFailure(GenerateAuthError(data, status)); + } +} + +void GaiaAuthFetcher::OnMergeSessionFetched(const std::string& data, + const net::URLRequestStatus& status, + int response_code) { + if (status.is_success() && response_code == net::HTTP_OK) { + consumer_->OnMergeSessionSuccess(data); + } else { + consumer_->OnMergeSessionFailure(GenerateAuthError(data, status)); + } +} + +void GaiaAuthFetcher::OnUberAuthTokenFetch(const std::string& data, + const net::URLRequestStatus& status, + int response_code) { + if (status.is_success() && response_code == net::HTTP_OK) { + consumer_->OnUberAuthTokenSuccess(data); + } else { + consumer_->OnUberAuthTokenFailure(GenerateAuthError(data, status)); + } +} + +void GaiaAuthFetcher::OnOAuthLoginFetched(const std::string& data, + const net::URLRequestStatus& status, + int response_code) { + if (status.is_success() && response_code == net::HTTP_OK) { + DVLOG(1) << "ClientLogin successful!"; + std::string sid; + std::string lsid; + std::string token; + ParseClientLoginResponse(data, &sid, &lsid, &token); + consumer_->OnClientLoginSuccess( + GaiaAuthConsumer::ClientLoginResult(sid, lsid, token, data)); + } else { + consumer_->OnClientLoginFailure(GenerateAuthError(data, status)); + } +} + +void GaiaAuthFetcher::OnURLFetchComplete(const net::URLFetcher* source) { + fetch_pending_ = false; + // Some of the GAIA requests perform redirects, which results in the final + // URL of the fetcher not being the original URL requested. Therefore use + // the original URL when determining which OnXXX function to call. + const GURL& url = source->GetOriginalURL(); + const net::URLRequestStatus& status = source->GetStatus(); + int response_code = source->GetResponseCode(); + std::string data; + source->GetResponseAsString(&data); +#ifndef NDEBUG + std::string headers; + if (source->GetResponseHeaders()) + source->GetResponseHeaders()->GetNormalizedHeaders(&headers); + DVLOG(2) << "Response " << url.spec() << ", code = " << response_code << "\n" + << headers << "\n"; + DVLOG(2) << "data: " << data << "\n"; +#endif + // Retrieve the response headers from the request. Must only be called after + // the OnURLFetchComplete callback has run. + if (url == client_login_gurl_) { + OnClientLoginFetched(data, status, response_code); + } else if (url == issue_auth_token_gurl_) { + OnIssueAuthTokenFetched(data, status, response_code); + } else if (url == client_login_to_oauth2_gurl_) { + OnClientLoginToOAuth2Fetched( + data, source->GetCookies(), status, response_code); + } else if (url == oauth2_token_gurl_) { + OnOAuth2TokenPairFetched(data, status, response_code); + } else if (url == get_user_info_gurl_) { + OnGetUserInfoFetched(data, status, response_code); + } else if (url == merge_session_gurl_) { + OnMergeSessionFetched(data, status, response_code); + } else if (url == uberauth_token_gurl_) { + OnUberAuthTokenFetch(data, status, response_code); + } else if (url == oauth_login_gurl_) { + OnOAuthLoginFetched(data, status, response_code); + } else if (url == oauth2_revoke_gurl_) { + OnOAuth2RevokeTokenFetched(data, status, response_code); + } else { + NOTREACHED(); + } +} + +// static +bool GaiaAuthFetcher::IsSecondFactorSuccess( + const std::string& alleged_error) { + return alleged_error.find(kSecondFactor) != + std::string::npos; +} diff --git a/chromium/google_apis/gaia/gaia_auth_fetcher.h b/chromium/google_apis/gaia/gaia_auth_fetcher.h new file mode 100644 index 00000000000..5021a534ce3 --- /dev/null +++ b/chromium/google_apis/gaia/gaia_auth_fetcher.h @@ -0,0 +1,391 @@ +// Copyright (c) 2012 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 GOOGLE_APIS_GAIA_GAIA_AUTH_FETCHER_H_ +#define GOOGLE_APIS_GAIA_GAIA_AUTH_FETCHER_H_ + +#include <string> +#include <vector> + +#include "base/gtest_prod_util.h" +#include "base/memory/scoped_ptr.h" +#include "google_apis/gaia/gaia_auth_consumer.h" +#include "google_apis/gaia/google_service_auth_error.h" +#include "net/url_request/url_fetcher_delegate.h" +#include "url/gurl.h" + +// Authenticate a user against the Google Accounts ClientLogin API +// with various capabilities and return results to a GaiaAuthConsumer. +// +// In the future, we will also issue auth tokens from this class. +// This class should be used on a single thread, but it can be whichever thread +// that you like. +// +// This class can handle one request at a time on any thread. To parallelize +// requests, create multiple GaiaAuthFetcher's. + +class GaiaAuthFetcherTest; + +namespace net { +class URLFetcher; +class URLRequestContextGetter; +class URLRequestStatus; +} + +class GaiaAuthFetcher : public net::URLFetcherDelegate { + public: + enum HostedAccountsSetting { + HostedAccountsAllowed, + HostedAccountsNotAllowed + }; + + // Magic string indicating that, while a second factor is still + // needed to complete authentication, the user provided the right password. + static const char kSecondFactor[]; + + // This will later be hidden behind an auth service which caches + // tokens. + GaiaAuthFetcher(GaiaAuthConsumer* consumer, + const std::string& source, + net::URLRequestContextGetter* getter); + virtual ~GaiaAuthFetcher(); + + // Start a request to obtain the SID and LSID cookies for the the account + // identified by |username| and |password|. If |service| is not null or + // empty, then also obtains a service token for specified service. + // + // If this is a second call because of captcha challenge, then the + // |login_token| and |login_captcha| arugment should correspond to the + // solution of the challenge. + // + // Either OnClientLoginSuccess or OnClientLoginFailure will be + // called on the consumer on the original thread. + void StartClientLogin(const std::string& username, + const std::string& password, + const char* const service, + const std::string& login_token, + const std::string& login_captcha, + HostedAccountsSetting allow_hosted_accounts); + + // Start a request to obtain service token for the the account identified by + // |sid| and |lsid| and the |service|. + // + // Either OnIssueAuthTokenSuccess or OnIssueAuthTokenFailure will be + // called on the consumer on the original thread. + void StartIssueAuthToken(const std::string& sid, + const std::string& lsid, + const char* const service); + + // Start a request to obtain |service| token for the the account identified by + // |uber_token|. + // + // Either OnIssueAuthTokenSuccess or OnIssueAuthTokenFailure will be + // called on the consumer on the original thread. + void StartTokenAuth(const std::string& uber_token, + const char* const service); + + // Start a request to obtain service token for the the account identified by + // |oauth2_access_token| and the |service|. + // + // Either OnIssueAuthTokenSuccess or OnIssueAuthTokenFailure will be + // called on the consumer on the original thread. + void StartIssueAuthTokenForOAuth2(const std::string& oauth2_access_token, + const char* const service); + + // Start a request to exchange an "lso" service token given by |auth_token| + // for an OAuthLogin-scoped oauth2 token. + // + // Either OnClientOAuthSuccess or OnClientOAuthFailure will be + // called on the consumer on the original thread. + void StartLsoForOAuthLoginTokenExchange(const std::string& auth_token); + + // Start a request to revoke |auth_token|. + // + // OnOAuth2RevokeTokenCompleted will be called on the consumer on the original + // thread. + void StartRevokeOAuth2Token(const std::string& auth_token); + + // Start a request to exchange the cookies of a signed-in user session + // for an OAuthLogin-scoped oauth2 token. In the case of a session with + // multiple accounts signed in, |session_index| indicate the which of accounts + // within the session. + // + // Either OnClientOAuthSuccess or OnClientOAuthFailure will be + // called on the consumer on the original thread. + void StartCookieForOAuthLoginTokenExchange(const std::string& session_index); + + // Start a request to exchange the authorization code for an OAuthLogin-scoped + // oauth2 token. + // + // Either OnClientOAuthSuccess or OnClientOAuthFailure will be + // called on the consumer on the original thread. + void StartAuthCodeForOAuth2TokenExchange(const std::string& auth_code); + + // Start a request to get user info for the account identified by |lsid|. + // + // Either OnGetUserInfoSuccess or OnGetUserInfoFailure will be + // called on the consumer on the original thread. + void StartGetUserInfo(const std::string& lsid); + + // Start a MergeSession request to pre-login the user with the given + // credentials. + // + // Start a MergeSession request to fill the browsing cookie jar with + // credentials represented by the account whose uber-auth token is + // |uber_token|. This method will modify the cookies of the current profile. + // + // Either OnMergeSessionSuccess or OnMergeSessionFailure will be + // called on the consumer on the original thread. + void StartMergeSession(const std::string& uber_token); + + // Start a request to exchange an OAuthLogin-scoped oauth2 access token for an + // uber-auth token. The returned token can be used with the method + // StartMergeSession(). + // + // Either OnUberAuthTokenSuccess or OnUberAuthTokenFailure will be + // called on the consumer on the original thread. + void StartTokenFetchForUberAuthExchange(const std::string& access_token); + + // Start a request to exchange an OAuthLogin-scoped oauth2 access token for a + // ClientLogin-style service tokens. The response to this request is the + // same as the response to a ClientLogin request, except that captcha + // challenges are never issued. + // + // Either OnClientLoginSuccess or OnClientLoginFailure will be + // called on the consumer on the original thread. If |service| is empty, + // the call will attempt to fetch uber auth token. + void StartOAuthLogin(const std::string& access_token, + const std::string& service); + + // Implementation of net::URLFetcherDelegate + virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE; + + // StartClientLogin been called && results not back yet? + bool HasPendingFetch(); + + // Stop any URL fetches in progress. + void CancelRequest(); + + // From a URLFetcher result, generate an appropriate error. + // From the API documentation, both IssueAuthToken and ClientLogin have + // the same error returns. + static GoogleServiceAuthError GenerateOAuthLoginError( + const std::string& data, + const net::URLRequestStatus& status); + + private: + // ClientLogin body constants that don't change + static const char kCookiePersistence[]; + static const char kAccountTypeHostedOrGoogle[]; + static const char kAccountTypeGoogle[]; + + // The format of the POST body for ClientLogin. + static const char kClientLoginFormat[]; + // The format of said POST body when CAPTCHA token & answer are specified. + static const char kClientLoginCaptchaFormat[]; + // The format of the POST body for IssueAuthToken. + static const char kIssueAuthTokenFormat[]; + // The format of the POST body to get OAuth2 auth code from auth token. + static const char kClientLoginToOAuth2BodyFormat[]; + // The format of the POST body to get OAuth2 token pair from auth code. + static const char kOAuth2CodeToTokenPairBodyFormat[]; + // The format of the POST body to revoke an OAuth2 token. + static const char kOAuth2RevokeTokenBodyFormat[]; + // The format of the POST body for GetUserInfo. + static const char kGetUserInfoFormat[]; + // The format of the POST body for MergeSession. + static const char kMergeSessionFormat[]; + // The format of the URL for UberAuthToken. + static const char kUberAuthTokenURLFormat[]; + // The format of the body for OAuthLogin. + static const char kOAuthLoginFormat[]; + + // Constants for parsing ClientLogin errors. + static const char kAccountDeletedError[]; + static const char kAccountDeletedErrorCode[]; + static const char kAccountDisabledError[]; + static const char kAccountDisabledErrorCode[]; + static const char kBadAuthenticationError[]; + static const char kBadAuthenticationErrorCode[]; + static const char kCaptchaError[]; + static const char kCaptchaErrorCode[]; + static const char kServiceUnavailableError[]; + static const char kServiceUnavailableErrorCode[]; + static const char kErrorParam[]; + static const char kErrorUrlParam[]; + static const char kCaptchaUrlParam[]; + static const char kCaptchaTokenParam[]; + + // Constants for parsing ClientOAuth errors. + static const char kNeedsAdditional[]; + static const char kCaptcha[]; + static const char kTwoFactor[]; + + // Constants for request/response for OAuth2 requests. + static const char kAuthHeaderFormat[]; + static const char kOAuthHeaderFormat[]; + static const char kOAuth2BearerHeaderFormat[]; + static const char kClientLoginToOAuth2CookiePartSecure[]; + static const char kClientLoginToOAuth2CookiePartHttpOnly[]; + static const char kClientLoginToOAuth2CookiePartCodePrefix[]; + static const int kClientLoginToOAuth2CookiePartCodePrefixLength; + + // Process the results of a ClientLogin fetch. + void OnClientLoginFetched(const std::string& data, + const net::URLRequestStatus& status, + int response_code); + + void OnIssueAuthTokenFetched(const std::string& data, + const net::URLRequestStatus& status, + int response_code); + + void OnClientLoginToOAuth2Fetched(const std::string& data, + const net::ResponseCookies& cookies, + const net::URLRequestStatus& status, + int response_code); + + void OnOAuth2TokenPairFetched(const std::string& data, + const net::URLRequestStatus& status, + int response_code); + + void OnOAuth2RevokeTokenFetched(const std::string& data, + const net::URLRequestStatus& status, + int response_code); + + void OnGetUserInfoFetched(const std::string& data, + const net::URLRequestStatus& status, + int response_code); + + void OnMergeSessionFetched(const std::string& data, + const net::URLRequestStatus& status, + int response_code); + + void OnUberAuthTokenFetch(const std::string& data, + const net::URLRequestStatus& status, + int response_code); + + void OnOAuthLoginFetched(const std::string& data, + const net::URLRequestStatus& status, + int response_code); + + // Tokenize the results of a ClientLogin fetch. + static void ParseClientLoginResponse(const std::string& data, + std::string* sid, + std::string* lsid, + std::string* token); + + static void ParseClientLoginFailure(const std::string& data, + std::string* error, + std::string* error_url, + std::string* captcha_url, + std::string* captcha_token); + + // Parse ClientLogin to OAuth2 response. + static bool ParseClientLoginToOAuth2Response( + const net::ResponseCookies& cookies, + std::string* auth_code); + + static bool ParseClientLoginToOAuth2Cookie(const std::string& cookie, + std::string* auth_code); + + // Is this a special case Gaia error for TwoFactor auth? + static bool IsSecondFactorSuccess(const std::string& alleged_error); + + // Given parameters, create a ClientLogin request body. + static std::string MakeClientLoginBody( + const std::string& username, + const std::string& password, + const std::string& source, + const char* const service, + const std::string& login_token, + const std::string& login_captcha, + HostedAccountsSetting allow_hosted_accounts); + // Supply the sid / lsid returned from ClientLogin in order to + // request a long lived auth token for a service. + static std::string MakeIssueAuthTokenBody(const std::string& sid, + const std::string& lsid, + const char* const service); + // Create body to get OAuth2 auth code. + static std::string MakeGetAuthCodeBody(); + // Given auth code, create body to get OAuth2 token pair. + static std::string MakeGetTokenPairBody(const std::string& auth_code); + // Given an OAuth2 token, create body to revoke the token. + std::string MakeRevokeTokenBody(const std::string& auth_token); + // Supply the lsid returned from ClientLogin in order to fetch + // user information. + static std::string MakeGetUserInfoBody(const std::string& lsid); + + // Supply the authentication token returned from StartIssueAuthToken. + static std::string MakeMergeSessionBody(const std::string& auth_token, + const std::string& continue_url, + const std::string& source); + + static std::string MakeGetAuthCodeHeader(const std::string& auth_token); + + static std::string MakeOAuthLoginBody(const std::string& service, + const std::string& source); + + // Create a fetcher usable for making any Gaia request. |body| is used + // as the body of the POST request sent to GAIA. Any strings listed in + // |headers| are added as extra HTTP headers in the request. + // + // |load_flags| are passed to directly to net::URLFetcher::Create() when + // creating the URL fetcher. + static net::URLFetcher* CreateGaiaFetcher( + net::URLRequestContextGetter* getter, + const std::string& body, + const std::string& headers, + const GURL& gaia_gurl, + int load_flags, + net::URLFetcherDelegate* delegate); + + // From a URLFetcher result, generate an appropriate error. + // From the API documentation, both IssueAuthToken and ClientLogin have + // the same error returns. + static GoogleServiceAuthError GenerateAuthError( + const std::string& data, + const net::URLRequestStatus& status); + + // These fields are common to GaiaAuthFetcher, same every request + GaiaAuthConsumer* const consumer_; + net::URLRequestContextGetter* const getter_; + std::string source_; + const GURL client_login_gurl_; + const GURL issue_auth_token_gurl_; + const GURL oauth2_token_gurl_; + const GURL oauth2_revoke_gurl_; + const GURL get_user_info_gurl_; + const GURL merge_session_gurl_; + const GURL uberauth_token_gurl_; + const GURL oauth_login_gurl_; + + // While a fetch is going on: + scoped_ptr<net::URLFetcher> fetcher_; + GURL client_login_to_oauth2_gurl_; + std::string request_body_; + std::string requested_service_; // Currently tracked for IssueAuthToken only. + bool fetch_pending_; + + friend class GaiaAuthFetcherTest; + FRIEND_TEST_ALL_PREFIXES(GaiaAuthFetcherTest, CaptchaParse); + FRIEND_TEST_ALL_PREFIXES(GaiaAuthFetcherTest, AccountDeletedError); + FRIEND_TEST_ALL_PREFIXES(GaiaAuthFetcherTest, AccountDisabledError); + FRIEND_TEST_ALL_PREFIXES(GaiaAuthFetcherTest, BadAuthenticationError); + FRIEND_TEST_ALL_PREFIXES(GaiaAuthFetcherTest, IncomprehensibleError); + FRIEND_TEST_ALL_PREFIXES(GaiaAuthFetcherTest, ServiceUnavailableError); + FRIEND_TEST_ALL_PREFIXES(GaiaAuthFetcherTest, CheckNormalErrorCode); + FRIEND_TEST_ALL_PREFIXES(GaiaAuthFetcherTest, CheckTwoFactorResponse); + FRIEND_TEST_ALL_PREFIXES(GaiaAuthFetcherTest, LoginNetFailure); + FRIEND_TEST_ALL_PREFIXES(GaiaAuthFetcherTest, + ParseClientLoginToOAuth2Response); + FRIEND_TEST_ALL_PREFIXES(GaiaAuthFetcherTest, ParseOAuth2TokenPairResponse); + FRIEND_TEST_ALL_PREFIXES(GaiaAuthFetcherTest, ClientOAuthSuccess); + FRIEND_TEST_ALL_PREFIXES(GaiaAuthFetcherTest, ClientOAuthWithQuote); + FRIEND_TEST_ALL_PREFIXES(GaiaAuthFetcherTest, ClientOAuthChallengeSuccess); + FRIEND_TEST_ALL_PREFIXES(GaiaAuthFetcherTest, ClientOAuthChallengeQuote); + + DISALLOW_COPY_AND_ASSIGN(GaiaAuthFetcher); +}; + +#endif // GOOGLE_APIS_GAIA_GAIA_AUTH_FETCHER_H_ diff --git a/chromium/google_apis/gaia/gaia_auth_fetcher_unittest.cc b/chromium/google_apis/gaia/gaia_auth_fetcher_unittest.cc new file mode 100644 index 00000000000..74fed98f47c --- /dev/null +++ b/chromium/google_apis/gaia/gaia_auth_fetcher_unittest.cc @@ -0,0 +1,803 @@ +// Copyright (c) 2012 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. +// +// A complete set of unit tests for GaiaAuthFetcher. +// Originally ported from GoogleAuthenticator tests. + +#include <string> + +#include "base/json/json_reader.h" +#include "base/strings/stringprintf.h" +#include "base/values.h" +#include "google_apis/gaia/gaia_auth_consumer.h" +#include "google_apis/gaia/gaia_auth_fetcher.h" +#include "google_apis/gaia/gaia_urls.h" +#include "google_apis/gaia/google_service_auth_error.h" +#include "google_apis/gaia/mock_url_fetcher_factory.h" +#include "google_apis/google_api_keys.h" +#include "net/base/load_flags.h" +#include "net/base/net_errors.h" +#include "net/http/http_status_code.h" +#include "net/url_request/test_url_fetcher_factory.h" +#include "net/url_request/url_fetcher_delegate.h" +#include "net/url_request/url_request_status.h" +#include "net/url_request/url_request_test_util.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +using ::testing::_; +using ::testing::Invoke; + +namespace { +static const char kGetAuthCodeValidCookie[] = + "oauth_code=test-code; Path=/test; Secure; HttpOnly"; +static const char kGetAuthCodeCookieNoSecure[] = + "oauth_code=test-code; Path=/test; HttpOnly"; +static const char kGetAuthCodeCookieNoHttpOnly[] = + "oauth_code=test-code; Path=/test; Secure"; +static const char kGetAuthCodeCookieNoOAuthCode[] = + "Path=/test; Secure; HttpOnly"; +static const char kGetTokenPairValidResponse[] = + "{" + " \"refresh_token\": \"rt1\"," + " \"access_token\": \"at1\"," + " \"expires_in\": 3600," + " \"token_type\": \"Bearer\"" + "}"; +static const char kClientOAuthValidResponse[] = + "{" + " \"oauth2\": {" + " \"refresh_token\": \"rt1\"," + " \"access_token\": \"at1\"," + " \"expires_in\": 3600," + " \"token_type\": \"Bearer\"" + " }" + "}"; + +} // namespace + +MockFetcher::MockFetcher(bool success, + const GURL& url, + const std::string& results, + net::URLFetcher::RequestType request_type, + net::URLFetcherDelegate* d) + : TestURLFetcher(0, url, d) { + set_url(url); + net::URLRequestStatus::Status code; + + if (success) { + set_response_code(net::HTTP_OK); + code = net::URLRequestStatus::SUCCESS; + } else { + set_response_code(net::HTTP_FORBIDDEN); + code = net::URLRequestStatus::FAILED; + } + + set_status(net::URLRequestStatus(code, 0)); + SetResponseString(results); +} + +MockFetcher::MockFetcher(const GURL& url, + const net::URLRequestStatus& status, + int response_code, + const net::ResponseCookies& cookies, + const std::string& results, + net::URLFetcher::RequestType request_type, + net::URLFetcherDelegate* d) + : TestURLFetcher(0, url, d) { + set_url(url); + set_status(status); + set_response_code(response_code); + set_cookies(cookies); + SetResponseString(results); +} + +MockFetcher::~MockFetcher() {} + +void MockFetcher::Start() { + delegate()->OnURLFetchComplete(this); +} + +class GaiaAuthFetcherTest : public testing::Test { + protected: + GaiaAuthFetcherTest() + : client_login_source_(GaiaUrls::GetInstance()->client_login_url()), + issue_auth_token_source_( + GaiaUrls::GetInstance()->issue_auth_token_url()), + client_login_to_oauth2_source_( + GaiaUrls::GetInstance()->client_login_to_oauth2_url()), + oauth2_token_source_(GaiaUrls::GetInstance()->oauth2_token_url()), + token_auth_source_(GaiaUrls::GetInstance()->token_auth_url()), + merge_session_source_(GaiaUrls::GetInstance()->merge_session_url()), + uberauth_token_source_(base::StringPrintf( + "%s?source=&issueuberauth=1", + GaiaUrls::GetInstance()->oauth1_login_url().c_str())), + oauth_login_gurl_(GaiaUrls::GetInstance()->oauth1_login_url()) {} + + void RunParsingTest(const std::string& data, + const std::string& sid, + const std::string& lsid, + const std::string& token) { + std::string out_sid; + std::string out_lsid; + std::string out_token; + + GaiaAuthFetcher::ParseClientLoginResponse(data, + &out_sid, + &out_lsid, + &out_token); + EXPECT_EQ(lsid, out_lsid); + EXPECT_EQ(sid, out_sid); + EXPECT_EQ(token, out_token); + } + + void RunErrorParsingTest(const std::string& data, + const std::string& error, + const std::string& error_url, + const std::string& captcha_url, + const std::string& captcha_token) { + std::string out_error; + std::string out_error_url; + std::string out_captcha_url; + std::string out_captcha_token; + + GaiaAuthFetcher::ParseClientLoginFailure(data, + &out_error, + &out_error_url, + &out_captcha_url, + &out_captcha_token); + EXPECT_EQ(error, out_error); + EXPECT_EQ(error_url, out_error_url); + EXPECT_EQ(captcha_url, out_captcha_url); + EXPECT_EQ(captcha_token, out_captcha_token); + } + + net::ResponseCookies cookies_; + GURL client_login_source_; + GURL issue_auth_token_source_; + GURL client_login_to_oauth2_source_; + GURL oauth2_token_source_; + GURL token_auth_source_; + GURL merge_session_source_; + GURL uberauth_token_source_; + GURL oauth_login_gurl_; + + protected: + net::TestURLRequestContextGetter* GetRequestContext() { + if (!request_context_getter_) { + request_context_getter_ = new net::TestURLRequestContextGetter( + message_loop_.message_loop_proxy()); + } + return request_context_getter_; + } + + base::MessageLoop message_loop_; + scoped_refptr<net::TestURLRequestContextGetter> request_context_getter_; +}; + +class MockGaiaConsumer : public GaiaAuthConsumer { + public: + MockGaiaConsumer() {} + ~MockGaiaConsumer() {} + + MOCK_METHOD1(OnClientLoginSuccess, void(const ClientLoginResult& result)); + MOCK_METHOD2(OnIssueAuthTokenSuccess, void(const std::string& service, + const std::string& token)); + MOCK_METHOD1(OnClientOAuthSuccess, + void(const GaiaAuthConsumer::ClientOAuthResult& result)); + MOCK_METHOD1(OnMergeSessionSuccess, void(const std::string& data)); + MOCK_METHOD1(OnUberAuthTokenSuccess, void(const std::string& data)); + MOCK_METHOD1(OnClientLoginFailure, + void(const GoogleServiceAuthError& error)); + MOCK_METHOD2(OnIssueAuthTokenFailure, void(const std::string& service, + const GoogleServiceAuthError& error)); + MOCK_METHOD1(OnClientOAuthFailure, + void(const GoogleServiceAuthError& error)); + MOCK_METHOD1(OnMergeSessionFailure, void( + const GoogleServiceAuthError& error)); + MOCK_METHOD1(OnUberAuthTokenFailure, void( + const GoogleServiceAuthError& error)); +}; + +#if defined(OS_WIN) +#define MAYBE_ErrorComparator DISABLED_ErrorComparator +#else +#define MAYBE_ErrorComparator ErrorComparator +#endif + +TEST_F(GaiaAuthFetcherTest, MAYBE_ErrorComparator) { + GoogleServiceAuthError expected_error = + GoogleServiceAuthError::FromConnectionError(-101); + + GoogleServiceAuthError matching_error = + GoogleServiceAuthError::FromConnectionError(-101); + + EXPECT_TRUE(expected_error == matching_error); + + expected_error = GoogleServiceAuthError::FromConnectionError(6); + + EXPECT_FALSE(expected_error == matching_error); + + expected_error = GoogleServiceAuthError(GoogleServiceAuthError::NONE); + + EXPECT_FALSE(expected_error == matching_error); + + matching_error = GoogleServiceAuthError(GoogleServiceAuthError::NONE); + + EXPECT_TRUE(expected_error == matching_error); +} + +TEST_F(GaiaAuthFetcherTest, LoginNetFailure) { + int error_no = net::ERR_CONNECTION_RESET; + net::URLRequestStatus status(net::URLRequestStatus::FAILED, error_no); + + GoogleServiceAuthError expected_error = + GoogleServiceAuthError::FromConnectionError(error_no); + + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnClientLoginFailure(expected_error)) + .Times(1); + + GaiaAuthFetcher auth(&consumer, std::string(), GetRequestContext()); + + MockFetcher mock_fetcher( + client_login_source_, status, 0, net::ResponseCookies(), std::string(), + net::URLFetcher::GET, &auth); + auth.OnURLFetchComplete(&mock_fetcher); +} + +TEST_F(GaiaAuthFetcherTest, TokenNetFailure) { + int error_no = net::ERR_CONNECTION_RESET; + net::URLRequestStatus status(net::URLRequestStatus::FAILED, error_no); + + GoogleServiceAuthError expected_error = + GoogleServiceAuthError::FromConnectionError(error_no); + + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnIssueAuthTokenFailure(_, expected_error)) + .Times(1); + + GaiaAuthFetcher auth(&consumer, std::string(), GetRequestContext()); + + MockFetcher mock_fetcher( + issue_auth_token_source_, status, 0, cookies_, std::string(), + net::URLFetcher::GET, &auth); + auth.OnURLFetchComplete(&mock_fetcher); +} + + +TEST_F(GaiaAuthFetcherTest, LoginDenied) { + std::string data("Error=BadAuthentication"); + net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); + + GoogleServiceAuthError expected_error( + GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS); + + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnClientLoginFailure(expected_error)) + .Times(1); + + GaiaAuthFetcher auth(&consumer, std::string(), GetRequestContext()); + + MockFetcher mock_fetcher( + client_login_source_, status, net::HTTP_FORBIDDEN, cookies_, data, + net::URLFetcher::GET, &auth); + auth.OnURLFetchComplete(&mock_fetcher); +} + +TEST_F(GaiaAuthFetcherTest, ParseRequest) { + RunParsingTest("SID=sid\nLSID=lsid\nAuth=auth\n", "sid", "lsid", "auth"); + RunParsingTest("LSID=lsid\nSID=sid\nAuth=auth\n", "sid", "lsid", "auth"); + RunParsingTest("SID=sid\nLSID=lsid\nAuth=auth", "sid", "lsid", "auth"); + RunParsingTest("SID=sid\nAuth=auth\n", "sid", std::string(), "auth"); + RunParsingTest("LSID=lsid\nAuth=auth\n", std::string(), "lsid", "auth"); + RunParsingTest("\nAuth=auth\n", std::string(), std::string(), "auth"); + RunParsingTest("SID=sid", "sid", std::string(), std::string()); +} + +TEST_F(GaiaAuthFetcherTest, ParseErrorRequest) { + RunErrorParsingTest("Url=U\n" + "Error=E\n" + "CaptchaToken=T\n" + "CaptchaUrl=C\n", "E", "U", "C", "T"); + RunErrorParsingTest("CaptchaToken=T\n" + "Error=E\n" + "Url=U\n" + "CaptchaUrl=C\n", "E", "U", "C", "T"); + RunErrorParsingTest("\n\n\nCaptchaToken=T\n" + "\nError=E\n" + "\nUrl=U\n" + "CaptchaUrl=C\n", "E", "U", "C", "T"); +} + + +TEST_F(GaiaAuthFetcherTest, OnlineLogin) { + std::string data("SID=sid\nLSID=lsid\nAuth=auth\n"); + + GaiaAuthConsumer::ClientLoginResult result; + result.lsid = "lsid"; + result.sid = "sid"; + result.token = "auth"; + result.data = data; + + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnClientLoginSuccess(result)) + .Times(1); + + GaiaAuthFetcher auth(&consumer, std::string(), GetRequestContext()); + net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); + MockFetcher mock_fetcher( + client_login_source_, status, net::HTTP_OK, cookies_, data, + net::URLFetcher::GET, &auth); + auth.OnURLFetchComplete(&mock_fetcher); +} + +TEST_F(GaiaAuthFetcherTest, WorkingIssueAuthToken) { + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnIssueAuthTokenSuccess(_, "token")) + .Times(1); + + GaiaAuthFetcher auth(&consumer, std::string(), GetRequestContext()); + net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); + MockFetcher mock_fetcher( + issue_auth_token_source_, status, net::HTTP_OK, cookies_, "token", + net::URLFetcher::GET, &auth); + auth.OnURLFetchComplete(&mock_fetcher); +} + +TEST_F(GaiaAuthFetcherTest, CheckTwoFactorResponse) { + std::string response = + base::StringPrintf("Error=BadAuthentication\n%s\n", + GaiaAuthFetcher::kSecondFactor); + EXPECT_TRUE(GaiaAuthFetcher::IsSecondFactorSuccess(response)); +} + +TEST_F(GaiaAuthFetcherTest, CheckNormalErrorCode) { + std::string response = "Error=BadAuthentication\n"; + EXPECT_FALSE(GaiaAuthFetcher::IsSecondFactorSuccess(response)); +} + +TEST_F(GaiaAuthFetcherTest, TwoFactorLogin) { + std::string response = base::StringPrintf("Error=BadAuthentication\n%s\n", + GaiaAuthFetcher::kSecondFactor); + + GoogleServiceAuthError error = + GoogleServiceAuthError(GoogleServiceAuthError::TWO_FACTOR); + + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnClientLoginFailure(error)) + .Times(1); + + GaiaAuthFetcher auth(&consumer, std::string(), GetRequestContext()); + net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); + MockFetcher mock_fetcher( + client_login_source_, status, net::HTTP_FORBIDDEN, cookies_, response, + net::URLFetcher::GET, &auth); + auth.OnURLFetchComplete(&mock_fetcher); +} + +TEST_F(GaiaAuthFetcherTest, CaptchaParse) { + net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); + std::string data = "Url=http://www.google.com/login/captcha\n" + "Error=CaptchaRequired\n" + "CaptchaToken=CCTOKEN\n" + "CaptchaUrl=Captcha?ctoken=CCTOKEN\n"; + GoogleServiceAuthError error = + GaiaAuthFetcher::GenerateAuthError(data, status); + + std::string token = "CCTOKEN"; + GURL image_url("http://accounts.google.com/Captcha?ctoken=CCTOKEN"); + GURL unlock_url("http://www.google.com/login/captcha"); + + EXPECT_EQ(error.state(), GoogleServiceAuthError::CAPTCHA_REQUIRED); + EXPECT_EQ(error.captcha().token, token); + EXPECT_EQ(error.captcha().image_url, image_url); + EXPECT_EQ(error.captcha().unlock_url, unlock_url); +} + +TEST_F(GaiaAuthFetcherTest, AccountDeletedError) { + net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); + std::string data = "Error=AccountDeleted\n"; + GoogleServiceAuthError error = + GaiaAuthFetcher::GenerateAuthError(data, status); + EXPECT_EQ(error.state(), GoogleServiceAuthError::ACCOUNT_DELETED); +} + +TEST_F(GaiaAuthFetcherTest, AccountDisabledError) { + net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); + std::string data = "Error=AccountDisabled\n"; + GoogleServiceAuthError error = + GaiaAuthFetcher::GenerateAuthError(data, status); + EXPECT_EQ(error.state(), GoogleServiceAuthError::ACCOUNT_DISABLED); +} + +TEST_F(GaiaAuthFetcherTest,BadAuthenticationError) { + net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); + std::string data = "Error=BadAuthentication\n"; + GoogleServiceAuthError error = + GaiaAuthFetcher::GenerateAuthError(data, status); + EXPECT_EQ(error.state(), GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS); +} + +TEST_F(GaiaAuthFetcherTest,IncomprehensibleError) { + net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); + std::string data = "Error=Gobbledygook\n"; + GoogleServiceAuthError error = + GaiaAuthFetcher::GenerateAuthError(data, status); + EXPECT_EQ(error.state(), GoogleServiceAuthError::SERVICE_UNAVAILABLE); +} + +TEST_F(GaiaAuthFetcherTest,ServiceUnavailableError) { + net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); + std::string data = "Error=ServiceUnavailable\n"; + GoogleServiceAuthError error = + GaiaAuthFetcher::GenerateOAuthLoginError(data, status); + EXPECT_EQ(error.state(), GoogleServiceAuthError::SERVICE_UNAVAILABLE); +} + +TEST_F(GaiaAuthFetcherTest, OAuthAccountDeletedError) { + net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); + std::string data = "Error=adel\n"; + GoogleServiceAuthError error = + GaiaAuthFetcher::GenerateOAuthLoginError(data, status); + EXPECT_EQ(error.state(), GoogleServiceAuthError::ACCOUNT_DELETED); +} + +TEST_F(GaiaAuthFetcherTest, OAuthAccountDisabledError) { + net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); + std::string data = "Error=adis\n"; + GoogleServiceAuthError error = + GaiaAuthFetcher::GenerateOAuthLoginError(data, status); + EXPECT_EQ(error.state(), GoogleServiceAuthError::ACCOUNT_DISABLED); +} + +TEST_F(GaiaAuthFetcherTest, OAuthBadAuthenticationError) { + net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); + std::string data = "Error=badauth\n"; + GoogleServiceAuthError error = + GaiaAuthFetcher::GenerateOAuthLoginError(data, status); + EXPECT_EQ(error.state(), GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS); +} + +TEST_F(GaiaAuthFetcherTest, OAuthServiceUnavailableError) { + net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); + std::string data = "Error=ire\n"; + GoogleServiceAuthError error = + GaiaAuthFetcher::GenerateOAuthLoginError(data, status); + EXPECT_EQ(error.state(), GoogleServiceAuthError::SERVICE_UNAVAILABLE); +} + +TEST_F(GaiaAuthFetcherTest, FullLogin) { + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnClientLoginSuccess(_)) + .Times(1); + + MockURLFetcherFactory<MockFetcher> factory; + + GaiaAuthFetcher auth(&consumer, std::string(), GetRequestContext()); + auth.StartClientLogin("username", + "password", + "service", + std::string(), + std::string(), + GaiaAuthFetcher::HostedAccountsAllowed); +} + +TEST_F(GaiaAuthFetcherTest, FullLoginFailure) { + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnClientLoginFailure(_)) + .Times(1); + + MockURLFetcherFactory<MockFetcher> factory; + factory.set_success(false); + + GaiaAuthFetcher auth(&consumer, std::string(), GetRequestContext()); + auth.StartClientLogin("username", + "password", + "service", + std::string(), + std::string(), + GaiaAuthFetcher::HostedAccountsAllowed); +} + +TEST_F(GaiaAuthFetcherTest, ClientFetchPending) { + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnClientLoginSuccess(_)) + .Times(1); + + net::TestURLFetcherFactory factory; + + GaiaAuthFetcher auth(&consumer, std::string(), GetRequestContext()); + auth.StartClientLogin("username", + "password", + "service", + std::string(), + std::string(), + GaiaAuthFetcher::HostedAccountsAllowed); + + EXPECT_TRUE(auth.HasPendingFetch()); + MockFetcher mock_fetcher( + client_login_source_, + net::URLRequestStatus(net::URLRequestStatus::SUCCESS, 0), + net::HTTP_OK, cookies_, "SID=sid\nLSID=lsid\nAuth=auth\n", + net::URLFetcher::GET, &auth); + auth.OnURLFetchComplete(&mock_fetcher); + EXPECT_FALSE(auth.HasPendingFetch()); +} + +TEST_F(GaiaAuthFetcherTest, FullTokenSuccess) { + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnIssueAuthTokenSuccess("service", "token")) + .Times(1); + + net::TestURLFetcherFactory factory; + GaiaAuthFetcher auth(&consumer, std::string(), GetRequestContext()); + auth.StartIssueAuthToken("sid", "lsid", "service"); + + EXPECT_TRUE(auth.HasPendingFetch()); + MockFetcher mock_fetcher( + issue_auth_token_source_, + net::URLRequestStatus(net::URLRequestStatus::SUCCESS, 0), + net::HTTP_OK, cookies_, "token", + net::URLFetcher::GET, &auth); + auth.OnURLFetchComplete(&mock_fetcher); + EXPECT_FALSE(auth.HasPendingFetch()); +} + +TEST_F(GaiaAuthFetcherTest, FullTokenFailure) { + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnIssueAuthTokenFailure("service", _)) + .Times(1); + + net::TestURLFetcherFactory factory; + + GaiaAuthFetcher auth(&consumer, std::string(), GetRequestContext()); + auth.StartIssueAuthToken("sid", "lsid", "service"); + + EXPECT_TRUE(auth.HasPendingFetch()); + MockFetcher mock_fetcher( + issue_auth_token_source_, + net::URLRequestStatus(net::URLRequestStatus::SUCCESS, 0), + net::HTTP_FORBIDDEN, + cookies_, + std::string(), + net::URLFetcher::GET, + &auth); + auth.OnURLFetchComplete(&mock_fetcher); + EXPECT_FALSE(auth.HasPendingFetch()); +} + +TEST_F(GaiaAuthFetcherTest, OAuthLoginTokenSuccess) { + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnClientOAuthSuccess( + GaiaAuthConsumer::ClientOAuthResult("rt1", "at1", 3600))).Times(1); + + net::TestURLFetcherFactory factory; + GaiaAuthFetcher auth(&consumer, std::string(), GetRequestContext()); + auth.StartLsoForOAuthLoginTokenExchange("lso_token"); + net::TestURLFetcher* fetcher = factory.GetFetcherByID(0); + EXPECT_TRUE(NULL != fetcher); + EXPECT_EQ(net::LOAD_DO_NOT_SEND_COOKIES | net::LOAD_DO_NOT_SAVE_COOKIES, + fetcher->GetLoadFlags()); + + net::ResponseCookies cookies; + cookies.push_back(kGetAuthCodeValidCookie); + EXPECT_TRUE(auth.HasPendingFetch()); + MockFetcher mock_fetcher1( + client_login_to_oauth2_source_, + net::URLRequestStatus(net::URLRequestStatus::SUCCESS, 0), + net::HTTP_OK, + cookies, + std::string(), + net::URLFetcher::POST, + &auth); + auth.OnURLFetchComplete(&mock_fetcher1); + EXPECT_TRUE(auth.HasPendingFetch()); + MockFetcher mock_fetcher2( + oauth2_token_source_, + net::URLRequestStatus(net::URLRequestStatus::SUCCESS, 0), + net::HTTP_OK, cookies_, kGetTokenPairValidResponse, + net::URLFetcher::POST, &auth); + auth.OnURLFetchComplete(&mock_fetcher2); + EXPECT_FALSE(auth.HasPendingFetch()); +} + +TEST_F(GaiaAuthFetcherTest, OAuthLoginTokenWithCookies) { + MockGaiaConsumer consumer; + net::TestURLFetcherFactory factory; + GaiaAuthFetcher auth(&consumer, std::string(), GetRequestContext()); + auth.StartCookieForOAuthLoginTokenExchange("0"); + net::TestURLFetcher* fetcher = factory.GetFetcherByID(0); + EXPECT_TRUE(NULL != fetcher); + EXPECT_EQ(net::LOAD_NORMAL, fetcher->GetLoadFlags()); +} + +TEST_F(GaiaAuthFetcherTest, OAuthLoginTokenClientLoginToOAuth2Failure) { + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnClientOAuthFailure(_)) + .Times(1); + + net::TestURLFetcherFactory factory; + GaiaAuthFetcher auth(&consumer, std::string(), GetRequestContext()); + auth.StartLsoForOAuthLoginTokenExchange("lso_token"); + + net::ResponseCookies cookies; + EXPECT_TRUE(auth.HasPendingFetch()); + MockFetcher mock_fetcher( + client_login_to_oauth2_source_, + net::URLRequestStatus(net::URLRequestStatus::SUCCESS, 0), + net::HTTP_FORBIDDEN, + cookies, + std::string(), + net::URLFetcher::POST, + &auth); + auth.OnURLFetchComplete(&mock_fetcher); + EXPECT_FALSE(auth.HasPendingFetch()); +} + +TEST_F(GaiaAuthFetcherTest, OAuthLoginTokenOAuth2TokenPairFailure) { + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnClientOAuthFailure(_)) + .Times(1); + + net::TestURLFetcherFactory factory; + GaiaAuthFetcher auth(&consumer, std::string(), GetRequestContext()); + auth.StartLsoForOAuthLoginTokenExchange("lso_token"); + + net::ResponseCookies cookies; + cookies.push_back(kGetAuthCodeValidCookie); + EXPECT_TRUE(auth.HasPendingFetch()); + MockFetcher mock_fetcher1( + client_login_to_oauth2_source_, + net::URLRequestStatus(net::URLRequestStatus::SUCCESS, 0), + net::HTTP_OK, + cookies, + std::string(), + net::URLFetcher::POST, + &auth); + auth.OnURLFetchComplete(&mock_fetcher1); + EXPECT_TRUE(auth.HasPendingFetch()); + MockFetcher mock_fetcher2( + oauth2_token_source_, + net::URLRequestStatus(net::URLRequestStatus::SUCCESS, 0), + net::HTTP_FORBIDDEN, + cookies_, + std::string(), + net::URLFetcher::POST, + &auth); + auth.OnURLFetchComplete(&mock_fetcher2); + EXPECT_FALSE(auth.HasPendingFetch()); +} + +TEST_F(GaiaAuthFetcherTest, MergeSessionSuccess) { + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnMergeSessionSuccess("<html></html>")) + .Times(1); + + net::TestURLFetcherFactory factory; + + GaiaAuthFetcher auth(&consumer, std::string(), GetRequestContext()); + auth.StartMergeSession("myubertoken"); + + EXPECT_TRUE(auth.HasPendingFetch()); + MockFetcher mock_fetcher( + merge_session_source_, + net::URLRequestStatus(net::URLRequestStatus::SUCCESS, 0), + net::HTTP_OK, cookies_, "<html></html>", net::URLFetcher::GET, + &auth); + auth.OnURLFetchComplete(&mock_fetcher); + EXPECT_FALSE(auth.HasPendingFetch()); +} + +TEST_F(GaiaAuthFetcherTest, MergeSessionSuccessRedirect) { + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnMergeSessionSuccess("<html></html>")) + .Times(1); + + net::TestURLFetcherFactory factory; + + GaiaAuthFetcher auth(&consumer, std::string(), GetRequestContext()); + auth.StartMergeSession("myubertoken"); + + // Make sure the fetcher created has the expected flags. Set its url() + // properties to reflect a redirect. + net::TestURLFetcher* test_fetcher = factory.GetFetcherByID(0); + EXPECT_TRUE(test_fetcher != NULL); + EXPECT_TRUE(test_fetcher->GetLoadFlags() == net::LOAD_NORMAL); + EXPECT_TRUE(auth.HasPendingFetch()); + + GURL final_url("http://www.google.com/CheckCookie"); + test_fetcher->set_url(final_url); + test_fetcher->set_status( + net::URLRequestStatus(net::URLRequestStatus::SUCCESS, 0)); + test_fetcher->set_response_code(net::HTTP_OK); + test_fetcher->set_cookies(cookies_); + test_fetcher->SetResponseString("<html></html>"); + + auth.OnURLFetchComplete(test_fetcher); + EXPECT_FALSE(auth.HasPendingFetch()); +} + +TEST_F(GaiaAuthFetcherTest, UberAuthTokenSuccess) { + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnUberAuthTokenSuccess("uberToken")) + .Times(1); + + net::TestURLFetcherFactory factory; + + GaiaAuthFetcher auth(&consumer, std::string(), GetRequestContext()); + auth.StartTokenFetchForUberAuthExchange("myAccessToken"); + + EXPECT_TRUE(auth.HasPendingFetch()); + MockFetcher mock_fetcher( + uberauth_token_source_, + net::URLRequestStatus(net::URLRequestStatus::SUCCESS, 0), + net::HTTP_OK, cookies_, "uberToken", net::URLFetcher::POST, + &auth); + auth.OnURLFetchComplete(&mock_fetcher); + EXPECT_FALSE(auth.HasPendingFetch()); +} + +TEST_F(GaiaAuthFetcherTest, ParseClientLoginToOAuth2Response) { + { // No cookies. + std::string auth_code; + net::ResponseCookies cookies; + EXPECT_FALSE(GaiaAuthFetcher::ParseClientLoginToOAuth2Response( + cookies, &auth_code)); + EXPECT_EQ("", auth_code); + } + { // Few cookies, nothing appropriate. + std::string auth_code; + net::ResponseCookies cookies; + cookies.push_back(kGetAuthCodeCookieNoSecure); + cookies.push_back(kGetAuthCodeCookieNoHttpOnly); + cookies.push_back(kGetAuthCodeCookieNoOAuthCode); + EXPECT_FALSE(GaiaAuthFetcher::ParseClientLoginToOAuth2Response( + cookies, &auth_code)); + EXPECT_EQ("", auth_code); + } + { // Few cookies, one of them is valid. + std::string auth_code; + net::ResponseCookies cookies; + cookies.push_back(kGetAuthCodeCookieNoSecure); + cookies.push_back(kGetAuthCodeCookieNoHttpOnly); + cookies.push_back(kGetAuthCodeCookieNoOAuthCode); + cookies.push_back(kGetAuthCodeValidCookie); + EXPECT_TRUE(GaiaAuthFetcher::ParseClientLoginToOAuth2Response( + cookies, &auth_code)); + EXPECT_EQ("test-code", auth_code); + } + { // Single valid cookie (like in real responses). + std::string auth_code; + net::ResponseCookies cookies; + cookies.push_back(kGetAuthCodeValidCookie); + EXPECT_TRUE(GaiaAuthFetcher::ParseClientLoginToOAuth2Response( + cookies, &auth_code)); + EXPECT_EQ("test-code", auth_code); + } +} + +TEST_F(GaiaAuthFetcherTest, StartOAuthLogin) { + // OAuthLogin returns the same as the ClientLogin endpoint, minus CAPTCHA + // responses. + std::string data("SID=sid\nLSID=lsid\nAuth=auth\n"); + + GaiaAuthConsumer::ClientLoginResult result; + result.lsid = "lsid"; + result.sid = "sid"; + result.token = "auth"; + result.data = data; + + MockGaiaConsumer consumer; + EXPECT_CALL(consumer, OnClientLoginSuccess(result)) + .Times(1); + + GaiaAuthFetcher auth(&consumer, std::string(), GetRequestContext()); + net::URLRequestStatus status(net::URLRequestStatus::SUCCESS, 0); + MockFetcher mock_fetcher( + oauth_login_gurl_, status, net::HTTP_OK, cookies_, data, + net::URLFetcher::GET, &auth); + auth.OnURLFetchComplete(&mock_fetcher); +} diff --git a/chromium/google_apis/gaia/gaia_auth_util.cc b/chromium/google_apis/gaia/gaia_auth_util.cc new file mode 100644 index 00000000000..a8cad86408c --- /dev/null +++ b/chromium/google_apis/gaia/gaia_auth_util.cc @@ -0,0 +1,75 @@ +// Copyright (c) 2012 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 "google_apis/gaia/gaia_auth_util.h" + +#include <vector> + +#include "base/logging.h" +#include "base/strings/string_split.h" +#include "base/strings/string_util.h" +#include "google_apis/gaia/gaia_urls.h" +#include "url/gurl.h" + +namespace gaia { + +namespace { +const char kGmailDomain[] = "gmail.com"; +} + +std::string CanonicalizeEmail(const std::string& email_address) { + std::vector<std::string> parts; + char at = '@'; + base::SplitString(email_address, at, &parts); + if (parts.size() != 2U) + NOTREACHED() << "expecting exactly one @, but got " << parts.size(); + else if (parts[1] == kGmailDomain) // only strip '.' for gmail accounts. + RemoveChars(parts[0], ".", &parts[0]); + std::string new_email = StringToLowerASCII(JoinString(parts, at)); + VLOG(1) << "Canonicalized " << email_address << " to " << new_email; + return new_email; +} + +std::string CanonicalizeDomain(const std::string& domain) { + // Canonicalization of domain names means lower-casing them. Make sure to + // update this function in sync with Canonicalize if this ever changes. + return StringToLowerASCII(domain); +} + +std::string SanitizeEmail(const std::string& email_address) { + std::string sanitized(email_address); + + // Apply a default domain if necessary. + if (sanitized.find('@') == std::string::npos) { + sanitized += '@'; + sanitized += kGmailDomain; + } + + return sanitized; +} + +bool AreEmailsSame(const std::string& email1, const std::string& email2) { + return gaia::CanonicalizeEmail(gaia::SanitizeEmail(email1)) == + gaia::CanonicalizeEmail(gaia::SanitizeEmail(email2)); +} + +std::string ExtractDomainName(const std::string& email_address) { + // First canonicalize which will also verify we have proper domain part. + std::string email = CanonicalizeEmail(email_address); + size_t separator_pos = email.find('@'); + if (separator_pos != email.npos && separator_pos < email.length() - 1) + return email.substr(separator_pos + 1); + else + NOTREACHED() << "Not a proper email address: " << email; + return std::string(); +} + +bool IsGaiaSignonRealm(const GURL& url) { + if (!url.SchemeIsSecure()) + return false; + + return url == GaiaUrls::GetInstance()->gaia_url(); +} + +} // namespace gaia diff --git a/chromium/google_apis/gaia/gaia_auth_util.h b/chromium/google_apis/gaia/gaia_auth_util.h new file mode 100644 index 00000000000..35e834b3629 --- /dev/null +++ b/chromium/google_apis/gaia/gaia_auth_util.h @@ -0,0 +1,39 @@ +// Copyright (c) 2012 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 GOOGLE_APIS_GAIA_GAIA_AUTH_UTIL_H_ +#define GOOGLE_APIS_GAIA_GAIA_AUTH_UTIL_H_ + +#include <string> + +class GURL; + +namespace gaia { + +// Perform basic canonicalization of |email_address|, taking into account that +// gmail does not consider '.' or caps inside a username to matter. It also +// ignores everything after a '+'. For example, c.masone+abc@gmail.com == +// cMaSone@gmail.com, per +// http://mail.google.com/support/bin/answer.py?hl=en&ctx=mail&answer=10313# +std::string CanonicalizeEmail(const std::string& email_address); + +// Returns the canonical form of the given domain. +std::string CanonicalizeDomain(const std::string& domain); + +// Sanitize emails. Currently, it only ensures all emails have a domain by +// adding gmail.com if no domain is present. +std::string SanitizeEmail(const std::string& email_address); + +// Returns true if the two specified email addresses are the same. Both +// addresses are first sanitized and then canoncialized before comparing. +bool AreEmailsSame(const std::string& email1, const std::string& email2); + +// Extract the domain part from the canonical form of the given email. +std::string ExtractDomainName(const std::string& email); + +bool IsGaiaSignonRealm(const GURL& url); + +} // namespace gaia + +#endif // GOOGLE_APIS_GAIA_GAIA_AUTH_UTIL_H_ diff --git a/chromium/google_apis/gaia/gaia_auth_util_unittest.cc b/chromium/google_apis/gaia/gaia_auth_util_unittest.cc new file mode 100644 index 00000000000..4fe58a3bba4 --- /dev/null +++ b/chromium/google_apis/gaia/gaia_auth_util_unittest.cc @@ -0,0 +1,111 @@ +// Copyright (c) 2012 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 "google_apis/gaia/gaia_auth_util.h" + +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +namespace gaia { + +TEST(GaiaAuthUtilTest, EmailAddressNoOp) { + const char lower_case[] = "user@what.com"; + EXPECT_EQ(lower_case, CanonicalizeEmail(lower_case)); +} + +TEST(GaiaAuthUtilTest, EmailAddressIgnoreCaps) { + EXPECT_EQ(CanonicalizeEmail("user@what.com"), + CanonicalizeEmail("UsEr@what.com")); +} + +TEST(GaiaAuthUtilTest, EmailAddressIgnoreDomainCaps) { + EXPECT_EQ(CanonicalizeEmail("user@what.com"), + CanonicalizeEmail("UsEr@what.COM")); +} + +TEST(GaiaAuthUtilTest, EmailAddressRejectOneUsernameDot) { + EXPECT_NE(CanonicalizeEmail("u.ser@what.com"), + CanonicalizeEmail("UsEr@what.com")); +} + +TEST(GaiaAuthUtilTest, EmailAddressMatchWithOneUsernameDot) { + EXPECT_EQ(CanonicalizeEmail("u.ser@what.com"), + CanonicalizeEmail("U.sEr@what.com")); +} + +TEST(GaiaAuthUtilTest, EmailAddressIgnoreOneUsernameDot) { + EXPECT_EQ(CanonicalizeEmail("us.er@gmail.com"), + CanonicalizeEmail("UsEr@gmail.com")); +} + +TEST(GaiaAuthUtilTest, EmailAddressIgnoreManyUsernameDots) { + EXPECT_EQ(CanonicalizeEmail("u.ser@gmail.com"), + CanonicalizeEmail("Us.E.r@gmail.com")); +} + +TEST(GaiaAuthUtilTest, EmailAddressIgnoreConsecutiveUsernameDots) { + EXPECT_EQ(CanonicalizeEmail("use.r@gmail.com"), + CanonicalizeEmail("Us....E.r@gmail.com")); +} + +TEST(GaiaAuthUtilTest, EmailAddressDifferentOnesRejected) { + EXPECT_NE(CanonicalizeEmail("who@what.com"), + CanonicalizeEmail("Us....E.r@what.com")); +} + +TEST(GaiaAuthUtilTest, EmailAddressIgnorePlusSuffix) { + const char with_plus[] = "user+cc@what.com"; + EXPECT_EQ(with_plus, CanonicalizeEmail(with_plus)); +} + +TEST(GaiaAuthUtilTest, EmailAddressIgnoreMultiPlusSuffix) { + const char multi_plus[] = "user+cc+bcc@what.com"; + EXPECT_EQ(multi_plus, CanonicalizeEmail(multi_plus)); +} + +TEST(GaiaAuthUtilTest, CanonicalizeDomain) { + const char domain[] = "example.com"; + EXPECT_EQ(domain, CanonicalizeDomain("example.com")); + EXPECT_EQ(domain, CanonicalizeDomain("EXAMPLE.cOm")); +} + +TEST(GaiaAuthUtilTest, ExtractDomainName) { + const char domain[] = "example.com"; + EXPECT_EQ(domain, ExtractDomainName("who@example.com")); + EXPECT_EQ(domain, ExtractDomainName("who@EXAMPLE.cOm")); +} + +TEST(GaiaAuthUtilTest, SanitizeMissingDomain) { + EXPECT_EQ("nodomain@gmail.com", SanitizeEmail("nodomain")); +} + +TEST(GaiaAuthUtilTest, SanitizeExistingDomain) { + const char existing[] = "test@example.com"; + EXPECT_EQ(existing, SanitizeEmail(existing)); +} + +TEST(GaiaAuthUtilTest, AreEmailsSame) { + EXPECT_TRUE(AreEmailsSame("foo", "foo")); + EXPECT_TRUE(AreEmailsSame("foo", "foo@gmail.com")); + EXPECT_TRUE(AreEmailsSame("foo@gmail.com", "Foo@Gmail.com")); + EXPECT_FALSE(AreEmailsSame("foo@gmail.com", "foo@othermail.com")); + EXPECT_FALSE(AreEmailsSame("user@gmail.com", "foo@gmail.com")); +} + +TEST(GaiaAuthUtilTest, IsGaiaSignonRealm) { + // Only https versions of Gaia URLs should be considered valid. + EXPECT_TRUE(IsGaiaSignonRealm(GURL("https://accounts.google.com/"))); + EXPECT_FALSE(IsGaiaSignonRealm(GURL("http://accounts.google.com/"))); + + // Other Google URLs are not valid. + EXPECT_FALSE(IsGaiaSignonRealm(GURL("https://www.google.com/"))); + EXPECT_FALSE(IsGaiaSignonRealm(GURL("http://www.google.com/"))); + EXPECT_FALSE(IsGaiaSignonRealm(GURL("https://google.com/"))); + EXPECT_FALSE(IsGaiaSignonRealm(GURL("https://mail.google.com/"))); + + // Other https URLs are not valid. + EXPECT_FALSE(IsGaiaSignonRealm(GURL("https://www.example.com/"))); +} + +} // namespace gaia diff --git a/chromium/google_apis/gaia/gaia_constants.cc b/chromium/google_apis/gaia/gaia_constants.cc new file mode 100644 index 00000000000..d1d932fb037 --- /dev/null +++ b/chromium/google_apis/gaia/gaia_constants.cc @@ -0,0 +1,65 @@ +// Copyright (c) 2012 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. +// +// Constants definitions + +#include "google_apis/gaia/gaia_constants.h" + +namespace GaiaConstants { + +// Gaia uses this for accounting where login is coming from. +const char kChromeOSSource[] = "chromeos"; +const char kChromeSource[] = "ChromiumBrowser"; + +// Service name for Gaia. Used to convert to cookie auth. +const char kGaiaService[] = "gaia"; +// Service name for Picasa API. API is used to get user's image. +const char kPicasaService[] = "lh2"; + +// Service/scope names for sync. +const char kSyncService[] = "chromiumsync"; + +// Service name for remoting. +const char kRemotingService[] = "chromoting"; + +// Service/scope names for device management (cloud-based policy) server. +const char kDeviceManagementServiceOAuth[] = + "https://www.googleapis.com/auth/chromeosdevicemanagement"; + +// OAuth2 scope for access to all Google APIs. +const char kAnyApiOAuth2Scope[] = "https://www.googleapis.com/auth/any-api"; + +// OAuth2 scope for access to Chrome sync APIs +const char kChromeSyncOAuth2Scope[] = + "https://www.googleapis.com/auth/chromesync"; +// OAuth2 scope for access to the Chrome Sync APIs for managed profiles. +const char kChromeSyncManagedOAuth2Scope[] = + "https://www.googleapis.com/auth/chromesync_playpen"; +// OAuth2 scope for access to Google Talk APIs (XMPP). +const char kGoogleTalkOAuth2Scope[] = + "https://www.googleapis.com/auth/googletalk"; + +// Service for LSO endpoint of Google that exposes OAuth APIs. +const char kLSOService[] = "lso"; + +// Used to mint uber auth tokens when needed. +const char kGaiaSid[] = "sid"; +const char kGaiaLsid[] = "lsid"; +const char kGaiaOAuthToken[] = "oauthToken"; +const char kGaiaOAuthSecret[] = "oauthSecret"; +const char kGaiaOAuthDuration[] = "3600"; +const char kGaiaOAuth2LoginRefreshToken[] = "oauth2LoginRefreshToken"; + +// Used to construct a channel ID for push messaging. +const char kObfuscatedGaiaId[] = "obfuscatedGaiaId"; + +// Used to build ClientOAuth requests. These are the names of keys used when +// building base::DictionaryValue that represent the json data that makes up +// the ClientOAuth endpoint protocol. The comment above each constant explains +// what value is associated with that key. + +// Canonical email of the account to sign in. +const char kClientOAuthEmailKey[] = "email"; + +} // namespace GaiaConstants diff --git a/chromium/google_apis/gaia/gaia_constants.h b/chromium/google_apis/gaia/gaia_constants.h new file mode 100644 index 00000000000..72cae741b9b --- /dev/null +++ b/chromium/google_apis/gaia/gaia_constants.h @@ -0,0 +1,44 @@ +// Copyright (c) 2012 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. +// +// Constants used by IssueAuthToken and ClientLogin + +#ifndef GOOGLE_APIS_GAIA_GAIA_CONSTANTS_H_ +#define GOOGLE_APIS_GAIA_GAIA_CONSTANTS_H_ + +namespace GaiaConstants { + +// Gaia sources for accounting +extern const char kChromeOSSource[]; +extern const char kChromeSource[]; + +// Gaia services for requesting +extern const char kGaiaService[]; // uber token +extern const char kPicasaService[]; +extern const char kSyncService[]; +extern const char kRemotingService[]; +extern const char kDeviceManagementServiceOAuth[]; +extern const char kAnyApiOAuth2Scope[]; +extern const char kChromeSyncOAuth2Scope[]; +extern const char kChromeSyncManagedOAuth2Scope[]; +extern const char kGoogleTalkOAuth2Scope[]; +extern const char kLSOService[]; + +// Used with uber auth tokens when needed. +extern const char kGaiaSid[]; +extern const char kGaiaLsid[]; +extern const char kGaiaOAuthToken[]; +extern const char kGaiaOAuthSecret[]; +extern const char kGaiaOAuthDuration[]; +extern const char kGaiaOAuth2LoginRefreshToken[]; + +// Used to construct a channel ID for push messaging. +extern const char kObfuscatedGaiaId[]; + +// Used by wallet sign in helper. +extern const char kClientOAuthEmailKey[]; + +} // namespace GaiaConstants + +#endif // GOOGLE_APIS_GAIA_GAIA_CONSTANTS_H_ diff --git a/chromium/google_apis/gaia/gaia_oauth_client.cc b/chromium/google_apis/gaia/gaia_oauth_client.cc new file mode 100644 index 00000000000..86064d9bc62 --- /dev/null +++ b/chromium/google_apis/gaia/gaia_oauth_client.cc @@ -0,0 +1,337 @@ +// Copyright (c) 2012 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 "google_apis/gaia/gaia_oauth_client.h" + +#include "base/json/json_reader.h" +#include "base/logging.h" +#include "base/memory/scoped_ptr.h" +#include "base/strings/string_util.h" +#include "base/values.h" +#include "google_apis/gaia/gaia_urls.h" +#include "net/base/escape.h" +#include "net/http/http_status_code.h" +#include "net/url_request/url_fetcher.h" +#include "net/url_request/url_fetcher_delegate.h" +#include "net/url_request/url_request_context_getter.h" +#include "url/gurl.h" + +namespace { +const char kAccessTokenValue[] = "access_token"; +const char kRefreshTokenValue[] = "refresh_token"; +const char kExpiresInValue[] = "expires_in"; +} + +namespace gaia { + +// Use a non-zero number, so unit tests can differentiate the URLFetcher used by +// this class from other fetchers (most other code just hardcodes the ID to 0). +const int GaiaOAuthClient::kUrlFetcherId = 17109006; + +class GaiaOAuthClient::Core + : public base::RefCountedThreadSafe<GaiaOAuthClient::Core>, + public net::URLFetcherDelegate { + public: + Core(net::URLRequestContextGetter* request_context_getter) + : num_retries_(0), + request_context_getter_(request_context_getter), + delegate_(NULL), + request_type_(NO_PENDING_REQUEST) { + } + + void GetTokensFromAuthCode(const OAuthClientInfo& oauth_client_info, + const std::string& auth_code, + int max_retries, + GaiaOAuthClient::Delegate* delegate); + void RefreshToken(const OAuthClientInfo& oauth_client_info, + const std::string& refresh_token, + const std::vector<std::string>& scopes, + int max_retries, + GaiaOAuthClient::Delegate* delegate); + void GetUserEmail(const std::string& oauth_access_token, + int max_retries, + Delegate* delegate); + void GetTokenInfo(const std::string& oauth_access_token, + int max_retries, + Delegate* delegate); + + // net::URLFetcherDelegate implementation. + virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE; + + private: + friend class base::RefCountedThreadSafe<Core>; + + enum RequestType { + NO_PENDING_REQUEST, + TOKENS_FROM_AUTH_CODE, + REFRESH_TOKEN, + TOKEN_INFO, + USER_INFO, + }; + + virtual ~Core() {} + + void MakeGaiaRequest(const GURL& url, + const std::string& post_body, + int max_retries, + GaiaOAuthClient::Delegate* delegate); + void HandleResponse(const net::URLFetcher* source, + bool* should_retry_request); + + int num_retries_; + scoped_refptr<net::URLRequestContextGetter> request_context_getter_; + GaiaOAuthClient::Delegate* delegate_; + scoped_ptr<net::URLFetcher> request_; + RequestType request_type_; +}; + +void GaiaOAuthClient::Core::GetTokensFromAuthCode( + const OAuthClientInfo& oauth_client_info, + const std::string& auth_code, + int max_retries, + GaiaOAuthClient::Delegate* delegate) { + DCHECK_EQ(request_type_, NO_PENDING_REQUEST); + request_type_ = TOKENS_FROM_AUTH_CODE; + std::string post_body = + "code=" + net::EscapeUrlEncodedData(auth_code, true) + + "&client_id=" + net::EscapeUrlEncodedData(oauth_client_info.client_id, + true) + + "&client_secret=" + + net::EscapeUrlEncodedData(oauth_client_info.client_secret, true) + + "&redirect_uri=" + + net::EscapeUrlEncodedData(oauth_client_info.redirect_uri, true) + + "&grant_type=authorization_code"; + MakeGaiaRequest(GURL(GaiaUrls::GetInstance()->oauth2_token_url()), + post_body, max_retries, delegate); +} + +void GaiaOAuthClient::Core::RefreshToken( + const OAuthClientInfo& oauth_client_info, + const std::string& refresh_token, + const std::vector<std::string>& scopes, + int max_retries, + GaiaOAuthClient::Delegate* delegate) { + DCHECK_EQ(request_type_, NO_PENDING_REQUEST); + request_type_ = REFRESH_TOKEN; + std::string post_body = + "refresh_token=" + net::EscapeUrlEncodedData(refresh_token, true) + + "&client_id=" + net::EscapeUrlEncodedData(oauth_client_info.client_id, + true) + + "&client_secret=" + + net::EscapeUrlEncodedData(oauth_client_info.client_secret, true) + + "&grant_type=refresh_token"; + + if (!scopes.empty()) { + std::string scopes_string = JoinString(scopes, ' '); + post_body += "&scope=" + net::EscapeUrlEncodedData(scopes_string, true); + } + + MakeGaiaRequest(GURL(GaiaUrls::GetInstance()->oauth2_token_url()), + post_body, max_retries, delegate); +} + +void GaiaOAuthClient::Core::GetUserEmail(const std::string& oauth_access_token, + int max_retries, + Delegate* delegate) { + DCHECK_EQ(request_type_, NO_PENDING_REQUEST); + DCHECK(!request_.get()); + request_type_ = USER_INFO; + delegate_ = delegate; + num_retries_ = 0; + request_.reset(net::URLFetcher::Create( + kUrlFetcherId, GURL(GaiaUrls::GetInstance()->oauth_user_info_url()), + net::URLFetcher::GET, this)); + request_->SetRequestContext(request_context_getter_.get()); + request_->AddExtraRequestHeader("Authorization: OAuth " + oauth_access_token); + request_->SetMaxRetriesOn5xx(max_retries); + // Fetchers are sometimes cancelled because a network change was detected, + // especially at startup and after sign-in on ChromeOS. Retrying once should + // be enough in those cases; let the fetcher retry up to 3 times just in case. + // http://crbug.com/163710 + request_->SetAutomaticallyRetryOnNetworkChanges(3); + request_->Start(); +} + +void GaiaOAuthClient::Core::GetTokenInfo(const std::string& oauth_access_token, + int max_retries, + Delegate* delegate) { + DCHECK_EQ(request_type_, NO_PENDING_REQUEST); + DCHECK(!request_.get()); + request_type_ = TOKEN_INFO; + std::string post_body = + "access_token=" + net::EscapeUrlEncodedData(oauth_access_token, true); + MakeGaiaRequest(GURL(GaiaUrls::GetInstance()->oauth2_token_info_url()), + post_body, + max_retries, + delegate); +} + +void GaiaOAuthClient::Core::MakeGaiaRequest( + const GURL& url, + const std::string& post_body, + int max_retries, + GaiaOAuthClient::Delegate* delegate) { + DCHECK(!request_.get()) << "Tried to fetch two things at once!"; + delegate_ = delegate; + num_retries_ = 0; + request_.reset(net::URLFetcher::Create( + kUrlFetcherId, url, net::URLFetcher::POST, this)); + request_->SetRequestContext(request_context_getter_.get()); + request_->SetUploadData("application/x-www-form-urlencoded", post_body); + request_->SetMaxRetriesOn5xx(max_retries); + // See comment on SetAutomaticallyRetryOnNetworkChanges() above. + request_->SetAutomaticallyRetryOnNetworkChanges(3); + request_->Start(); +} + +// URLFetcher::Delegate implementation. +void GaiaOAuthClient::Core::OnURLFetchComplete( + const net::URLFetcher* source) { + bool should_retry = false; + HandleResponse(source, &should_retry); + if (should_retry) { + // Explicitly call ReceivedContentWasMalformed() to ensure the current + // request gets counted as a failure for calculation of the back-off + // period. If it was already a failure by status code, this call will + // be ignored. + request_->ReceivedContentWasMalformed(); + num_retries_++; + // We must set our request_context_getter_ again because + // URLFetcher::Core::RetryOrCompleteUrlFetch resets it to NULL... + request_->SetRequestContext(request_context_getter_.get()); + request_->Start(); + } +} + +void GaiaOAuthClient::Core::HandleResponse( + const net::URLFetcher* source, + bool* should_retry_request) { + // Move ownership of the request fetcher into a local scoped_ptr which + // will be nuked when we're done handling the request, unless we need + // to retry, in which case ownership will be returned to request_. + scoped_ptr<net::URLFetcher> old_request = request_.Pass(); + DCHECK_EQ(source, old_request.get()); + + // RC_BAD_REQUEST means the arguments are invalid. No point retrying. We are + // done here. + if (source->GetResponseCode() == net::HTTP_BAD_REQUEST) { + delegate_->OnOAuthError(); + return; + } + + scoped_ptr<base::DictionaryValue> response_dict; + if (source->GetResponseCode() == net::HTTP_OK) { + std::string data; + source->GetResponseAsString(&data); + scoped_ptr<base::Value> message_value(base::JSONReader::Read(data)); + if (message_value.get() && + message_value->IsType(base::Value::TYPE_DICTIONARY)) { + response_dict.reset( + static_cast<base::DictionaryValue*>(message_value.release())); + } + } + + if (!response_dict.get()) { + // If we don't have an access token yet and the the error was not + // RC_BAD_REQUEST, we may need to retry. + if ((source->GetMaxRetriesOn5xx() != -1) && + (num_retries_ >= source->GetMaxRetriesOn5xx())) { + // Retry limit reached. Give up. + delegate_->OnNetworkError(source->GetResponseCode()); + } else { + request_ = old_request.Pass(); + *should_retry_request = true; + } + return; + } + + RequestType type = request_type_; + request_type_ = NO_PENDING_REQUEST; + + switch (type) { + case USER_INFO: { + std::string email; + response_dict->GetString("email", &email); + delegate_->OnGetUserEmailResponse(email); + break; + } + + case TOKEN_INFO: { + delegate_->OnGetTokenInfoResponse(response_dict.Pass()); + break; + } + + case TOKENS_FROM_AUTH_CODE: + case REFRESH_TOKEN: { + std::string access_token; + std::string refresh_token; + int expires_in_seconds = 0; + response_dict->GetString(kAccessTokenValue, &access_token); + response_dict->GetString(kRefreshTokenValue, &refresh_token); + response_dict->GetInteger(kExpiresInValue, &expires_in_seconds); + + if (access_token.empty()) { + delegate_->OnOAuthError(); + return; + } + + if (type == REFRESH_TOKEN) { + delegate_->OnRefreshTokenResponse(access_token, expires_in_seconds); + } else { + delegate_->OnGetTokensResponse(refresh_token, + access_token, + expires_in_seconds); + } + break; + } + + default: + NOTREACHED(); + } +} + +GaiaOAuthClient::GaiaOAuthClient(net::URLRequestContextGetter* context_getter) { + core_ = new Core(context_getter); +} + +GaiaOAuthClient::~GaiaOAuthClient() { +} + +void GaiaOAuthClient::GetTokensFromAuthCode( + const OAuthClientInfo& oauth_client_info, + const std::string& auth_code, + int max_retries, + Delegate* delegate) { + return core_->GetTokensFromAuthCode(oauth_client_info, + auth_code, + max_retries, + delegate); +} + +void GaiaOAuthClient::RefreshToken( + const OAuthClientInfo& oauth_client_info, + const std::string& refresh_token, + const std::vector<std::string>& scopes, + int max_retries, + Delegate* delegate) { + return core_->RefreshToken(oauth_client_info, + refresh_token, + scopes, + max_retries, + delegate); +} + +void GaiaOAuthClient::GetUserEmail(const std::string& access_token, + int max_retries, + Delegate* delegate) { + return core_->GetUserEmail(access_token, max_retries, delegate); +} + +void GaiaOAuthClient::GetTokenInfo(const std::string& access_token, + int max_retries, + Delegate* delegate) { + return core_->GetTokenInfo(access_token, max_retries, delegate); +} + +} // namespace gaia diff --git a/chromium/google_apis/gaia/gaia_oauth_client.h b/chromium/google_apis/gaia/gaia_oauth_client.h new file mode 100644 index 00000000000..94d33137e84 --- /dev/null +++ b/chromium/google_apis/gaia/gaia_oauth_client.h @@ -0,0 +1,116 @@ +// Copyright (c) 2012 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 GOOGLE_APIS_GAIA_GAIA_OAUTH_CLIENT_H_ +#define GOOGLE_APIS_GAIA_GAIA_OAUTH_CLIENT_H_ + +#include <string> +#include <vector> + +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/message_loop/message_loop_proxy.h" +#include "base/values.h" + +namespace net { +class URLRequestContextGetter; +} + +// A helper class to get and refresh OAuth2 refresh and access tokens. +// Also exposes utility methods for fetching user email and token information. +// +// Supports one request at a time; for parallel requests, create multiple +// instances. +namespace gaia { + +struct OAuthClientInfo { + std::string client_id; + std::string client_secret; + std::string redirect_uri; +}; + +class GaiaOAuthClient { + public: + const static int kUrlFetcherId; + + class Delegate { + public: + // Invoked on a successful response to the GetTokensFromAuthCode request. + virtual void OnGetTokensResponse(const std::string& refresh_token, + const std::string& access_token, + int expires_in_seconds) {} + // Invoked on a successful response to the RefreshToken request. + virtual void OnRefreshTokenResponse(const std::string& access_token, + int expires_in_seconds) {} + // Invoked on a successful response to the GetUserInfo request. + virtual void OnGetUserEmailResponse(const std::string& user_email) {} + // Invoked on a successful response to the GetTokenInfo request. + virtual void OnGetTokenInfoResponse( + scoped_ptr<DictionaryValue> token_info) {} + // Invoked when there is an OAuth error with one of the requests. + virtual void OnOAuthError() = 0; + // Invoked when there is a network error or upon receiving an invalid + // response. This is invoked when the maximum number of retries have been + // exhausted. If max_retries is -1, this is never invoked. + virtual void OnNetworkError(int response_code) = 0; + + protected: + virtual ~Delegate() {} + }; + + GaiaOAuthClient(net::URLRequestContextGetter* context_getter); + ~GaiaOAuthClient(); + + // In the below methods, |max_retries| specifies the maximum number of times + // we should retry on a network error in invalid response. This does not + // apply in the case of an OAuth error (i.e. there was something wrong with + // the input arguments). Setting |max_retries| to -1 implies infinite retries. + + // Given an OAuth2 authorization code, fetch the long-lived refresh token + // and a valid access token. After the access token expires, RefreshToken() + // can be used to fetch a fresh access token. See |max_retries| docs above. + void GetTokensFromAuthCode(const OAuthClientInfo& oauth_client_info, + const std::string& auth_code, + int max_retries, + Delegate* delegate); + + // Given a valid refresh token (usually fetched via + // |GetTokensFromAuthCode()|), fetch a fresh access token that can be used + // to authenticate an API call. If |scopes| is non-empty, then fetch an + // access token for those specific scopes (assuming the refresh token has the + // appropriate permissions). See |max_retries| docs above. + void RefreshToken(const OAuthClientInfo& oauth_client_info, + const std::string& refresh_token, + const std::vector<std::string>& scopes, + int max_retries, + Delegate* delegate); + + // Call the userinfo API, returning the user email address associated + // with the given access token. The provided access token must have + // https://www.googleapis.com/auth/userinfo.email as one of its scopes. + // See |max_retries| docs above. + void GetUserEmail(const std::string& oauth_access_token, + int max_retries, + Delegate* delegate); + + // Call the tokeninfo API, returning a dictionary of response values. The + // provided access token may have any scope, and basic results will be + // returned: issued_to, audience, scope, expires_in, access_type. In + // addition, if the https://www.googleapis.com/auth/userinfo.email scope is + // present, the email and verified_email fields will be returned. If the + // https://www.googleapis.com/auth/userinfo.profile scope is present, the + // user_id field will be returned. See |max_retries| docs above. + void GetTokenInfo(const std::string& oauth_access_token, + int max_retries, + Delegate* delegate); + + private: + // The guts of the implementation live in this class. + class Core; + scoped_refptr<Core> core_; + DISALLOW_COPY_AND_ASSIGN(GaiaOAuthClient); +}; +} + +#endif // GOOGLE_APIS_GAIA_GAIA_OAUTH_CLIENT_H_ diff --git a/chromium/google_apis/gaia/gaia_oauth_client_unittest.cc b/chromium/google_apis/gaia/gaia_oauth_client_unittest.cc new file mode 100644 index 00000000000..cdeb4831154 --- /dev/null +++ b/chromium/google_apis/gaia/gaia_oauth_client_unittest.cc @@ -0,0 +1,332 @@ +// Copyright (c) 2012 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. +// +// A complete set of unit tests for GaiaOAuthClient. + +#include <string> +#include <vector> + +#include "base/strings/string_number_conversions.h" +#include "base/values.h" +#include "google_apis/gaia/gaia_oauth_client.h" +#include "net/base/net_errors.h" +#include "net/http/http_status_code.h" +#include "net/url_request/test_url_fetcher_factory.h" +#include "net/url_request/url_fetcher_delegate.h" +#include "net/url_request/url_request_status.h" +#include "net/url_request/url_request_test_util.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +using ::testing::_; +using ::testing::Eq; +using ::testing::HasSubstr; +using ::testing::Pointee; +using ::testing::SaveArg; + +namespace { + +// Responds as though OAuth returned from the server. +class MockOAuthFetcher : public net::TestURLFetcher { + public: + MockOAuthFetcher(int response_code, + int max_failure_count, + bool complete_immediately, + const GURL& url, + const std::string& results, + net::URLFetcher::RequestType request_type, + net::URLFetcherDelegate* d) + : net::TestURLFetcher(0, url, d), + max_failure_count_(max_failure_count), + current_failure_count_(0), + complete_immediately_(complete_immediately) { + set_url(url); + set_response_code(response_code); + SetResponseString(results); + } + + virtual ~MockOAuthFetcher() { } + + virtual void Start() OVERRIDE { + if ((GetResponseCode() != net::HTTP_OK) && (max_failure_count_ != -1) && + (current_failure_count_ == max_failure_count_)) { + set_response_code(net::HTTP_OK); + } + + net::URLRequestStatus::Status code = net::URLRequestStatus::SUCCESS; + if (GetResponseCode() != net::HTTP_OK) { + code = net::URLRequestStatus::FAILED; + current_failure_count_++; + } + set_status(net::URLRequestStatus(code, 0)); + + if (complete_immediately_) + delegate()->OnURLFetchComplete(this); + } + + void Finish() { + ASSERT_FALSE(complete_immediately_); + delegate()->OnURLFetchComplete(this); + } + + private: + int max_failure_count_; + int current_failure_count_; + bool complete_immediately_; + DISALLOW_COPY_AND_ASSIGN(MockOAuthFetcher); +}; + +class MockOAuthFetcherFactory : public net::URLFetcherFactory, + public net::ScopedURLFetcherFactory { + public: + MockOAuthFetcherFactory() + : net::ScopedURLFetcherFactory(this), + response_code_(net::HTTP_OK), + complete_immediately_(true) { + } + virtual ~MockOAuthFetcherFactory() {} + virtual net::URLFetcher* CreateURLFetcher( + int id, + const GURL& url, + net::URLFetcher::RequestType request_type, + net::URLFetcherDelegate* d) OVERRIDE { + url_fetcher_ = new MockOAuthFetcher( + response_code_, + max_failure_count_, + complete_immediately_, + url, + results_, + request_type, + d); + return url_fetcher_; + } + void set_response_code(int response_code) { + response_code_ = response_code; + } + void set_max_failure_count(int count) { + max_failure_count_ = count; + } + void set_results(const std::string& results) { + results_ = results; + } + MockOAuthFetcher* get_url_fetcher() { + return url_fetcher_; + } + void set_complete_immediately(bool complete_immediately) { + complete_immediately_ = complete_immediately; + } + private: + MockOAuthFetcher* url_fetcher_; + int response_code_; + bool complete_immediately_; + int max_failure_count_; + std::string results_; + DISALLOW_COPY_AND_ASSIGN(MockOAuthFetcherFactory); +}; + +const std::string kTestAccessToken = "1/fFAGRNJru1FTz70BzhT3Zg"; +const std::string kTestRefreshToken = + "1/6BMfW9j53gdGImsixUH6kU5RsR4zwI9lUVX-tqf8JXQ"; +const std::string kTestUserEmail = "a_user@gmail.com"; +const int kTestExpiresIn = 3920; + +const std::string kDummyGetTokensResult = + "{\"access_token\":\"" + kTestAccessToken + "\"," + "\"expires_in\":" + base::IntToString(kTestExpiresIn) + "," + "\"refresh_token\":\"" + kTestRefreshToken + "\"}"; + +const std::string kDummyRefreshTokenResult = + "{\"access_token\":\"" + kTestAccessToken + "\"," + "\"expires_in\":" + base::IntToString(kTestExpiresIn) + "}"; + +const std::string kDummyUserInfoResult = + "{\"email\":\"" + kTestUserEmail + "\"}"; + +const std::string kDummyTokenInfoResult = + "{\"issued_to\": \"1234567890.apps.googleusercontent.com\"," + "\"audience\": \"1234567890.apps.googleusercontent.com\"," + "\"scope\": \"https://googleapis.com/oauth2/v2/tokeninfo\"," + "\"expires_in\":" + base::IntToString(kTestExpiresIn) + "}"; +} + +namespace gaia { + +class GaiaOAuthClientTest : public testing::Test { + protected: + virtual void SetUp() OVERRIDE { + client_info_.client_id = "test_client_id"; + client_info_.client_secret = "test_client_secret"; + client_info_.redirect_uri = "test_redirect_uri"; + }; + + protected: + net::TestURLRequestContextGetter* GetRequestContext() { + if (!request_context_getter_) { + request_context_getter_ = new net::TestURLRequestContextGetter( + message_loop_.message_loop_proxy()); + } + return request_context_getter_; + } + + base::MessageLoop message_loop_; + scoped_refptr<net::TestURLRequestContextGetter> request_context_getter_; + OAuthClientInfo client_info_; +}; + +class MockGaiaOAuthClientDelegate : public gaia::GaiaOAuthClient::Delegate { + public: + MockGaiaOAuthClientDelegate() {} + ~MockGaiaOAuthClientDelegate() {} + + MOCK_METHOD3(OnGetTokensResponse, void(const std::string& refresh_token, + const std::string& access_token, + int expires_in_seconds)); + MOCK_METHOD2(OnRefreshTokenResponse, void(const std::string& access_token, + int expires_in_seconds)); + MOCK_METHOD1(OnGetUserEmailResponse, void(const std::string& user_email)); + MOCK_METHOD0(OnOAuthError, void()); + MOCK_METHOD1(OnNetworkError, void(int response_code)); + + // gMock doesn't like methods that take or return scoped_ptr. A + // work-around is to create a mock method that takes a raw ptr, and + // override the problematic method to call through to it. + // https://groups.google.com/a/chromium.org/d/msg/chromium-dev/01sDxsJ1OYw/I_S0xCBRF2oJ + MOCK_METHOD1(OnGetTokenInfoResponsePtr, + void(const DictionaryValue* token_info)); + virtual void OnGetTokenInfoResponse(scoped_ptr<DictionaryValue> token_info) + OVERRIDE { + token_info_.reset(token_info.release()); + OnGetTokenInfoResponsePtr(token_info_.get()); + } + + private: + scoped_ptr<DictionaryValue> token_info_; + DISALLOW_COPY_AND_ASSIGN(MockGaiaOAuthClientDelegate); +}; + +TEST_F(GaiaOAuthClientTest, NetworkFailure) { + int response_code = net::HTTP_INTERNAL_SERVER_ERROR; + + MockGaiaOAuthClientDelegate delegate; + EXPECT_CALL(delegate, OnNetworkError(response_code)) + .Times(1); + + MockOAuthFetcherFactory factory; + factory.set_response_code(response_code); + factory.set_max_failure_count(4); + + GaiaOAuthClient auth(GetRequestContext()); + auth.GetTokensFromAuthCode(client_info_, "auth_code", 2, &delegate); +} + +TEST_F(GaiaOAuthClientTest, NetworkFailureRecover) { + int response_code = net::HTTP_INTERNAL_SERVER_ERROR; + + MockGaiaOAuthClientDelegate delegate; + EXPECT_CALL(delegate, OnGetTokensResponse(kTestRefreshToken, kTestAccessToken, + kTestExpiresIn)).Times(1); + + MockOAuthFetcherFactory factory; + factory.set_response_code(response_code); + factory.set_max_failure_count(4); + factory.set_results(kDummyGetTokensResult); + + GaiaOAuthClient auth(GetRequestContext()); + auth.GetTokensFromAuthCode(client_info_, "auth_code", -1, &delegate); +} + +TEST_F(GaiaOAuthClientTest, OAuthFailure) { + int response_code = net::HTTP_BAD_REQUEST; + + MockGaiaOAuthClientDelegate delegate; + EXPECT_CALL(delegate, OnOAuthError()).Times(1); + + MockOAuthFetcherFactory factory; + factory.set_response_code(response_code); + factory.set_max_failure_count(-1); + factory.set_results(kDummyGetTokensResult); + + GaiaOAuthClient auth(GetRequestContext()); + auth.GetTokensFromAuthCode(client_info_, "auth_code", -1, &delegate); +} + + +TEST_F(GaiaOAuthClientTest, GetTokensSuccess) { + MockGaiaOAuthClientDelegate delegate; + EXPECT_CALL(delegate, OnGetTokensResponse(kTestRefreshToken, kTestAccessToken, + kTestExpiresIn)).Times(1); + + MockOAuthFetcherFactory factory; + factory.set_results(kDummyGetTokensResult); + + GaiaOAuthClient auth(GetRequestContext()); + auth.GetTokensFromAuthCode(client_info_, "auth_code", -1, &delegate); +} + +TEST_F(GaiaOAuthClientTest, RefreshTokenSuccess) { + MockGaiaOAuthClientDelegate delegate; + EXPECT_CALL(delegate, OnRefreshTokenResponse(kTestAccessToken, + kTestExpiresIn)).Times(1); + + MockOAuthFetcherFactory factory; + factory.set_results(kDummyRefreshTokenResult); + factory.set_complete_immediately(false); + + GaiaOAuthClient auth(GetRequestContext()); + auth.RefreshToken(client_info_, "refresh_token", std::vector<std::string>(), + -1, &delegate); + EXPECT_THAT(factory.get_url_fetcher()->upload_data(), + Not(HasSubstr("scope"))); + factory.get_url_fetcher()->Finish(); +} + +TEST_F(GaiaOAuthClientTest, RefreshTokenDownscopingSuccess) { + MockGaiaOAuthClientDelegate delegate; + EXPECT_CALL(delegate, OnRefreshTokenResponse(kTestAccessToken, + kTestExpiresIn)).Times(1); + + MockOAuthFetcherFactory factory; + factory.set_results(kDummyRefreshTokenResult); + factory.set_complete_immediately(false); + + GaiaOAuthClient auth(GetRequestContext()); + auth.RefreshToken(client_info_, "refresh_token", + std::vector<std::string>(1, "scope4test"), -1, &delegate); + EXPECT_THAT(factory.get_url_fetcher()->upload_data(), + HasSubstr("&scope=scope4test")); + factory.get_url_fetcher()->Finish(); +} + + +TEST_F(GaiaOAuthClientTest, GetUserEmail) { + MockGaiaOAuthClientDelegate delegate; + EXPECT_CALL(delegate, OnGetUserEmailResponse(kTestUserEmail)).Times(1); + + MockOAuthFetcherFactory factory; + factory.set_results(kDummyUserInfoResult); + + GaiaOAuthClient auth(GetRequestContext()); + auth.GetUserEmail("access_token", 1, &delegate); +} + +TEST_F(GaiaOAuthClientTest, GetTokenInfo) { + const DictionaryValue* captured_result; + + MockGaiaOAuthClientDelegate delegate; + EXPECT_CALL(delegate, OnGetTokenInfoResponsePtr(_)) + .WillOnce(SaveArg<0>(&captured_result)); + + MockOAuthFetcherFactory factory; + factory.set_results(kDummyTokenInfoResult); + + GaiaOAuthClient auth(GetRequestContext()); + auth.GetTokenInfo("access_token", 1, &delegate); + + std::string issued_to; + ASSERT_TRUE(captured_result->GetString("issued_to", &issued_to)); + ASSERT_EQ("1234567890.apps.googleusercontent.com", issued_to); +} + +} // namespace gaia diff --git a/chromium/google_apis/gaia/gaia_switches.cc b/chromium/google_apis/gaia/gaia_switches.cc new file mode 100644 index 00000000000..c2430b0b737 --- /dev/null +++ b/chromium/google_apis/gaia/gaia_switches.cc @@ -0,0 +1,18 @@ +// Copyright (c) 2012 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 "google_apis/gaia/gaia_switches.h" + +namespace switches { + +const char kClientLoginToOAuth2Url[] = "client-login-to-oauth2-url"; +const char kGaiaUrl[] = "gaia-url"; +const char kGoogleApisHost[] = "google-apis-host"; +const char kLsoUrl[] = "lso-url"; +const char kOAuth1LoginScope[] = "oauth1-login-scope"; +const char kOAuth2IssueTokenUrl[] = "oauth2-issue-token-url"; +const char kOAuth2TokenUrl[] = "oauth2-token-url"; +const char kOAuthUserInfoUrl[] = "oauth-user-info-url"; + +} // namespace switches diff --git a/chromium/google_apis/gaia/gaia_switches.h b/chromium/google_apis/gaia/gaia_switches.h new file mode 100644 index 00000000000..727a35557a9 --- /dev/null +++ b/chromium/google_apis/gaia/gaia_switches.h @@ -0,0 +1,42 @@ +// Copyright (c) 2012 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 GOOGLE_APIS_GAIA_GAIA_SWITCHES_H_ +#define GOOGLE_APIS_GAIA_GAIA_SWITCHES_H_ + +namespace switches { + +// Supplies custom client login to OAuth2 URL for testing purposes. +extern const char kClientLoginToOAuth2Url[]; + +// Specifies the path for GAIA authentication URL. The default value is +// "https://accounts.google.com". +extern const char kGaiaUrl[]; + +// Specifies the backend server used for Google API calls. The https:// prefix +// and the trailing slash should be omitted. +// The default value is "www.googleapis.com". +extern const char kGoogleApisHost[]; + +// Specifies the backend server used for lso authentication calls. +// "https://accounts.google.com". +extern const char kLsoUrl[]; + +// TODO(zelidrag): Get rid of all following since all URLs should be +// controlled only with --gaia-host, --lso-host and --google-apis-host. + +// Specifies custom OAuth1 login scope for testing purposes. +extern const char kOAuth1LoginScope[]; + +// Specifies custom OAuth2 issue token URL for testing purposes. +extern const char kOAuth2IssueTokenUrl[]; + +// Specifies custom OAuth2 token URL for testing purposes. +extern const char kOAuth2TokenUrl[]; + +// Specifies custom OAuth user info URL for testing purposes. +extern const char kOAuthUserInfoUrl[]; +} // namespace switches + +#endif // GOOGLE_APIS_GAIA_GAIA_SWITCHES_H_ diff --git a/chromium/google_apis/gaia/gaia_urls.cc b/chromium/google_apis/gaia/gaia_urls.cc new file mode 100644 index 00000000000..578205cb09b --- /dev/null +++ b/chromium/google_apis/gaia/gaia_urls.cc @@ -0,0 +1,249 @@ +// Copyright (c) 2012 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 "google_apis/gaia/gaia_urls.h" + +#include "base/command_line.h" +#include "base/logging.h" +#include "google_apis/gaia/gaia_switches.h" +#include "google_apis/google_api_keys.h" + +namespace { + +// Gaia service constants +const char kDefaultGaiaUrl[] = "https://accounts.google.com"; +const char kDefaultGoogleApisBaseUrl[] = "www.googleapis.com"; +const char kCaptchaUrlPrefixSuffix[] = "/"; + +// API calls from accounts.google.com +const char kClientLoginUrlSuffix[] = "/ClientLogin"; +const char kServiceLoginUrlSuffix[] = "/ServiceLogin"; +const char kServiceLogoutUrlSuffix[] = "/Logout"; +const char kIssueAuthTokenUrlSuffix[] = "/IssueAuthToken"; +const char kGetUserInfoUrlSuffix[] = "/GetUserInfo"; +const char kTokenAuthUrlSuffix[] = "/TokenAuth"; +const char kMergeSessionUrlSuffix[] = "/MergeSession"; +const char kOAuthGetAccessTokenUrlSuffix[] = "/OAuthGetAccessToken"; +const char kOAuthWrapBridgeUrlSuffix[] = "/OAuthWrapBridge"; +const char kOAuth1LoginUrlSuffix[] = "/OAuthLogin"; +const char kOAuthRevokeTokenUrlSuffix[] = "/AuthSubRevokeToken"; + +// API calls from accounts.google.com (LSO) +const char kGetOAuthTokenUrlSuffix[] = "/o/oauth/GetOAuthToken/"; +const char kClientLoginToOAuth2UrlSuffix[] = "/o/oauth2/programmatic_auth"; +const char kOAuth2AuthUrlSuffix[] = "/o/oauth2/auth"; +const char kOAuth2RevokeUrlSuffix[] = "/o/oauth2/revoke"; +const char kOAuth2TokenUrlSuffix[] = "/o/oauth2/token"; +const char kClientOAuthUrlSuffix[] = "/ClientOAuth"; + +// API calls from www.googleapis.com +const char kOAuth2IssueTokenUrlSuffix[] = "/oauth2/v2/IssueToken"; +const char kOAuth2TokenInfoUrlSuffix[] = "/oauth2/v2/tokeninfo"; +const char kOAuthUserInfoUrlSuffix[] = "/oauth2/v1/userinfo"; +const char kOAuthWrapBridgeUserInfoScopeUrlSuffix[] = "/auth/userinfo.email"; + +const char kOAuth1LoginScope[] = + "https://www.google.com/accounts/OAuthLogin"; + +void GetSwitchValueWithDefault(const char* switch_value, + const char* default_value, + std::string* output_value) { + CommandLine* command_line = CommandLine::ForCurrentProcess(); + if (command_line->HasSwitch(switch_value)) { + *output_value = command_line->GetSwitchValueASCII(switch_value); + } else { + *output_value = default_value; + } +} + +} // namespace + +GaiaUrls* GaiaUrls::GetInstance() { + return Singleton<GaiaUrls>::get(); +} + +GaiaUrls::GaiaUrls() { + std::string gaia_url_str; + GetSwitchValueWithDefault(switches::kGaiaUrl, + kDefaultGaiaUrl, + &gaia_url_str); + gaia_url_ = GURL(gaia_url_str); + DCHECK(gaia_url_.is_valid()); + + GetSwitchValueWithDefault(switches::kLsoUrl, + kDefaultGaiaUrl, + &lso_origin_url_); + + std::string google_apis_base; + GetSwitchValueWithDefault(switches::kGoogleApisHost, + kDefaultGoogleApisBaseUrl, + &google_apis_base); + + captcha_url_prefix_ = "http://" + gaia_url_.host() + + (gaia_url_.has_port() ? ":" + gaia_url_.port() : "") + + kCaptchaUrlPrefixSuffix; + + google_apis_origin_url_ = "https://" + google_apis_base; + + oauth2_chrome_client_id_ = + google_apis::GetOAuth2ClientID(google_apis::CLIENT_MAIN); + oauth2_chrome_client_secret_ = + google_apis::GetOAuth2ClientSecret(google_apis::CLIENT_MAIN); + + // URLs from accounts.google.com. + gaia_login_form_realm_ = gaia_url_str + "/"; + client_login_url_ = gaia_url_str + kClientLoginUrlSuffix; + service_login_url_ = gaia_url_str + kServiceLoginUrlSuffix; + service_logout_url_ = gaia_url_str + kServiceLogoutUrlSuffix; + issue_auth_token_url_ = gaia_url_str + kIssueAuthTokenUrlSuffix; + get_user_info_url_ = gaia_url_str + kGetUserInfoUrlSuffix; + token_auth_url_ = gaia_url_str + kTokenAuthUrlSuffix; + merge_session_url_ = gaia_url_str + kMergeSessionUrlSuffix; + oauth_get_access_token_url_ = gaia_url_str + kOAuthGetAccessTokenUrlSuffix; + oauth_wrap_bridge_url_ = gaia_url_str + kOAuthWrapBridgeUrlSuffix; + oauth_revoke_token_url_ = gaia_url_str + kOAuthRevokeTokenUrlSuffix; + oauth1_login_url_ = gaia_url_str + kOAuth1LoginUrlSuffix; + + // URLs from accounts.google.com (LSO). + get_oauth_token_url_ = lso_origin_url_ + kGetOAuthTokenUrlSuffix; + std::string client_login_to_oauth2_url = lso_origin_url_ + + kClientLoginToOAuth2UrlSuffix; + oauth2_auth_url_ = lso_origin_url_ + kOAuth2AuthUrlSuffix; + std::string oauth2_token_url = lso_origin_url_ + kOAuth2TokenUrlSuffix; + oauth2_revoke_url_ = lso_origin_url_ + kOAuth2RevokeUrlSuffix; + + // URLs from www.googleapis.com. + oauth_wrap_bridge_user_info_scope_ = google_apis_origin_url_ + + kOAuthWrapBridgeUserInfoScopeUrlSuffix; + std::string oauth2_issue_token_url = google_apis_origin_url_ + + kOAuth2IssueTokenUrlSuffix; + oauth2_token_info_url_ = google_apis_origin_url_ + kOAuth2TokenInfoUrlSuffix; + std::string oauth_user_info_url = google_apis_origin_url_ + + kOAuthUserInfoUrlSuffix; + + // TODO(zelidrag): Get rid of all these switches since all URLs should be + // controlled only with --gaia-url, --lso-url and --google-apis-host. + GetSwitchValueWithDefault(switches::kOAuth1LoginScope, + kOAuth1LoginScope, + &oauth1_login_scope_); + GetSwitchValueWithDefault(switches::kClientLoginToOAuth2Url, + client_login_to_oauth2_url.c_str(), + &client_login_to_oauth2_url_); + GetSwitchValueWithDefault(switches::kOAuth2TokenUrl, + oauth2_token_url.c_str(), + &oauth2_token_url_); + GetSwitchValueWithDefault(switches::kOAuth2IssueTokenUrl, + oauth2_issue_token_url.c_str(), + &oauth2_issue_token_url_); + GetSwitchValueWithDefault(switches::kOAuthUserInfoUrl, + oauth_user_info_url.c_str(), + &oauth_user_info_url_); +} + +GaiaUrls::~GaiaUrls() { +} + +const std::string& GaiaUrls::captcha_url_prefix() const { + return captcha_url_prefix_; +} + +const GURL& GaiaUrls::gaia_url() const { + return gaia_url_; +} + +const std::string& GaiaUrls::client_login_url() const { + return client_login_url_; +} + +const std::string& GaiaUrls::service_login_url() const { + return service_login_url_; +} + +const std::string& GaiaUrls::service_logout_url() const { + return service_logout_url_; +} + +const std::string& GaiaUrls::issue_auth_token_url() const { + return issue_auth_token_url_; +} + +const std::string& GaiaUrls::get_user_info_url() const { + return get_user_info_url_; +} + +const std::string& GaiaUrls::token_auth_url() const { + return token_auth_url_; +} + +const std::string& GaiaUrls::merge_session_url() const { + return merge_session_url_; +} + +const std::string& GaiaUrls::get_oauth_token_url() const { + return get_oauth_token_url_; +} + +const std::string& GaiaUrls::oauth_get_access_token_url() const { + return oauth_get_access_token_url_; +} + +const std::string& GaiaUrls::oauth_wrap_bridge_url() const { + return oauth_wrap_bridge_url_; +} + +const std::string& GaiaUrls::oauth_user_info_url() const { + return oauth_user_info_url_; +} + +const std::string& GaiaUrls::oauth_revoke_token_url() const { + return oauth_revoke_token_url_; +} + +const std::string& GaiaUrls::oauth1_login_url() const { + return oauth1_login_url_; +} + +const std::string& GaiaUrls::oauth1_login_scope() const { + return oauth1_login_scope_; +} + +const std::string& GaiaUrls::oauth_wrap_bridge_user_info_scope() const { + return oauth_wrap_bridge_user_info_scope_; +} + +const std::string& GaiaUrls::oauth2_chrome_client_id() const { + return oauth2_chrome_client_id_; +} + +const std::string& GaiaUrls::oauth2_chrome_client_secret() const { + return oauth2_chrome_client_secret_; +} + +const std::string& GaiaUrls::client_login_to_oauth2_url() const { + return client_login_to_oauth2_url_; +} + +const std::string& GaiaUrls::oauth2_auth_url() const { + return oauth2_auth_url_; +} + +const std::string& GaiaUrls::oauth2_token_url() const { + return oauth2_token_url_; +} + +const std::string& GaiaUrls::oauth2_issue_token_url() const { + return oauth2_issue_token_url_; +} + +const std::string& GaiaUrls::oauth2_token_info_url() const { + return oauth2_token_info_url_; +} + +const std::string& GaiaUrls::oauth2_revoke_url() const { + return oauth2_revoke_url_; +} + +const std::string& GaiaUrls::gaia_login_form_realm() const { + return gaia_login_form_realm_; +} diff --git a/chromium/google_apis/gaia/gaia_urls.h b/chromium/google_apis/gaia/gaia_urls.h new file mode 100644 index 00000000000..28c72792f0b --- /dev/null +++ b/chromium/google_apis/gaia/gaia_urls.h @@ -0,0 +1,92 @@ +// Copyright (c) 2012 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 GOOGLE_APIS_GAIA_GAIA_URLS_H_ +#define GOOGLE_APIS_GAIA_GAIA_URLS_H_ + +#include <string> + +#include "base/memory/singleton.h" +#include "url/gurl.h" + +// A signleton that provides all the URLs that are used for connecting to GAIA. +class GaiaUrls { + public: + static GaiaUrls* GetInstance(); + + // The URLs for different calls in the Google Accounts programmatic login API. + const std::string& captcha_url_prefix() const; + + const GURL& gaia_url() const; + const std::string& client_login_url() const; + const std::string& service_login_url() const; + const std::string& service_logout_url() const; + const std::string& issue_auth_token_url() const; + const std::string& get_user_info_url() const; + const std::string& token_auth_url() const; + const std::string& merge_session_url() const; + const std::string& get_oauth_token_url() const; + const std::string& oauth_get_access_token_url() const; + const std::string& oauth_wrap_bridge_url() const; + const std::string& oauth_user_info_url() const; + const std::string& oauth_revoke_token_url() const; + const std::string& oauth1_login_url() const; + + const std::string& oauth1_login_scope() const; + const std::string& oauth_wrap_bridge_user_info_scope() const; + + const std::string& oauth2_chrome_client_id() const; + const std::string& oauth2_chrome_client_secret() const; + const std::string& client_login_to_oauth2_url() const; + const std::string& oauth2_auth_url() const; + const std::string& oauth2_token_url() const; + const std::string& oauth2_issue_token_url() const; + const std::string& oauth2_token_info_url() const; + const std::string& oauth2_revoke_url() const; + + const std::string& gaia_login_form_realm() const; + + private: + GaiaUrls(); + ~GaiaUrls(); + + friend struct DefaultSingletonTraits<GaiaUrls>; + + std::string captcha_url_prefix_; + + GURL gaia_url_; + std::string lso_origin_url_; + std::string google_apis_origin_url_; + std::string client_login_url_; + std::string service_login_url_; + std::string service_logout_url_; + std::string issue_auth_token_url_; + std::string get_user_info_url_; + std::string token_auth_url_; + std::string merge_session_url_; + std::string get_oauth_token_url_; + std::string oauth_get_access_token_url_; + std::string oauth_wrap_bridge_url_; + std::string oauth_user_info_url_; + std::string oauth_revoke_token_url_; + std::string oauth1_login_url_; + + std::string oauth1_login_scope_; + std::string oauth_wrap_bridge_user_info_scope_; + + std::string oauth2_chrome_client_id_; + std::string oauth2_chrome_client_secret_; + std::string client_login_to_oauth2_url_; + std::string oauth2_auth_url_; + std::string oauth2_token_url_; + std::string oauth2_issue_token_url_; + std::string oauth2_token_info_url_; + std::string oauth2_revoke_url_; + + std::string gaia_login_form_realm_; + + DISALLOW_COPY_AND_ASSIGN(GaiaUrls); +}; + +#endif // GOOGLE_APIS_GAIA_GAIA_URLS_H_ diff --git a/chromium/google_apis/gaia/google_service_auth_error.cc b/chromium/google_apis/gaia/google_service_auth_error.cc new file mode 100644 index 00000000000..ab3d9c07de5 --- /dev/null +++ b/chromium/google_apis/gaia/google_service_auth_error.cc @@ -0,0 +1,272 @@ +// Copyright (c) 2012 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 "google_apis/gaia/google_service_auth_error.h" + +#include <string> + +#include "base/json/json_reader.h" +#include "base/logging.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "base/values.h" +#include "net/base/net_errors.h" + +GoogleServiceAuthError::Captcha::Captcha() : image_width(0), image_height(0) { +} + +GoogleServiceAuthError::Captcha::Captcha( + const std::string& token, const GURL& audio, const GURL& img, + const GURL& unlock, int width, int height) + : token(token), audio_url(audio), image_url(img), unlock_url(unlock), + image_width(width), image_height(height) { +} + +GoogleServiceAuthError::Captcha::~Captcha() { +} + +bool GoogleServiceAuthError::Captcha::operator==(const Captcha& b) const { + return (token == b.token && + audio_url == b.audio_url && + image_url == b.image_url && + unlock_url == b.unlock_url && + image_width == b.image_width && + image_height == b.image_height); +} + +GoogleServiceAuthError::SecondFactor::SecondFactor() : field_length(0) { +} + +GoogleServiceAuthError::SecondFactor::SecondFactor( + const std::string& token, const std::string& prompt, + const std::string& alternate, int length) + : token(token), prompt_text(prompt), alternate_text(alternate), + field_length(length) { +} + +GoogleServiceAuthError::SecondFactor::~SecondFactor() { +} + +bool GoogleServiceAuthError::SecondFactor::operator==( + const SecondFactor& b) const { + return (token == b.token && + prompt_text == b.prompt_text && + alternate_text == b.alternate_text && + field_length == b.field_length); +} + +bool GoogleServiceAuthError::operator==( + const GoogleServiceAuthError& b) const { + return (state_ == b.state_ && + network_error_ == b.network_error_ && + captcha_ == b.captcha_ && + second_factor_ == b.second_factor_); +} + +GoogleServiceAuthError::GoogleServiceAuthError(State s) + : state_(s), + network_error_(0) { + // If the caller has no idea, then we just set it to a generic failure. + if (s == CONNECTION_FAILED) { + network_error_ = net::ERR_FAILED; + } +} + +GoogleServiceAuthError::GoogleServiceAuthError( + State state, + const std::string& error_message) + : state_(state), + network_error_(0), + error_message_(error_message) { +} + +GoogleServiceAuthError::GoogleServiceAuthError(const std::string& error_message) + : state_(INVALID_GAIA_CREDENTIALS), + network_error_(0), + error_message_(error_message) { +} + +// static +GoogleServiceAuthError + GoogleServiceAuthError::FromConnectionError(int error) { + return GoogleServiceAuthError(CONNECTION_FAILED, error); +} + +// static +GoogleServiceAuthError GoogleServiceAuthError::FromClientLoginCaptchaChallenge( + const std::string& captcha_token, + const GURL& captcha_image_url, + const GURL& captcha_unlock_url) { + return GoogleServiceAuthError(CAPTCHA_REQUIRED, captcha_token, GURL(), + captcha_image_url, captcha_unlock_url, 0, 0); +} + +// static +GoogleServiceAuthError GoogleServiceAuthError::FromServiceError( + const std::string& error_message) { + return GoogleServiceAuthError(SERVICE_ERROR, error_message); +} + +// static +GoogleServiceAuthError GoogleServiceAuthError::FromUnexpectedServiceResponse( + const std::string& error_message) { + return GoogleServiceAuthError(UNEXPECTED_SERVICE_RESPONSE, error_message); +} + +// static +GoogleServiceAuthError GoogleServiceAuthError::AuthErrorNone() { + return GoogleServiceAuthError(NONE); +} + +GoogleServiceAuthError::State GoogleServiceAuthError::state() const { + return state_; +} + +const GoogleServiceAuthError::Captcha& GoogleServiceAuthError::captcha() const { + return captcha_; +} + +const GoogleServiceAuthError::SecondFactor& +GoogleServiceAuthError::second_factor() const { + return second_factor_; +} + +int GoogleServiceAuthError::network_error() const { + return network_error_; +} + +const std::string& GoogleServiceAuthError::token() const { + switch (state_) { + case CAPTCHA_REQUIRED: + return captcha_.token; + break; + case TWO_FACTOR: + return second_factor_.token; + break; + default: + NOTREACHED(); + } + return EmptyString(); +} + +const std::string& GoogleServiceAuthError::error_message() const { + return error_message_; +} + +base::DictionaryValue* GoogleServiceAuthError::ToValue() const { + base::DictionaryValue* value = new base::DictionaryValue(); + std::string state_str; + switch (state_) { +#define STATE_CASE(x) case x: state_str = #x; break + STATE_CASE(NONE); + STATE_CASE(INVALID_GAIA_CREDENTIALS); + STATE_CASE(USER_NOT_SIGNED_UP); + STATE_CASE(CONNECTION_FAILED); + STATE_CASE(CAPTCHA_REQUIRED); + STATE_CASE(ACCOUNT_DELETED); + STATE_CASE(ACCOUNT_DISABLED); + STATE_CASE(SERVICE_UNAVAILABLE); + STATE_CASE(TWO_FACTOR); + STATE_CASE(REQUEST_CANCELED); + STATE_CASE(HOSTED_NOT_ALLOWED); + STATE_CASE(UNEXPECTED_SERVICE_RESPONSE); + STATE_CASE(SERVICE_ERROR); +#undef STATE_CASE + default: + NOTREACHED(); + break; + } + value->SetString("state", state_str); + if (!error_message_.empty()) { + value->SetString("errorMessage", error_message_); + } + if (state_ == CAPTCHA_REQUIRED) { + base::DictionaryValue* captcha_value = new base::DictionaryValue(); + value->Set("captcha", captcha_value); + captcha_value->SetString("token", captcha_.token); + captcha_value->SetString("audioUrl", captcha_.audio_url.spec()); + captcha_value->SetString("imageUrl", captcha_.image_url.spec()); + captcha_value->SetString("unlockUrl", captcha_.unlock_url.spec()); + captcha_value->SetInteger("imageWidth", captcha_.image_width); + captcha_value->SetInteger("imageHeight", captcha_.image_height); + } else if (state_ == CONNECTION_FAILED) { + value->SetString("networkError", net::ErrorToString(network_error_)); + } else if (state_ == TWO_FACTOR) { + base::DictionaryValue* two_factor_value = new base::DictionaryValue(); + value->Set("two_factor", two_factor_value); + two_factor_value->SetString("token", second_factor_.token); + two_factor_value->SetString("promptText", second_factor_.prompt_text); + two_factor_value->SetString("alternateText", second_factor_.alternate_text); + two_factor_value->SetInteger("fieldLength", second_factor_.field_length); + } + return value; +} + +std::string GoogleServiceAuthError::ToString() const { + switch (state_) { + case NONE: + return std::string(); + case INVALID_GAIA_CREDENTIALS: + return "Invalid credentials."; + case USER_NOT_SIGNED_UP: + return "Not authorized."; + case CONNECTION_FAILED: + return base::StringPrintf("Connection failed (%d).", network_error_); + case CAPTCHA_REQUIRED: + return base::StringPrintf("CAPTCHA required (%s).", + captcha_.token.c_str()); + case ACCOUNT_DELETED: + return "Account deleted."; + case ACCOUNT_DISABLED: + return "Account disabled."; + case SERVICE_UNAVAILABLE: + return "Service unavailable; try again later."; + case TWO_FACTOR: + return base::StringPrintf("2-step verification required (%s).", + second_factor_.token.c_str()); + case REQUEST_CANCELED: + return "Request canceled."; + case HOSTED_NOT_ALLOWED: + return "Google account required."; + case UNEXPECTED_SERVICE_RESPONSE: + return base::StringPrintf("Unexpected service response (%s)", + error_message_.c_str()); + case SERVICE_ERROR: + return base::StringPrintf("Service responded with error: '%s'", + error_message_.c_str()); + default: + NOTREACHED(); + return std::string(); + } +} + +GoogleServiceAuthError::GoogleServiceAuthError(State s, int error) + : state_(s), + network_error_(error) { +} + +GoogleServiceAuthError::GoogleServiceAuthError( + State s, + const std::string& captcha_token, + const GURL& captcha_audio_url, + const GURL& captcha_image_url, + const GURL& captcha_unlock_url, + int image_width, + int image_height) + : state_(s), + captcha_(captcha_token, captcha_audio_url, captcha_image_url, + captcha_unlock_url, image_width, image_height), + network_error_(0) { +} + +GoogleServiceAuthError::GoogleServiceAuthError( + State s, + const std::string& captcha_token, + const std::string& prompt_text, + const std::string& alternate_text, + int field_length) + : state_(s), + second_factor_(captcha_token, prompt_text, alternate_text, field_length), + network_error_(0) { +} diff --git a/chromium/google_apis/gaia/google_service_auth_error.h b/chromium/google_apis/gaia/google_service_auth_error.h new file mode 100644 index 00000000000..a99e515f31d --- /dev/null +++ b/chromium/google_apis/gaia/google_service_auth_error.h @@ -0,0 +1,213 @@ +// Copyright (c) 2012 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. + +// A GoogleServiceAuthError is immutable, plain old data representing an +// error from an attempt to authenticate with a Google service. +// It could be from Google Accounts itself, or any service using Google +// Accounts (e.g expired credentials). It may contain additional data such as +// captcha or OTP challenges. + +// A GoogleServiceAuthError without additional data is just a State, defined +// below. A case could be made to have this relation implicit, to allow raising +// error events concisely by doing OnAuthError(GoogleServiceAuthError::NONE), +// for example. But the truth is this class is ever so slightly more than a +// transparent wrapper around 'State' due to additional Captcha data +// (e.g consider operator=), and this would violate the style guide. Thus, +// you must explicitly use the constructor when all you have is a State. +// The good news is the implementation nests the enum inside a class, so you +// may forward declare and typedef GoogleServiceAuthError to something shorter +// in the comfort of your own translation unit. + +#ifndef GOOGLE_APIS_GAIA_GOOGLE_SERVICE_AUTH_ERROR_H_ +#define GOOGLE_APIS_GAIA_GOOGLE_SERVICE_AUTH_ERROR_H_ + +#include <string> + +#include "url/gurl.h" + +namespace base { +class DictionaryValue; +} + +class GoogleServiceAuthError { + public: + // + // These enumerations are referenced by integer value in HTML login code. + // Do not change the numeric values. + // + enum State { + // The user is authenticated. + NONE = 0, + + // The credentials supplied to GAIA were either invalid, or the locally + // cached credentials have expired. + INVALID_GAIA_CREDENTIALS = 1, + + // The GAIA user is not authorized to use the service. + USER_NOT_SIGNED_UP = 2, + + // Could not connect to server to verify credentials. This could be in + // response to either failure to connect to GAIA or failure to connect to + // the service needing GAIA tokens during authentication. + CONNECTION_FAILED = 3, + + // The user needs to satisfy a CAPTCHA challenge to unlock their account. + // If no other information is available, this can be resolved by visiting + // https://accounts.google.com/DisplayUnlockCaptcha. Otherwise, captcha() + // will provide details about the associated challenge. + CAPTCHA_REQUIRED = 4, + + // The user account has been deleted. + ACCOUNT_DELETED = 5, + + // The user account has been disabled. + ACCOUNT_DISABLED = 6, + + // The service is not available; try again later. + SERVICE_UNAVAILABLE = 7, + + // The password is valid but we need two factor to get a token. + TWO_FACTOR = 8, + + // The requestor of the authentication step cancelled the request + // prior to completion. + REQUEST_CANCELED = 9, + + // The user has provided a HOSTED account, when this service requires + // a GOOGLE account. + HOSTED_NOT_ALLOWED = 10, + + // Indicates the service responded to a request, but we cannot + // interpret the response. + UNEXPECTED_SERVICE_RESPONSE = 11, + + // Indicates the service responded and response carried details of the + // application error. + SERVICE_ERROR = 12, + + // The number of known error states. + NUM_STATES = 13, + }; + + // Additional data for CAPTCHA_REQUIRED errors. + struct Captcha { + Captcha(); + Captcha(const std::string& token, + const GURL& audio, + const GURL& img, + const GURL& unlock, + int width, + int height); + ~Captcha(); + // For test only. + bool operator==(const Captcha &b) const; + + std::string token; // Globally identifies the specific CAPTCHA challenge. + GURL audio_url; // The CAPTCHA audio to use instead of image. + GURL image_url; // The CAPTCHA image to show the user. + GURL unlock_url; // Pretty unlock page containing above captcha. + int image_width; // Width of captcha image. + int image_height; // Height of capture image. + }; + + // Additional data for TWO_FACTOR errors. + struct SecondFactor { + SecondFactor(); + SecondFactor(const std::string& token, + const std::string& prompt, + const std::string& alternate, + int length); + ~SecondFactor(); + // For test only. + bool operator==(const SecondFactor &b) const; + + // Globally identifies the specific second-factor challenge. + std::string token; + // Localised prompt text, eg “Enter the verification code sent to your + // phone number ending in XXX”. + std::string prompt_text; + // Localized text describing an alternate option, eg “Get a verification + // code in a text message”. + std::string alternate_text; + // Character length for the challenge field. + int field_length; + }; + + // For test only. + bool operator==(const GoogleServiceAuthError &b) const; + + // Construct a GoogleServiceAuthError from a State with no additional data. + explicit GoogleServiceAuthError(State s); + + // Construct a GoogleServiceAuthError from a network error. + // It will be created with CONNECTION_FAILED set. + static GoogleServiceAuthError FromConnectionError(int error); + + // Construct a CAPTCHA_REQUIRED error with CAPTCHA challenge data from the + // the ClientLogin endpoint. + // TODO(rogerta): once ClientLogin is no longer used, may be able to get + // rid of this function. + static GoogleServiceAuthError FromClientLoginCaptchaChallenge( + const std::string& captcha_token, + const GURL& captcha_image_url, + const GURL& captcha_unlock_url); + + // Construct a SERVICE_ERROR error, e.g. invalid client ID, with an + // |error_message| which provides more information about the service error. + static GoogleServiceAuthError FromServiceError( + const std::string& error_message); + + // Construct an UNEXPECTED_SERVICE_RESPONSE error, with an |error_message| + // detailing the problems with the response. + static GoogleServiceAuthError FromUnexpectedServiceResponse( + const std::string& error_message); + + // Provided for convenience for clients needing to reset an instance to NONE. + // (avoids err_ = GoogleServiceAuthError(GoogleServiceAuthError::NONE), due + // to explicit class and State enum relation. Note: shouldn't be inlined! + static GoogleServiceAuthError AuthErrorNone(); + + // The error information. + State state() const; + const Captcha& captcha() const; + const SecondFactor& second_factor() const; + int network_error() const; + const std::string& token() const; + const std::string& error_message() const; + + // Returns info about this object in a dictionary. Caller takes + // ownership of returned dictionary. + base::DictionaryValue* ToValue() const; + + // Returns a message describing the error. + std::string ToString() const; + + private: + GoogleServiceAuthError(State s, int error); + + // Construct a GoogleServiceAuthError from |state| and |error_message|. + GoogleServiceAuthError(State state, const std::string& error_message); + + explicit GoogleServiceAuthError(const std::string& error_message); + + GoogleServiceAuthError(State s, const std::string& captcha_token, + const GURL& captcha_audio_url, + const GURL& captcha_image_url, + const GURL& captcha_unlock_url, + int image_width, + int image_height); + + GoogleServiceAuthError(State s, const std::string& captcha_token, + const std::string& prompt_text, + const std::string& alternate_text, + int field_length); + + State state_; + Captcha captcha_; + SecondFactor second_factor_; + int network_error_; + std::string error_message_; +}; + +#endif // GOOGLE_APIS_GAIA_GOOGLE_SERVICE_AUTH_ERROR_H_ diff --git a/chromium/google_apis/gaia/google_service_auth_error_unittest.cc b/chromium/google_apis/gaia/google_service_auth_error_unittest.cc new file mode 100644 index 00000000000..d1f920ab9c2 --- /dev/null +++ b/chromium/google_apis/gaia/google_service_auth_error_unittest.cc @@ -0,0 +1,55 @@ +// Copyright (c) 2012 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 "google_apis/gaia/google_service_auth_error.h" + +#include <string> + +#include "base/memory/scoped_ptr.h" +#include "base/test/values_test_util.h" +#include "base/values.h" +#include "net/base/net_errors.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +using base::ExpectDictStringValue; + +class GoogleServiceAuthErrorTest : public testing::Test {}; + +void TestSimpleState(GoogleServiceAuthError::State state) { + GoogleServiceAuthError error(state); + scoped_ptr<DictionaryValue> value(error.ToValue()); + EXPECT_EQ(1u, value->size()); + std::string state_str; + EXPECT_TRUE(value->GetString("state", &state_str)); + EXPECT_FALSE(state_str.empty()); + EXPECT_NE("CONNECTION_FAILED", state_str); + EXPECT_NE("CAPTCHA_REQUIRED", state_str); +} + +TEST_F(GoogleServiceAuthErrorTest, SimpleToValue) { + for (int i = GoogleServiceAuthError::NONE; + i <= GoogleServiceAuthError::USER_NOT_SIGNED_UP; ++i) { + TestSimpleState(static_cast<GoogleServiceAuthError::State>(i)); + } +} + +TEST_F(GoogleServiceAuthErrorTest, None) { + GoogleServiceAuthError error(GoogleServiceAuthError::AuthErrorNone()); + scoped_ptr<DictionaryValue> value(error.ToValue()); + EXPECT_EQ(1u, value->size()); + ExpectDictStringValue("NONE", *value, "state"); +} + +TEST_F(GoogleServiceAuthErrorTest, ConnectionFailed) { + GoogleServiceAuthError error( + GoogleServiceAuthError::FromConnectionError(net::OK)); + scoped_ptr<DictionaryValue> value(error.ToValue()); + EXPECT_EQ(2u, value->size()); + ExpectDictStringValue("CONNECTION_FAILED", *value, "state"); + ExpectDictStringValue("net::OK", *value, "networkError"); +} + +} // namespace diff --git a/chromium/google_apis/gaia/mock_url_fetcher_factory.h b/chromium/google_apis/gaia/mock_url_fetcher_factory.h new file mode 100644 index 00000000000..862f220e380 --- /dev/null +++ b/chromium/google_apis/gaia/mock_url_fetcher_factory.h @@ -0,0 +1,70 @@ +// Copyright (c) 2012 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. +// +// A collection of classes that are useful when testing things that use a +// GaiaAuthFetcher. + +#ifndef GOOGLE_APIS_GAIA_MOCK_URL_FETCHER_FACTORY_H_ +#define GOOGLE_APIS_GAIA_MOCK_URL_FETCHER_FACTORY_H_ + +#include <string> + +#include "google_apis/gaia/gaia_auth_fetcher.h" +#include "net/url_request/test_url_fetcher_factory.h" +#include "net/url_request/url_request_status.h" + +// Responds as though ClientLogin returned from the server. +class MockFetcher : public net::TestURLFetcher { + public: + MockFetcher(bool success, + const GURL& url, + const std::string& results, + net::URLFetcher::RequestType request_type, + net::URLFetcherDelegate* d); + + MockFetcher(const GURL& url, + const net::URLRequestStatus& status, + int response_code, + const net::ResponseCookies& cookies, + const std::string& results, + net::URLFetcher::RequestType request_type, + net::URLFetcherDelegate* d); + + virtual ~MockFetcher(); + + virtual void Start() OVERRIDE; + + private: + DISALLOW_COPY_AND_ASSIGN(MockFetcher); +}; + +template<typename T> +class MockURLFetcherFactory : public net::URLFetcherFactory, + public net::ScopedURLFetcherFactory { + public: + MockURLFetcherFactory() + : net::ScopedURLFetcherFactory(this), + success_(true) { + } + ~MockURLFetcherFactory() {} + net::URLFetcher* CreateURLFetcher( + int id, + const GURL& url, + net::URLFetcher::RequestType request_type, + net::URLFetcherDelegate* d) OVERRIDE { + return new T(success_, url, results_, request_type, d); + } + void set_success(bool success) { + success_ = success; + } + void set_results(const std::string& results) { + results_ = results; + } + private: + bool success_; + std::string results_; + DISALLOW_COPY_AND_ASSIGN(MockURLFetcherFactory); +}; + +#endif // GOOGLE_APIS_GAIA_MOCK_URL_FETCHER_FACTORY_H_ diff --git a/chromium/google_apis/gaia/oauth2_access_token_consumer.h b/chromium/google_apis/gaia/oauth2_access_token_consumer.h new file mode 100644 index 00000000000..65d32e85ab1 --- /dev/null +++ b/chromium/google_apis/gaia/oauth2_access_token_consumer.h @@ -0,0 +1,31 @@ +// Copyright (c) 2012 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 GOOGLE_APIS_GAIA_OAUTH2_ACCESS_TOKEN_CONSUMER_H_ +#define GOOGLE_APIS_GAIA_OAUTH2_ACCESS_TOKEN_CONSUMER_H_ + +#include <string> + +class GoogleServiceAuthError; + +namespace base { +class Time; +} + +// An interface that defines the callbacks for consumers to which +// OAuth2AccessTokenFetcher can return results. +class OAuth2AccessTokenConsumer { + public: + // Success callback. |access_token| will contain a valid OAuth2 access token. + // |expiration_time| is the date until which the token can be used. This + // value has a built-in safety margin, so it can be used as-is. + virtual void OnGetTokenSuccess(const std::string& access_token, + const base::Time& expiration_time) {} + virtual void OnGetTokenFailure(const GoogleServiceAuthError& error) {} + + protected: + virtual ~OAuth2AccessTokenConsumer() {} +}; + +#endif // GOOGLE_APIS_GAIA_OAUTH2_ACCESS_TOKEN_CONSUMER_H_ diff --git a/chromium/google_apis/gaia/oauth2_access_token_fetcher.cc b/chromium/google_apis/gaia/oauth2_access_token_fetcher.cc new file mode 100644 index 00000000000..456251f0134 --- /dev/null +++ b/chromium/google_apis/gaia/oauth2_access_token_fetcher.cc @@ -0,0 +1,233 @@ +// Copyright (c) 2012 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 "google_apis/gaia/oauth2_access_token_fetcher.h" + +#include <algorithm> +#include <string> +#include <vector> + +#include "base/json/json_reader.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "base/time/time.h" +#include "base/values.h" +#include "google_apis/gaia/gaia_urls.h" +#include "google_apis/gaia/google_service_auth_error.h" +#include "net/base/escape.h" +#include "net/base/load_flags.h" +#include "net/http/http_status_code.h" +#include "net/url_request/url_fetcher.h" +#include "net/url_request/url_request_context_getter.h" +#include "net/url_request/url_request_status.h" + +using net::ResponseCookies; +using net::URLFetcher; +using net::URLFetcherDelegate; +using net::URLRequestContextGetter; +using net::URLRequestStatus; + +namespace { +static const char kGetAccessTokenBodyFormat[] = + "client_id=%s&" + "client_secret=%s&" + "grant_type=refresh_token&" + "refresh_token=%s"; + +static const char kGetAccessTokenBodyWithScopeFormat[] = + "client_id=%s&" + "client_secret=%s&" + "grant_type=refresh_token&" + "refresh_token=%s&" + "scope=%s"; + +static const char kAccessTokenKey[] = "access_token"; +static const char kExpiresInKey[] = "expires_in"; + +static GoogleServiceAuthError CreateAuthError(URLRequestStatus status) { + CHECK(!status.is_success()); + if (status.status() == URLRequestStatus::CANCELED) { + return GoogleServiceAuthError(GoogleServiceAuthError::REQUEST_CANCELED); + } else { + DLOG(WARNING) << "Could not reach Google Accounts servers: errno " + << status.error(); + return GoogleServiceAuthError::FromConnectionError(status.error()); + } +} + +static URLFetcher* CreateFetcher(URLRequestContextGetter* getter, + const GURL& url, + const std::string& body, + URLFetcherDelegate* delegate) { + bool empty_body = body.empty(); + URLFetcher* result = net::URLFetcher::Create( + 0, url, + empty_body ? URLFetcher::GET : URLFetcher::POST, + delegate); + + result->SetRequestContext(getter); + result->SetLoadFlags(net::LOAD_DO_NOT_SEND_COOKIES | + net::LOAD_DO_NOT_SAVE_COOKIES); + // Fetchers are sometimes cancelled because a network change was detected, + // especially at startup and after sign-in on ChromeOS. Retrying once should + // be enough in those cases; let the fetcher retry up to 3 times just in case. + // http://crbug.com/163710 + result->SetAutomaticallyRetryOnNetworkChanges(3); + + if (!empty_body) + result->SetUploadData("application/x-www-form-urlencoded", body); + + return result; +} +} // namespace + +OAuth2AccessTokenFetcher::OAuth2AccessTokenFetcher( + OAuth2AccessTokenConsumer* consumer, + URLRequestContextGetter* getter) + : consumer_(consumer), + getter_(getter), + state_(INITIAL) { } + +OAuth2AccessTokenFetcher::~OAuth2AccessTokenFetcher() { } + +void OAuth2AccessTokenFetcher::CancelRequest() { + fetcher_.reset(); +} + +void OAuth2AccessTokenFetcher::Start(const std::string& client_id, + const std::string& client_secret, + const std::string& refresh_token, + const std::vector<std::string>& scopes) { + client_id_ = client_id; + client_secret_ = client_secret; + refresh_token_ = refresh_token; + scopes_ = scopes; + StartGetAccessToken(); +} + +void OAuth2AccessTokenFetcher::StartGetAccessToken() { + CHECK_EQ(INITIAL, state_); + state_ = GET_ACCESS_TOKEN_STARTED; + fetcher_.reset(CreateFetcher( + getter_, + MakeGetAccessTokenUrl(), + MakeGetAccessTokenBody( + client_id_, client_secret_, refresh_token_, scopes_), + this)); + fetcher_->Start(); // OnURLFetchComplete will be called. +} + +void OAuth2AccessTokenFetcher::EndGetAccessToken( + const net::URLFetcher* source) { + CHECK_EQ(GET_ACCESS_TOKEN_STARTED, state_); + state_ = GET_ACCESS_TOKEN_DONE; + + URLRequestStatus status = source->GetStatus(); + if (!status.is_success()) { + OnGetTokenFailure(CreateAuthError(status)); + return; + } + + // HTTP_FORBIDDEN (403) is treated as temporary error, because it may be + // '403 Rate Limit Exeeded.' + if (source->GetResponseCode() == net::HTTP_FORBIDDEN) { + OnGetTokenFailure(GoogleServiceAuthError( + GoogleServiceAuthError::SERVICE_UNAVAILABLE)); + return; + } + + // The other errors are treated as permanent error. + if (source->GetResponseCode() != net::HTTP_OK) { + OnGetTokenFailure(GoogleServiceAuthError( + GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS)); + return; + } + + // The request was successfully fetched and it returned OK. + // Parse out the access token and the expiration time. + std::string access_token; + int expires_in; + if (!ParseGetAccessTokenResponse(source, &access_token, &expires_in)) { + DLOG(WARNING) << "Response doesn't match expected format"; + OnGetTokenFailure( + GoogleServiceAuthError(GoogleServiceAuthError::SERVICE_UNAVAILABLE)); + return; + } + // The token will expire in |expires_in| seconds. Take a 10% error margin to + // prevent reusing a token too close to its expiration date. + OnGetTokenSuccess( + access_token, + base::Time::Now() + base::TimeDelta::FromSeconds(9 * expires_in / 10)); +} + +void OAuth2AccessTokenFetcher::OnGetTokenSuccess( + const std::string& access_token, + const base::Time& expiration_time) { + consumer_->OnGetTokenSuccess(access_token, expiration_time); +} + +void OAuth2AccessTokenFetcher::OnGetTokenFailure( + const GoogleServiceAuthError& error) { + state_ = ERROR_STATE; + consumer_->OnGetTokenFailure(error); +} + +void OAuth2AccessTokenFetcher::OnURLFetchComplete( + const net::URLFetcher* source) { + CHECK(source); + CHECK(state_ == GET_ACCESS_TOKEN_STARTED); + EndGetAccessToken(source); +} + +// static +GURL OAuth2AccessTokenFetcher::MakeGetAccessTokenUrl() { + return GURL(GaiaUrls::GetInstance()->oauth2_token_url()); +} + +// static +std::string OAuth2AccessTokenFetcher::MakeGetAccessTokenBody( + const std::string& client_id, + const std::string& client_secret, + const std::string& refresh_token, + const std::vector<std::string>& scopes) { + std::string enc_client_id = net::EscapeUrlEncodedData(client_id, true); + std::string enc_client_secret = + net::EscapeUrlEncodedData(client_secret, true); + std::string enc_refresh_token = + net::EscapeUrlEncodedData(refresh_token, true); + if (scopes.empty()) { + return base::StringPrintf( + kGetAccessTokenBodyFormat, + enc_client_id.c_str(), + enc_client_secret.c_str(), + enc_refresh_token.c_str()); + } else { + std::string scopes_string = JoinString(scopes, ' '); + return base::StringPrintf( + kGetAccessTokenBodyWithScopeFormat, + enc_client_id.c_str(), + enc_client_secret.c_str(), + enc_refresh_token.c_str(), + net::EscapeUrlEncodedData(scopes_string, true).c_str()); + } +} + +// static +bool OAuth2AccessTokenFetcher::ParseGetAccessTokenResponse( + const net::URLFetcher* source, + std::string* access_token, + int* expires_in) { + CHECK(source); + CHECK(access_token); + std::string data; + source->GetResponseAsString(&data); + scoped_ptr<base::Value> value(base::JSONReader::Read(data)); + if (!value.get() || value->GetType() != base::Value::TYPE_DICTIONARY) + return false; + + base::DictionaryValue* dict = + static_cast<base::DictionaryValue*>(value.get()); + return dict->GetString(kAccessTokenKey, access_token) && + dict->GetInteger(kExpiresInKey, expires_in); +} diff --git a/chromium/google_apis/gaia/oauth2_access_token_fetcher.h b/chromium/google_apis/gaia/oauth2_access_token_fetcher.h new file mode 100644 index 00000000000..11ac6ea8b5c --- /dev/null +++ b/chromium/google_apis/gaia/oauth2_access_token_fetcher.h @@ -0,0 +1,118 @@ +// Copyright (c) 2012 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 GOOGLE_APIS_GAIA_OAUTH2_ACCESS_TOKEN_FETCHER_H_ +#define GOOGLE_APIS_GAIA_OAUTH2_ACCESS_TOKEN_FETCHER_H_ + +#include <string> +#include <vector> + +#include "base/gtest_prod_util.h" +#include "base/memory/scoped_ptr.h" +#include "google_apis/gaia/oauth2_access_token_consumer.h" +#include "net/url_request/url_fetcher_delegate.h" +#include "url/gurl.h" + +class OAuth2AccessTokenFetcherTest; + +namespace base { +class Time; +} + +namespace net { +class URLFetcher; +class URLRequestContextGetter; +class URLRequestStatus; +} + +// Abstracts the details to get OAuth2 access token token from +// OAuth2 refresh token. +// See "Using the Refresh Token" section in: +// http://code.google.com/apis/accounts/docs/OAuth2WebServer.html +// +// This class should be used on a single thread, but it can be whichever thread +// that you like. +// Also, do not reuse the same instance. Once Start() is called, the instance +// should not be reused. +// +// Usage: +// * Create an instance with a consumer. +// * Call Start() +// * The consumer passed in the constructor will be called on the same +// thread Start was called with the results. +// +// This class can handle one request at a time. To parallelize requests, +// create multiple instances. +class OAuth2AccessTokenFetcher : public net::URLFetcherDelegate { + public: + OAuth2AccessTokenFetcher(OAuth2AccessTokenConsumer* consumer, + net::URLRequestContextGetter* getter); + virtual ~OAuth2AccessTokenFetcher(); + + // Starts the flow with the given parameters. + // |scopes| can be empty. If it is empty then the access token will have the + // same scope as the refresh token. If not empty, then access token will have + // the scopes specified. In this case, the access token will successfully be + // generated only if refresh token has login scope of a list of scopes that is + // a super-set of the specified scopes. + virtual void Start(const std::string& client_id, + const std::string& client_secret, + const std::string& refresh_token, + const std::vector<std::string>& scopes); + + void CancelRequest(); + + // Implementation of net::URLFetcherDelegate + virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE; + + private: + enum State { + INITIAL, + GET_ACCESS_TOKEN_STARTED, + GET_ACCESS_TOKEN_DONE, + ERROR_STATE, + }; + + // Helper methods for the flow. + void StartGetAccessToken(); + void EndGetAccessToken(const net::URLFetcher* source); + + // Helper mehtods for reporting back results. + void OnGetTokenSuccess(const std::string& access_token, + const base::Time& expiration_time); + void OnGetTokenFailure(const GoogleServiceAuthError& error); + + // Other helpers. + static GURL MakeGetAccessTokenUrl(); + static std::string MakeGetAccessTokenBody( + const std::string& client_id, + const std::string& client_secret, + const std::string& refresh_token, + const std::vector<std::string>& scopes); + static bool ParseGetAccessTokenResponse(const net::URLFetcher* source, + std::string* access_token, + int* expires_in); + + // State that is set during construction. + OAuth2AccessTokenConsumer* const consumer_; + net::URLRequestContextGetter* const getter_; + State state_; + + // While a fetch is in progress. + scoped_ptr<net::URLFetcher> fetcher_; + std::string client_id_; + std::string client_secret_; + std::string refresh_token_; + std::vector<std::string> scopes_; + + friend class OAuth2AccessTokenFetcherTest; + FRIEND_TEST_ALL_PREFIXES(OAuth2AccessTokenFetcherTest, + ParseGetAccessTokenResponse); + FRIEND_TEST_ALL_PREFIXES(OAuth2AccessTokenFetcherTest, + MakeGetAccessTokenBody); + + DISALLOW_COPY_AND_ASSIGN(OAuth2AccessTokenFetcher); +}; + +#endif // GOOGLE_APIS_GAIA_OAUTH2_ACCESS_TOKEN_FETCHER_H_ diff --git a/chromium/google_apis/gaia/oauth2_access_token_fetcher_unittest.cc b/chromium/google_apis/gaia/oauth2_access_token_fetcher_unittest.cc new file mode 100644 index 00000000000..6b12da3fbd0 --- /dev/null +++ b/chromium/google_apis/gaia/oauth2_access_token_fetcher_unittest.cc @@ -0,0 +1,233 @@ +// Copyright (c) 2012 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. +// +// A complete set of unit tests for OAuth2AccessTokenFetcher. + +#include <string> + +#include "base/memory/scoped_ptr.h" +#include "content/public/test/test_browser_thread_bundle.h" +#include "google_apis/gaia/gaia_urls.h" +#include "google_apis/gaia/google_service_auth_error.h" +#include "google_apis/gaia/oauth2_access_token_consumer.h" +#include "google_apis/gaia/oauth2_access_token_fetcher.h" +#include "net/http/http_status_code.h" +#include "net/url_request/test_url_fetcher_factory.h" +#include "net/url_request/url_fetcher.h" +#include "net/url_request/url_fetcher_delegate.h" +#include "net/url_request/url_fetcher_factory.h" +#include "net/url_request/url_request.h" +#include "net/url_request/url_request_status.h" +#include "net/url_request/url_request_test_util.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +using net::ResponseCookies; +using net::ScopedURLFetcherFactory; +using net::TestURLFetcher; +using net::URLFetcher; +using net::URLFetcherDelegate; +using net::URLFetcherFactory; +using net::URLRequestStatus; +using testing::_; +using testing::Return; + +namespace { + +typedef std::vector<std::string> ScopeList; + +static const char kValidTokenResponse[] = + "{" + " \"access_token\": \"at1\"," + " \"expires_in\": 3600," + " \"token_type\": \"Bearer\"" + "}"; +static const char kTokenResponseNoAccessToken[] = + "{" + " \"expires_in\": 3600," + " \"token_type\": \"Bearer\"" + "}"; + +class MockUrlFetcherFactory : public ScopedURLFetcherFactory, + public URLFetcherFactory { +public: + MockUrlFetcherFactory() + : ScopedURLFetcherFactory(this) { + } + virtual ~MockUrlFetcherFactory() {} + + MOCK_METHOD4( + CreateURLFetcher, + URLFetcher* (int id, + const GURL& url, + URLFetcher::RequestType request_type, + URLFetcherDelegate* d)); +}; + +class MockOAuth2AccessTokenConsumer : public OAuth2AccessTokenConsumer { + public: + MockOAuth2AccessTokenConsumer() {} + ~MockOAuth2AccessTokenConsumer() {} + + MOCK_METHOD2(OnGetTokenSuccess, void(const std::string& access_token, + const base::Time& expiration_time)); + MOCK_METHOD1(OnGetTokenFailure, + void(const GoogleServiceAuthError& error)); +}; + +} // namespace + +class OAuth2AccessTokenFetcherTest : public testing::Test { + public: + OAuth2AccessTokenFetcherTest() + : request_context_getter_(new net::TestURLRequestContextGetter( + base::MessageLoopProxy::current())), + fetcher_(&consumer_, request_context_getter_) { + } + + virtual ~OAuth2AccessTokenFetcherTest() {} + + virtual TestURLFetcher* SetupGetAccessToken( + bool fetch_succeeds, int response_code, const std::string& body) { + GURL url(GaiaUrls::GetInstance()->oauth2_token_url()); + TestURLFetcher* url_fetcher = new TestURLFetcher(0, url, &fetcher_); + URLRequestStatus::Status status = + fetch_succeeds ? URLRequestStatus::SUCCESS : URLRequestStatus::FAILED; + url_fetcher->set_status(URLRequestStatus(status, 0)); + + if (response_code != 0) + url_fetcher->set_response_code(response_code); + + if (!body.empty()) + url_fetcher->SetResponseString(body); + + EXPECT_CALL(factory_, CreateURLFetcher(_, url, _, _)) + .WillOnce(Return(url_fetcher)); + return url_fetcher; + } + + protected: + content::TestBrowserThreadBundle thread_bundle_; + MockUrlFetcherFactory factory_; + MockOAuth2AccessTokenConsumer consumer_; + scoped_refptr<net::TestURLRequestContextGetter> request_context_getter_; + OAuth2AccessTokenFetcher fetcher_; +}; + +// These four tests time out, see http://crbug.com/113446. +TEST_F(OAuth2AccessTokenFetcherTest, DISABLED_GetAccessTokenRequestFailure) { + TestURLFetcher* url_fetcher = SetupGetAccessToken(false, 0, std::string()); + EXPECT_CALL(consumer_, OnGetTokenFailure(_)).Times(1); + fetcher_.Start("client_id", "client_secret", "refresh_token", ScopeList()); + fetcher_.OnURLFetchComplete(url_fetcher); +} + +TEST_F(OAuth2AccessTokenFetcherTest, + DISABLED_GetAccessTokenResponseCodeFailure) { + TestURLFetcher* url_fetcher = + SetupGetAccessToken(true, net::HTTP_FORBIDDEN, std::string()); + EXPECT_CALL(consumer_, OnGetTokenFailure(_)).Times(1); + fetcher_.Start("client_id", "client_secret", "refresh_token", ScopeList()); + fetcher_.OnURLFetchComplete(url_fetcher); +} + +TEST_F(OAuth2AccessTokenFetcherTest, DISABLED_Success) { + TestURLFetcher* url_fetcher = SetupGetAccessToken( + true, net::HTTP_OK, kValidTokenResponse); + EXPECT_CALL(consumer_, OnGetTokenSuccess("at1", _)).Times(1); + fetcher_.Start("client_id", "client_secret", "refresh_token", ScopeList()); + fetcher_.OnURLFetchComplete(url_fetcher); +} + +TEST_F(OAuth2AccessTokenFetcherTest, DISABLED_MakeGetAccessTokenBody) { + { // No scope. + std::string body = + "client_id=cid1&" + "client_secret=cs1&" + "grant_type=refresh_token&" + "refresh_token=rt1"; + EXPECT_EQ(body, OAuth2AccessTokenFetcher::MakeGetAccessTokenBody( + "cid1", "cs1", "rt1", ScopeList())); + } + + { // One scope. + std::string body = + "client_id=cid1&" + "client_secret=cs1&" + "grant_type=refresh_token&" + "refresh_token=rt1&" + "scope=https://www.googleapis.com/foo"; + ScopeList scopes; + scopes.push_back("https://www.googleapis.com/foo"); + EXPECT_EQ(body, OAuth2AccessTokenFetcher::MakeGetAccessTokenBody( + "cid1", "cs1", "rt1", scopes)); + } + + { // Multiple scopes. + std::string body = + "client_id=cid1&" + "client_secret=cs1&" + "grant_type=refresh_token&" + "refresh_token=rt1&" + "scope=https://www.googleapis.com/foo+" + "https://www.googleapis.com/bar+" + "https://www.googleapis.com/baz"; + ScopeList scopes; + scopes.push_back("https://www.googleapis.com/foo"); + scopes.push_back("https://www.googleapis.com/bar"); + scopes.push_back("https://www.googleapis.com/baz"); + EXPECT_EQ(body, OAuth2AccessTokenFetcher::MakeGetAccessTokenBody( + "cid1", "cs1", "rt1", scopes)); + } +} + +// http://crbug.com/114215 +#if defined(OS_WIN) +#define MAYBE_ParseGetAccessTokenResponse DISABLED_ParseGetAccessTokenResponse +#else +#define MAYBE_ParseGetAccessTokenResponse ParseGetAccessTokenResponse +#endif // defined(OS_WIN) +TEST_F(OAuth2AccessTokenFetcherTest, MAYBE_ParseGetAccessTokenResponse) { + { // No body. + TestURLFetcher url_fetcher(0, GURL("www.google.com"), NULL); + + std::string at; + int expires_in; + EXPECT_FALSE(OAuth2AccessTokenFetcher::ParseGetAccessTokenResponse( + &url_fetcher, &at, &expires_in)); + EXPECT_TRUE(at.empty()); + } + { // Bad json. + TestURLFetcher url_fetcher(0, GURL("www.google.com"), NULL); + url_fetcher.SetResponseString("foo"); + + std::string at; + int expires_in; + EXPECT_FALSE(OAuth2AccessTokenFetcher::ParseGetAccessTokenResponse( + &url_fetcher, &at, &expires_in)); + EXPECT_TRUE(at.empty()); + } + { // Valid json: access token missing. + TestURLFetcher url_fetcher(0, GURL("www.google.com"), NULL); + url_fetcher.SetResponseString(kTokenResponseNoAccessToken); + + std::string at; + int expires_in; + EXPECT_FALSE(OAuth2AccessTokenFetcher::ParseGetAccessTokenResponse( + &url_fetcher, &at, &expires_in)); + EXPECT_TRUE(at.empty()); + } + { // Valid json: all good. + TestURLFetcher url_fetcher(0, GURL("www.google.com"), NULL); + url_fetcher.SetResponseString(kValidTokenResponse); + + std::string at; + int expires_in; + EXPECT_TRUE(OAuth2AccessTokenFetcher::ParseGetAccessTokenResponse( + &url_fetcher, &at, &expires_in)); + EXPECT_EQ("at1", at); + EXPECT_EQ(3600, expires_in); + } +} diff --git a/chromium/google_apis/gaia/oauth2_api_call_flow.cc b/chromium/google_apis/gaia/oauth2_api_call_flow.cc new file mode 100644 index 00000000000..4c4940d9853 --- /dev/null +++ b/chromium/google_apis/gaia/oauth2_api_call_flow.cc @@ -0,0 +1,172 @@ +// Copyright (c) 2012 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 "google_apis/gaia/oauth2_api_call_flow.h" + +#include <string> +#include <vector> + +#include "base/basictypes.h" +#include "base/strings/stringprintf.h" +#include "google_apis/gaia/gaia_urls.h" +#include "net/base/escape.h" +#include "net/base/load_flags.h" +#include "net/http/http_status_code.h" +#include "net/url_request/url_fetcher.h" +#include "net/url_request/url_request_context_getter.h" +#include "net/url_request/url_request_status.h" + +using net::ResponseCookies; +using net::URLFetcher; +using net::URLFetcherDelegate; +using net::URLRequestContextGetter; +using net::URLRequestStatus; + +namespace { +static const char kAuthorizationHeaderFormat[] = + "Authorization: Bearer %s"; + +static std::string MakeAuthorizationHeader(const std::string& auth_token) { + return base::StringPrintf(kAuthorizationHeaderFormat, auth_token.c_str()); +} +} // namespace + +OAuth2ApiCallFlow::OAuth2ApiCallFlow( + net::URLRequestContextGetter* context, + const std::string& refresh_token, + const std::string& access_token, + const std::vector<std::string>& scopes) + : context_(context), + refresh_token_(refresh_token), + access_token_(access_token), + scopes_(scopes), + state_(INITIAL), + tried_mint_access_token_(false) { +} + +OAuth2ApiCallFlow::~OAuth2ApiCallFlow() {} + +void OAuth2ApiCallFlow::Start() { + BeginApiCall(); +} + +void OAuth2ApiCallFlow::BeginApiCall() { + CHECK(state_ == INITIAL || state_ == MINT_ACCESS_TOKEN_DONE); + + // If the access token is empty then directly try to mint one. + if (access_token_.empty()) { + BeginMintAccessToken(); + } else { + state_ = API_CALL_STARTED; + url_fetcher_.reset(CreateURLFetcher()); + url_fetcher_->Start(); // OnURLFetchComplete will be called. + } +} + +void OAuth2ApiCallFlow::EndApiCall(const net::URLFetcher* source) { + CHECK_EQ(API_CALL_STARTED, state_); + state_ = API_CALL_DONE; + + URLRequestStatus status = source->GetStatus(); + if (!status.is_success()) { + state_ = ERROR_STATE; + ProcessApiCallFailure(source); + return; + } + + // If the response code is 401 Unauthorized then access token may have + // expired. So try generating a new access token. + if (source->GetResponseCode() == net::HTTP_UNAUTHORIZED) { + // If we already tried minting a new access token, don't do it again. + if (tried_mint_access_token_) { + state_ = ERROR_STATE; + ProcessApiCallFailure(source); + } else { + BeginMintAccessToken(); + } + + return; + } + + if (source->GetResponseCode() != net::HTTP_OK) { + state_ = ERROR_STATE; + ProcessApiCallFailure(source); + return; + } + + ProcessApiCallSuccess(source); +} + +void OAuth2ApiCallFlow::BeginMintAccessToken() { + CHECK(state_ == INITIAL || state_ == API_CALL_DONE); + CHECK(!tried_mint_access_token_); + state_ = MINT_ACCESS_TOKEN_STARTED; + tried_mint_access_token_ = true; + + oauth2_access_token_fetcher_.reset(CreateAccessTokenFetcher()); + oauth2_access_token_fetcher_->Start( + GaiaUrls::GetInstance()->oauth2_chrome_client_id(), + GaiaUrls::GetInstance()->oauth2_chrome_client_secret(), + refresh_token_, + scopes_); +} + +void OAuth2ApiCallFlow::EndMintAccessToken( + const GoogleServiceAuthError* error) { + CHECK_EQ(MINT_ACCESS_TOKEN_STARTED, state_); + + if (!error) { + state_ = MINT_ACCESS_TOKEN_DONE; + BeginApiCall(); + } else { + state_ = ERROR_STATE; + ProcessMintAccessTokenFailure(*error); + } +} + +OAuth2AccessTokenFetcher* OAuth2ApiCallFlow::CreateAccessTokenFetcher() { + return new OAuth2AccessTokenFetcher(this, context_); +} + +void OAuth2ApiCallFlow::OnURLFetchComplete(const net::URLFetcher* source) { + CHECK(source); + CHECK_EQ(API_CALL_STARTED, state_); + EndApiCall(source); +} + +void OAuth2ApiCallFlow::OnGetTokenSuccess(const std::string& access_token, + const base::Time& expiration_time) { + access_token_ = access_token; + EndMintAccessToken(NULL); +} + +void OAuth2ApiCallFlow::OnGetTokenFailure( + const GoogleServiceAuthError& error) { + EndMintAccessToken(&error); +} + +URLFetcher* OAuth2ApiCallFlow::CreateURLFetcher() { + std::string body = CreateApiCallBody(); + bool empty_body = body.empty(); + URLFetcher* result = net::URLFetcher::Create( + 0, + CreateApiCallUrl(), + empty_body ? URLFetcher::GET : URLFetcher::POST, + this); + + result->SetRequestContext(context_); + result->SetLoadFlags(net::LOAD_DO_NOT_SEND_COOKIES | + net::LOAD_DO_NOT_SAVE_COOKIES); + result->AddExtraRequestHeader(MakeAuthorizationHeader(access_token_)); + // Fetchers are sometimes cancelled because a network change was detected, + // especially at startup and after sign-in on ChromeOS. Retrying once should + // be enough in those cases; let the fetcher retry up to 3 times just in case. + // http://crbug.com/163710 + result->SetAutomaticallyRetryOnNetworkChanges(3); + + if (!empty_body) + result->SetUploadData("application/x-www-form-urlencoded", body); + + return result; +} diff --git a/chromium/google_apis/gaia/oauth2_api_call_flow.h b/chromium/google_apis/gaia/oauth2_api_call_flow.h new file mode 100644 index 00000000000..fccd1e753be --- /dev/null +++ b/chromium/google_apis/gaia/oauth2_api_call_flow.h @@ -0,0 +1,126 @@ +// Copyright (c) 2012 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 GOOGLE_APIS_GAIA_OAUTH2_API_CALL_FLOW_H_ +#define GOOGLE_APIS_GAIA_OAUTH2_API_CALL_FLOW_H_ + +#include <string> + +#include "base/memory/scoped_ptr.h" +#include "google_apis/gaia/oauth2_access_token_consumer.h" +#include "google_apis/gaia/oauth2_access_token_fetcher.h" +#include "net/url_request/url_fetcher_delegate.h" + +class GoogleServiceAuthError; +class OAuth2MintTokenFlowTest; + +namespace net { +class URLFetcher; +class URLRequestContextGetter; +} + +// Base class for all classes that implement a flow to call OAuth2 +// enabled APIs. +// +// Given a refresh token, an access token, and a list of scopes an OAuth2 +// enabled API is called in the following way: +// 1. Try the given access token to call the API. +// 2. If that does not work, use the refresh token and scopes to generate +// a new access token. +// 3. Try the new access token to call the API. +// +// This class abstracts the basic steps and exposes template methods +// for sub-classes to implement for API specific details. +class OAuth2ApiCallFlow + : public net::URLFetcherDelegate, + public OAuth2AccessTokenConsumer { + public: + // Creates an instance that works with the given data. + // Note that |access_token| can be empty. In that case, the flow will skip + // the first step (of trying an existing access token). + OAuth2ApiCallFlow( + net::URLRequestContextGetter* context, + const std::string& refresh_token, + const std::string& access_token, + const std::vector<std::string>& scopes); + + virtual ~OAuth2ApiCallFlow(); + + // Start the flow. + virtual void Start(); + + // OAuth2AccessTokenFetcher implementation. + virtual void OnGetTokenSuccess(const std::string& access_token, + const base::Time& expiration_time) OVERRIDE; + virtual void OnGetTokenFailure(const GoogleServiceAuthError& error) OVERRIDE; + + // net::URLFetcherDelegate implementation. + virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE; + + protected: + // Template methods for sub-classes. + + // Methods to help create HTTP request. + virtual GURL CreateApiCallUrl() = 0; + virtual std::string CreateApiCallBody() = 0; + + // Sub-classes can expose an appropriate observer interface by implementing + // these template methods. + // Called when the API call finished successfully. + virtual void ProcessApiCallSuccess(const net::URLFetcher* source) = 0; + // Called when the API call failed. + virtual void ProcessApiCallFailure(const net::URLFetcher* source) = 0; + // Called when a new access token is generated. + virtual void ProcessNewAccessToken(const std::string& access_token) = 0; + virtual void ProcessMintAccessTokenFailure( + const GoogleServiceAuthError& error) = 0; + + private: + enum State { + INITIAL, + API_CALL_STARTED, + API_CALL_DONE, + MINT_ACCESS_TOKEN_STARTED, + MINT_ACCESS_TOKEN_DONE, + ERROR_STATE + }; + + friend class OAuth2ApiCallFlowTest; + FRIEND_TEST_ALL_PREFIXES(OAuth2ApiCallFlowTest, CreateURLFetcher); + + // Helper to create an instance of access token fetcher. + // Caller owns the returned instance. + // Note that this is virtual since it is mocked during unit testing. + virtual OAuth2AccessTokenFetcher* CreateAccessTokenFetcher(); + + // Creates an instance of URLFetcher that does not send or save cookies. + // Template method CreateApiCallUrl is used to get the URL. + // Template method CreateApiCallBody is used to get the body. + // The URLFether's method will be GET if body is empty, POST otherwise. + // Caller owns the returned instance. + // Note that this is virtual since it is mocked during unit testing. + virtual net::URLFetcher* CreateURLFetcher(); + + // Helper methods to implement the state machine for the flow. + void BeginApiCall(); + void EndApiCall(const net::URLFetcher* source); + void BeginMintAccessToken(); + void EndMintAccessToken(const GoogleServiceAuthError* error); + + net::URLRequestContextGetter* context_; + std::string refresh_token_; + std::string access_token_; + std::vector<std::string> scopes_; + + State state_; + // Whether we have already tried minting an access token once. + bool tried_mint_access_token_; + + scoped_ptr<net::URLFetcher> url_fetcher_; + scoped_ptr<OAuth2AccessTokenFetcher> oauth2_access_token_fetcher_; + + DISALLOW_COPY_AND_ASSIGN(OAuth2ApiCallFlow); +}; + +#endif // GOOGLE_APIS_GAIA_OAUTH2_API_CALL_FLOW_H_ diff --git a/chromium/google_apis/gaia/oauth2_api_call_flow_unittest.cc b/chromium/google_apis/gaia/oauth2_api_call_flow_unittest.cc new file mode 100644 index 00000000000..d56a613eded --- /dev/null +++ b/chromium/google_apis/gaia/oauth2_api_call_flow_unittest.cc @@ -0,0 +1,298 @@ +// Copyright (c) 2012 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. +// +// A complete set of unit tests for OAuth2MintTokenFlow. + +#include <string> +#include <vector> + +#include "base/memory/scoped_ptr.h" +#include "base/message_loop/message_loop.h" +#include "base/time/time.h" +#include "google_apis/gaia/gaia_urls.h" +#include "google_apis/gaia/google_service_auth_error.h" +#include "google_apis/gaia/oauth2_access_token_consumer.h" +#include "google_apis/gaia/oauth2_access_token_fetcher.h" +#include "google_apis/gaia/oauth2_api_call_flow.h" +#include "net/http/http_request_headers.h" +#include "net/http/http_status_code.h" +#include "net/url_request/test_url_fetcher_factory.h" +#include "net/url_request/url_fetcher.h" +#include "net/url_request/url_fetcher_delegate.h" +#include "net/url_request/url_fetcher_factory.h" +#include "net/url_request/url_request.h" +#include "net/url_request/url_request_status.h" +#include "net/url_request/url_request_test_util.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using net::HttpRequestHeaders; +using net::ScopedURLFetcherFactory; +using net::TestURLFetcher; +using net::URLFetcher; +using net::URLFetcherDelegate; +using net::URLFetcherFactory; +using net::URLRequestStatus; +using testing::_; +using testing::Return; + +namespace { + +static std::string CreateBody() { + return "some body"; +} + +static GURL CreateApiUrl() { + return GURL("https://www.googleapis.com/someapi"); +} + +static std::vector<std::string> CreateTestScopes() { + std::vector<std::string> scopes; + scopes.push_back("scope1"); + scopes.push_back("scope2"); + return scopes; +} + +class MockUrlFetcherFactory : public ScopedURLFetcherFactory, + public URLFetcherFactory { + public: + MockUrlFetcherFactory() + : ScopedURLFetcherFactory(this) { + } + virtual ~MockUrlFetcherFactory() {} + + MOCK_METHOD4( + CreateURLFetcher, + URLFetcher* (int id, + const GURL& url, + URLFetcher::RequestType request_type, + URLFetcherDelegate* d)); +}; + +class MockAccessTokenFetcher : public OAuth2AccessTokenFetcher { + public: + MockAccessTokenFetcher(OAuth2AccessTokenConsumer* consumer, + net::URLRequestContextGetter* getter) + : OAuth2AccessTokenFetcher(consumer, getter) {} + ~MockAccessTokenFetcher() {} + + MOCK_METHOD4(Start, + void (const std::string& client_id, + const std::string& client_secret, + const std::string& refresh_token, + const std::vector<std::string>& scopes)); +}; + +class MockApiCallFlow : public OAuth2ApiCallFlow { + public: + MockApiCallFlow(net::URLRequestContextGetter* context, + const std::string& refresh_token, + const std::string& access_token, + const std::vector<std::string>& scopes) + : OAuth2ApiCallFlow(context, refresh_token, access_token, scopes) {} + ~MockApiCallFlow() {} + + MOCK_METHOD0(CreateApiCallUrl, GURL ()); + MOCK_METHOD0(CreateApiCallBody, std::string ()); + MOCK_METHOD1(ProcessApiCallSuccess, + void (const URLFetcher* source)); + MOCK_METHOD1(ProcessApiCallFailure, + void (const URLFetcher* source)); + MOCK_METHOD1(ProcessNewAccessToken, + void (const std::string& access_token)); + MOCK_METHOD1(ProcessMintAccessTokenFailure, + void (const GoogleServiceAuthError& error)); + MOCK_METHOD0(CreateAccessTokenFetcher, OAuth2AccessTokenFetcher* ()); +}; + +} // namespace + +class OAuth2ApiCallFlowTest : public testing::Test { + protected: + void SetupAccessTokenFetcher( + const std::string& rt, const std::vector<std::string>& scopes) { + EXPECT_CALL(*access_token_fetcher_, + Start(GaiaUrls::GetInstance()->oauth2_chrome_client_id(), + GaiaUrls::GetInstance()->oauth2_chrome_client_secret(), + rt, scopes)) + .Times(1); + EXPECT_CALL(*flow_, CreateAccessTokenFetcher()) + .WillOnce(Return(access_token_fetcher_.release())); + } + + TestURLFetcher* CreateURLFetcher( + const GURL& url, bool fetch_succeeds, + int response_code, const std::string& body) { + TestURLFetcher* url_fetcher = new TestURLFetcher(0, url, flow_.get()); + URLRequestStatus::Status status = + fetch_succeeds ? URLRequestStatus::SUCCESS : URLRequestStatus::FAILED; + url_fetcher->set_status(URLRequestStatus(status, 0)); + + if (response_code != 0) + url_fetcher->set_response_code(response_code); + + if (!body.empty()) + url_fetcher->SetResponseString(body); + + return url_fetcher; + } + + void CreateFlow(const std::string& refresh_token, + const std::string& access_token, + const std::vector<std::string>& scopes) { + scoped_refptr<net::TestURLRequestContextGetter> request_context_getter = + new net::TestURLRequestContextGetter( + message_loop_.message_loop_proxy()); + flow_.reset(new MockApiCallFlow( + request_context_getter, refresh_token, access_token, scopes)); + access_token_fetcher_.reset( + new MockAccessTokenFetcher(flow_.get(), request_context_getter)); + } + + TestURLFetcher* SetupApiCall(bool succeeds, net::HttpStatusCode status) { + std::string body(CreateBody()); + GURL url(CreateApiUrl()); + EXPECT_CALL(*flow_, CreateApiCallBody()).WillOnce(Return(body)); + EXPECT_CALL(*flow_, CreateApiCallUrl()).WillOnce(Return(url)); + TestURLFetcher* url_fetcher = + CreateURLFetcher(url, succeeds, status, std::string()); + EXPECT_CALL(factory_, CreateURLFetcher(_, url, _, _)) + .WillOnce(Return(url_fetcher)); + return url_fetcher; + } + + MockUrlFetcherFactory factory_; + scoped_ptr<MockApiCallFlow> flow_; + scoped_ptr<MockAccessTokenFetcher> access_token_fetcher_; + base::MessageLoop message_loop_; +}; + +TEST_F(OAuth2ApiCallFlowTest, FirstApiCallSucceeds) { + std::string rt = "refresh_token"; + std::string at = "access_token"; + std::vector<std::string> scopes(CreateTestScopes()); + + CreateFlow(rt, at, scopes); + TestURLFetcher* url_fetcher = SetupApiCall(true, net::HTTP_OK); + EXPECT_CALL(*flow_, ProcessApiCallSuccess(url_fetcher)); + flow_->Start(); + flow_->OnURLFetchComplete(url_fetcher); +} + +TEST_F(OAuth2ApiCallFlowTest, SecondApiCallSucceeds) { + std::string rt = "refresh_token"; + std::string at = "access_token"; + std::vector<std::string> scopes(CreateTestScopes()); + + CreateFlow(rt, at, scopes); + TestURLFetcher* url_fetcher1 = SetupApiCall(true, net::HTTP_UNAUTHORIZED); + flow_->Start(); + SetupAccessTokenFetcher(rt, scopes); + flow_->OnURLFetchComplete(url_fetcher1); + TestURLFetcher* url_fetcher2 = SetupApiCall(true, net::HTTP_OK); + EXPECT_CALL(*flow_, ProcessApiCallSuccess(url_fetcher2)); + flow_->OnGetTokenSuccess( + at, + base::Time::Now() + base::TimeDelta::FromMinutes(3600)); + flow_->OnURLFetchComplete(url_fetcher2); +} + +TEST_F(OAuth2ApiCallFlowTest, SecondApiCallFails) { + std::string rt = "refresh_token"; + std::string at = "access_token"; + std::vector<std::string> scopes(CreateTestScopes()); + + CreateFlow(rt, at, scopes); + TestURLFetcher* url_fetcher1 = SetupApiCall(true, net::HTTP_UNAUTHORIZED); + flow_->Start(); + SetupAccessTokenFetcher(rt, scopes); + flow_->OnURLFetchComplete(url_fetcher1); + TestURLFetcher* url_fetcher2 = SetupApiCall(false, net::HTTP_UNAUTHORIZED); + EXPECT_CALL(*flow_, ProcessApiCallFailure(url_fetcher2)); + flow_->OnGetTokenSuccess( + at, + base::Time::Now() + base::TimeDelta::FromMinutes(3600)); + flow_->OnURLFetchComplete(url_fetcher2); +} + +TEST_F(OAuth2ApiCallFlowTest, NewTokenGenerationFails) { + std::string rt = "refresh_token"; + std::string at = "access_token"; + std::vector<std::string> scopes(CreateTestScopes()); + + CreateFlow(rt, at, scopes); + TestURLFetcher* url_fetcher = SetupApiCall(true, net::HTTP_UNAUTHORIZED); + flow_->Start(); + SetupAccessTokenFetcher(rt, scopes); + flow_->OnURLFetchComplete(url_fetcher); + GoogleServiceAuthError error( + GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS); + EXPECT_CALL(*flow_, ProcessMintAccessTokenFailure(error)); + flow_->OnGetTokenFailure(error); +} + +TEST_F(OAuth2ApiCallFlowTest, EmptyAccessTokenFirstApiCallSucceeds) { + std::string rt = "refresh_token"; + std::string at = "access_token"; + std::vector<std::string> scopes(CreateTestScopes()); + + CreateFlow(rt, std::string(), scopes); + SetupAccessTokenFetcher(rt, scopes); + TestURLFetcher* url_fetcher = SetupApiCall(true, net::HTTP_OK); + EXPECT_CALL(*flow_, ProcessApiCallSuccess(url_fetcher)); + flow_->Start(); + flow_->OnGetTokenSuccess( + at, + base::Time::Now() + base::TimeDelta::FromMinutes(3600)); + flow_->OnURLFetchComplete(url_fetcher); +} + +TEST_F(OAuth2ApiCallFlowTest, EmptyAccessTokenApiCallFails) { + std::string rt = "refresh_token"; + std::string at = "access_token"; + std::vector<std::string> scopes(CreateTestScopes()); + + CreateFlow(rt, std::string(), scopes); + SetupAccessTokenFetcher(rt, scopes); + TestURLFetcher* url_fetcher = SetupApiCall(false, net::HTTP_BAD_GATEWAY); + EXPECT_CALL(*flow_, ProcessApiCallFailure(url_fetcher)); + flow_->Start(); + flow_->OnGetTokenSuccess( + at, + base::Time::Now() + base::TimeDelta::FromMinutes(3600)); + flow_->OnURLFetchComplete(url_fetcher); +} + +TEST_F(OAuth2ApiCallFlowTest, EmptyAccessTokenNewTokenGenerationFails) { + std::string rt = "refresh_token"; + std::string at = "access_token"; + std::vector<std::string> scopes(CreateTestScopes()); + + CreateFlow(rt, std::string(), scopes); + SetupAccessTokenFetcher(rt, scopes); + GoogleServiceAuthError error( + GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS); + EXPECT_CALL(*flow_, ProcessMintAccessTokenFailure(error)); + flow_->Start(); + flow_->OnGetTokenFailure(error); +} + +TEST_F(OAuth2ApiCallFlowTest, CreateURLFetcher) { + std::string rt = "refresh_token"; + std::string at = "access_token"; + std::vector<std::string> scopes(CreateTestScopes()); + std::string body = CreateBody(); + GURL url(CreateApiUrl()); + + CreateFlow(rt, at, scopes); + scoped_ptr<TestURLFetcher> url_fetcher(SetupApiCall(true, net::HTTP_OK)); + flow_->CreateURLFetcher(); + HttpRequestHeaders headers; + url_fetcher->GetExtraRequestHeaders(&headers); + std::string auth_header; + EXPECT_TRUE(headers.GetHeader("Authorization", &auth_header)); + EXPECT_EQ("Bearer access_token", auth_header); + EXPECT_EQ(url, url_fetcher->GetOriginalURL()); + EXPECT_EQ(body, url_fetcher->upload_data()); +} diff --git a/chromium/google_apis/gaia/oauth2_mint_token_flow.cc b/chromium/google_apis/gaia/oauth2_mint_token_flow.cc new file mode 100644 index 00000000000..f66e0fb11d2 --- /dev/null +++ b/chromium/google_apis/gaia/oauth2_mint_token_flow.cc @@ -0,0 +1,282 @@ +// Copyright (c) 2012 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 "google_apis/gaia/oauth2_mint_token_flow.h" + +#include <string> +#include <vector> + +#include "base/basictypes.h" +#include "base/bind.h" +#include "base/command_line.h" +#include "base/json/json_reader.h" +#include "base/message_loop/message_loop.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "base/strings/utf_string_conversions.h" +#include "base/values.h" +#include "google_apis/gaia/gaia_urls.h" +#include "google_apis/gaia/google_service_auth_error.h" +#include "net/base/escape.h" +#include "net/url_request/url_fetcher.h" +#include "net/url_request/url_request_context_getter.h" +#include "net/url_request/url_request_status.h" + +using net::URLFetcher; +using net::URLRequestContextGetter; +using net::URLRequestStatus; + +namespace { + +static const char kForceValueFalse[] = "false"; +static const char kForceValueTrue[] = "true"; +static const char kResponseTypeValueNone[] = "none"; +static const char kResponseTypeValueToken[] = "token"; + +static const char kOAuth2IssueTokenBodyFormat[] = + "force=%s" + "&response_type=%s" + "&scope=%s" + "&client_id=%s" + "&origin=%s"; +static const char kIssueAdviceKey[] = "issueAdvice"; +static const char kIssueAdviceValueAuto[] = "auto"; +static const char kIssueAdviceValueConsent[] = "consent"; +static const char kAccessTokenKey[] = "token"; +static const char kConsentKey[] = "consent"; +static const char kExpiresInKey[] = "expiresIn"; +static const char kScopesKey[] = "scopes"; +static const char kDescriptionKey[] = "description"; +static const char kDetailKey[] = "detail"; +static const char kDetailSeparators[] = "\n"; +static const char kError[] = "error"; +static const char kMessage[] = "message"; + +static GoogleServiceAuthError CreateAuthError(const net::URLFetcher* source) { + URLRequestStatus status = source->GetStatus(); + if (status.status() == URLRequestStatus::CANCELED) { + return GoogleServiceAuthError(GoogleServiceAuthError::REQUEST_CANCELED); + } + if (status.status() == URLRequestStatus::FAILED) { + DLOG(WARNING) << "Server returned error: errno " << status.error(); + return GoogleServiceAuthError::FromConnectionError(status.error()); + } + + std::string response_body; + source->GetResponseAsString(&response_body); + scoped_ptr<Value> value(base::JSONReader::Read(response_body)); + DictionaryValue* response; + if (!value.get() || !value->GetAsDictionary(&response)) { + return GoogleServiceAuthError::FromUnexpectedServiceResponse( + base::StringPrintf( + "Not able to parse a JSON object from a service response. " + "HTTP Status of the response is: %d", source->GetResponseCode())); + } + DictionaryValue* error; + if (!response->GetDictionary(kError, &error)) { + return GoogleServiceAuthError::FromUnexpectedServiceResponse( + "Not able to find a detailed error in a service response."); + } + std::string message; + if (!error->GetString(kMessage, &message)) { + return GoogleServiceAuthError::FromUnexpectedServiceResponse( + "Not able to find an error message within a service error."); + } + return GoogleServiceAuthError::FromServiceError(message); +} + +} // namespace + +IssueAdviceInfoEntry::IssueAdviceInfoEntry() {} +IssueAdviceInfoEntry::~IssueAdviceInfoEntry() {} + +bool IssueAdviceInfoEntry::operator ==(const IssueAdviceInfoEntry& rhs) const { + return description == rhs.description && details == rhs.details; +} + +OAuth2MintTokenFlow::Parameters::Parameters() : mode(MODE_ISSUE_ADVICE) {} + +OAuth2MintTokenFlow::Parameters::Parameters( + const std::string& at, + const std::string& eid, + const std::string& cid, + const std::vector<std::string>& scopes_arg, + Mode mode_arg) + : access_token(at), + extension_id(eid), + client_id(cid), + scopes(scopes_arg), + mode(mode_arg) { +} + +OAuth2MintTokenFlow::Parameters::~Parameters() {} + +OAuth2MintTokenFlow::OAuth2MintTokenFlow(URLRequestContextGetter* context, + Delegate* delegate, + const Parameters& parameters) + : OAuth2ApiCallFlow(context, + std::string(), + parameters.access_token, + std::vector<std::string>()), + delegate_(delegate), + parameters_(parameters), + weak_factory_(this) {} + +OAuth2MintTokenFlow::~OAuth2MintTokenFlow() { } + +void OAuth2MintTokenFlow::ReportSuccess(const std::string& access_token, + int time_to_live) { + if (delegate_) + delegate_->OnMintTokenSuccess(access_token, time_to_live); + + // |this| may already be deleted. +} + +void OAuth2MintTokenFlow::ReportIssueAdviceSuccess( + const IssueAdviceInfo& issue_advice) { + if (delegate_) + delegate_->OnIssueAdviceSuccess(issue_advice); + + // |this| may already be deleted. +} + +void OAuth2MintTokenFlow::ReportFailure( + const GoogleServiceAuthError& error) { + if (delegate_) + delegate_->OnMintTokenFailure(error); + + // |this| may already be deleted. +} + +GURL OAuth2MintTokenFlow::CreateApiCallUrl() { + return GURL(GaiaUrls::GetInstance()->oauth2_issue_token_url()); +} + +std::string OAuth2MintTokenFlow::CreateApiCallBody() { + const char* force_value = + (parameters_.mode == MODE_MINT_TOKEN_FORCE || + parameters_.mode == MODE_RECORD_GRANT) + ? kForceValueTrue : kForceValueFalse; + const char* response_type_value = + (parameters_.mode == MODE_MINT_TOKEN_NO_FORCE || + parameters_.mode == MODE_MINT_TOKEN_FORCE) + ? kResponseTypeValueToken : kResponseTypeValueNone; + return base::StringPrintf( + kOAuth2IssueTokenBodyFormat, + net::EscapeUrlEncodedData(force_value, true).c_str(), + net::EscapeUrlEncodedData(response_type_value, true).c_str(), + net::EscapeUrlEncodedData( + JoinString(parameters_.scopes, ' '), true).c_str(), + net::EscapeUrlEncodedData(parameters_.client_id, true).c_str(), + net::EscapeUrlEncodedData(parameters_.extension_id, true).c_str()); +} + +void OAuth2MintTokenFlow::ProcessApiCallSuccess( + const net::URLFetcher* source) { + std::string response_body; + source->GetResponseAsString(&response_body); + scoped_ptr<base::Value> value(base::JSONReader::Read(response_body)); + base::DictionaryValue* dict = NULL; + if (!value.get() || !value->GetAsDictionary(&dict)) { + ReportFailure(GoogleServiceAuthError::FromUnexpectedServiceResponse( + "Not able to parse a JSON object from a service response.")); + return; + } + + std::string issue_advice_value; + if (!dict->GetString(kIssueAdviceKey, &issue_advice_value)) { + ReportFailure(GoogleServiceAuthError::FromUnexpectedServiceResponse( + "Not able to find an issueAdvice in a service response.")); + return; + } + if (issue_advice_value == kIssueAdviceValueConsent) { + IssueAdviceInfo issue_advice; + if (ParseIssueAdviceResponse(dict, &issue_advice)) + ReportIssueAdviceSuccess(issue_advice); + else + ReportFailure(GoogleServiceAuthError::FromUnexpectedServiceResponse( + "Not able to parse the contents of consent " + "from a service response.")); + } else { + std::string access_token; + int time_to_live; + if (ParseMintTokenResponse(dict, &access_token, &time_to_live)) + ReportSuccess(access_token, time_to_live); + else + ReportFailure(GoogleServiceAuthError::FromUnexpectedServiceResponse( + "Not able to parse the contents of access token " + "from a service response.")); + } + + // |this| may be deleted! +} + +void OAuth2MintTokenFlow::ProcessApiCallFailure( + const net::URLFetcher* source) { + ReportFailure(CreateAuthError(source)); +} +void OAuth2MintTokenFlow::ProcessNewAccessToken( + const std::string& access_token) { + // We don't currently store new access tokens. We generate one every time. + // So we have nothing to do here. + return; +} +void OAuth2MintTokenFlow::ProcessMintAccessTokenFailure( + const GoogleServiceAuthError& error) { + ReportFailure(error); +} + +// static +bool OAuth2MintTokenFlow::ParseMintTokenResponse( + const base::DictionaryValue* dict, std::string* access_token, + int* time_to_live) { + CHECK(dict); + CHECK(access_token); + CHECK(time_to_live); + std::string ttl_string; + return dict->GetString(kExpiresInKey, &ttl_string) && + base::StringToInt(ttl_string, time_to_live) && + dict->GetString(kAccessTokenKey, access_token); +} + +// static +bool OAuth2MintTokenFlow::ParseIssueAdviceResponse( + const base::DictionaryValue* dict, IssueAdviceInfo* issue_advice) { + CHECK(dict); + CHECK(issue_advice); + + const base::DictionaryValue* consent_dict = NULL; + if (!dict->GetDictionary(kConsentKey, &consent_dict)) + return false; + + const base::ListValue* scopes_list = NULL; + if (!consent_dict->GetList(kScopesKey, &scopes_list)) + return false; + + bool success = true; + for (size_t index = 0; index < scopes_list->GetSize(); ++index) { + const base::DictionaryValue* scopes_entry = NULL; + IssueAdviceInfoEntry entry; + string16 detail; + if (!scopes_list->GetDictionary(index, &scopes_entry) || + !scopes_entry->GetString(kDescriptionKey, &entry.description) || + !scopes_entry->GetString(kDetailKey, &detail)) { + success = false; + break; + } + + TrimWhitespace(entry.description, TRIM_ALL, &entry.description); + static const string16 detail_separators = ASCIIToUTF16(kDetailSeparators); + Tokenize(detail, detail_separators, &entry.details); + for (size_t i = 0; i < entry.details.size(); i++) + TrimWhitespace(entry.details[i], TRIM_ALL, &entry.details[i]); + issue_advice->push_back(entry); + } + + if (!success) + issue_advice->clear(); + + return success; +} diff --git a/chromium/google_apis/gaia/oauth2_mint_token_flow.h b/chromium/google_apis/gaia/oauth2_mint_token_flow.h new file mode 100644 index 00000000000..5bb751d6c71 --- /dev/null +++ b/chromium/google_apis/gaia/oauth2_mint_token_flow.h @@ -0,0 +1,147 @@ +// Copyright (c) 2012 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 GOOGLE_APIS_GAIA_OAUTH2_MINT_TOKEN_FLOW_H_ +#define GOOGLE_APIS_GAIA_OAUTH2_MINT_TOKEN_FLOW_H_ + +#include <string> +#include <vector> + +#include "base/memory/weak_ptr.h" +#include "base/strings/string16.h" +#include "google_apis/gaia/oauth2_api_call_flow.h" + +class GoogleServiceAuthError; +class OAuth2MintTokenFlowTest; + +namespace base { +class DictionaryValue; +} + +namespace content { +class URLFetcher; +} + +namespace net { +class URLRequestContextGetter; +} + +// IssueAdvice: messages to show to the user to get a user's approval. +// The structure is as follows: +// * Description 1 +// - Detail 1.1 +// - Details 1.2 +// * Description 2 +// - Detail 2.1 +// - Detail 2.2 +// - Detail 2.3 +// * Description 3 +// - Detail 3.1 +struct IssueAdviceInfoEntry { + public: + IssueAdviceInfoEntry(); + ~IssueAdviceInfoEntry(); + + string16 description; + std::vector<string16> details; + + bool operator==(const IssueAdviceInfoEntry& rhs) const; +}; + +typedef std::vector<IssueAdviceInfoEntry> IssueAdviceInfo; + +// This class implements the OAuth2 flow to Google to mint an OAuth2 +// token for the given client and the given set of scopes from the +// OAuthLogin scoped "master" OAuth2 token for the user logged in to +// Chrome. +class OAuth2MintTokenFlow : public OAuth2ApiCallFlow { + public: + // There are four differnt modes when minting a token to grant + // access to third-party app for a user. + enum Mode { + // Get the messages to display to the user without minting a token. + MODE_ISSUE_ADVICE, + // Record a grant but do not get a token back. + MODE_RECORD_GRANT, + // Mint a token for an existing grant. + MODE_MINT_TOKEN_NO_FORCE, + // Mint a token forcefully even if there is no existing grant. + MODE_MINT_TOKEN_FORCE, + }; + + // Parameters needed to mint a token. + struct Parameters { + public: + Parameters(); + Parameters(const std::string& at, + const std::string& eid, + const std::string& cid, + const std::vector<std::string>& scopes_arg, + Mode mode_arg); + ~Parameters(); + + std::string access_token; + std::string extension_id; + std::string client_id; + std::vector<std::string> scopes; + Mode mode; + }; + + class Delegate { + public: + virtual void OnMintTokenSuccess(const std::string& access_token, + int time_to_live) {} + virtual void OnIssueAdviceSuccess(const IssueAdviceInfo& issue_advice) {} + virtual void OnMintTokenFailure(const GoogleServiceAuthError& error) {} + + protected: + virtual ~Delegate() {} + }; + + OAuth2MintTokenFlow(net::URLRequestContextGetter* context, + Delegate* delegate, + const Parameters& parameters); + virtual ~OAuth2MintTokenFlow(); + + protected: + // Implementation of template methods in OAuth2ApiCallFlow. + virtual GURL CreateApiCallUrl() OVERRIDE; + virtual std::string CreateApiCallBody() OVERRIDE; + + virtual void ProcessApiCallSuccess( + const net::URLFetcher* source) OVERRIDE; + virtual void ProcessApiCallFailure( + const net::URLFetcher* source) OVERRIDE; + virtual void ProcessNewAccessToken(const std::string& access_token) OVERRIDE; + virtual void ProcessMintAccessTokenFailure( + const GoogleServiceAuthError& error) OVERRIDE; + + private: + friend class OAuth2MintTokenFlowTest; + FRIEND_TEST_ALL_PREFIXES(OAuth2MintTokenFlowTest, CreateApiCallBody); + FRIEND_TEST_ALL_PREFIXES(OAuth2MintTokenFlowTest, ParseIssueAdviceResponse); + FRIEND_TEST_ALL_PREFIXES(OAuth2MintTokenFlowTest, ParseMintTokenResponse); + FRIEND_TEST_ALL_PREFIXES(OAuth2MintTokenFlowTest, ProcessApiCallSuccess); + FRIEND_TEST_ALL_PREFIXES(OAuth2MintTokenFlowTest, ProcessApiCallFailure); + FRIEND_TEST_ALL_PREFIXES(OAuth2MintTokenFlowTest, + ProcessMintAccessTokenFailure); + + void ReportSuccess(const std::string& access_token, int time_to_live); + void ReportIssueAdviceSuccess(const IssueAdviceInfo& issue_advice); + void ReportFailure(const GoogleServiceAuthError& error); + + static bool ParseIssueAdviceResponse( + const base::DictionaryValue* dict, IssueAdviceInfo* issue_advice); + static bool ParseMintTokenResponse( + const base::DictionaryValue* dict, std::string* access_token, + int* time_to_live); + + Delegate* delegate_; + Parameters parameters_; + base::WeakPtrFactory<OAuth2MintTokenFlow> weak_factory_; + + DISALLOW_COPY_AND_ASSIGN(OAuth2MintTokenFlow); +}; + +#endif // GOOGLE_APIS_GAIA_OAUTH2_MINT_TOKEN_FLOW_H_ diff --git a/chromium/google_apis/gaia/oauth2_mint_token_flow_unittest.cc b/chromium/google_apis/gaia/oauth2_mint_token_flow_unittest.cc new file mode 100644 index 00000000000..7cd3d672c62 --- /dev/null +++ b/chromium/google_apis/gaia/oauth2_mint_token_flow_unittest.cc @@ -0,0 +1,364 @@ +// Copyright (c) 2012 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. +// +// A complete set of unit tests for OAuth2MintTokenFlow. + +#include <string> +#include <vector> + +#include "base/json/json_reader.h" +#include "base/memory/scoped_ptr.h" +#include "base/strings/utf_string_conversions.h" +#include "base/values.h" +#include "google_apis/gaia/google_service_auth_error.h" +#include "google_apis/gaia/oauth2_mint_token_flow.h" +#include "net/url_request/test_url_fetcher_factory.h" +#include "net/url_request/url_request_status.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using net::TestURLFetcher; +using net::URLFetcher; +using net::URLRequestStatus; +using testing::_; +using testing::StrictMock; + +namespace { + +static const char kValidTokenResponse[] = + "{" + " \"token\": \"at1\"," + " \"issueAdvice\": \"Auto\"," + " \"expiresIn\": \"3600\"" + "}"; +static const char kTokenResponseNoAccessToken[] = + "{" + " \"issueAdvice\": \"Auto\"" + "}"; + +static const char kValidIssueAdviceResponse[] = + "{" + " \"issueAdvice\": \"consent\"," + " \"consent\": {" + " \"oauthClient\": {" + " \"name\": \"Test app\"," + " \"iconUri\": \"\"," + " \"developerEmail\": \"munjal@chromium.org\"" + " }," + " \"scopes\": [" + " {" + " \"description\": \"Manage your calendars\"," + " \"detail\": \"\nView and manage your calendars\n\"" + " }," + " {" + " \"description\": \"Manage your documents\"," + " \"detail\": \"\nView your documents\nUpload new documents\n\"" + " }" + " ]" + " }" + "}"; + +static const char kIssueAdviceResponseNoDescription[] = + "{" + " \"issueAdvice\": \"consent\"," + " \"consent\": {" + " \"oauthClient\": {" + " \"name\": \"Test app\"," + " \"iconUri\": \"\"," + " \"developerEmail\": \"munjal@chromium.org\"" + " }," + " \"scopes\": [" + " {" + " \"description\": \"Manage your calendars\"," + " \"detail\": \"\nView and manage your calendars\n\"" + " }," + " {" + " \"detail\": \"\nView your documents\nUpload new documents\n\"" + " }" + " ]" + " }" + "}"; + +static const char kIssueAdviceResponseNoDetail[] = + "{" + " \"issueAdvice\": \"consent\"," + " \"consent\": {" + " \"oauthClient\": {" + " \"name\": \"Test app\"," + " \"iconUri\": \"\"," + " \"developerEmail\": \"munjal@chromium.org\"" + " }," + " \"scopes\": [" + " {" + " \"description\": \"Manage your calendars\"," + " \"detail\": \"\nView and manage your calendars\n\"" + " }," + " {" + " \"description\": \"Manage your documents\"" + " }" + " ]" + " }" + "}"; + +std::vector<std::string> CreateTestScopes() { + std::vector<std::string> scopes; + scopes.push_back("http://scope1"); + scopes.push_back("http://scope2"); + return scopes; +} + +static IssueAdviceInfo CreateIssueAdvice() { + IssueAdviceInfo ia; + IssueAdviceInfoEntry e1; + e1.description = ASCIIToUTF16("Manage your calendars"); + e1.details.push_back(ASCIIToUTF16("View and manage your calendars")); + ia.push_back(e1); + IssueAdviceInfoEntry e2; + e2.description = ASCIIToUTF16("Manage your documents"); + e2.details.push_back(ASCIIToUTF16("View your documents")); + e2.details.push_back(ASCIIToUTF16("Upload new documents")); + ia.push_back(e2); + return ia; +} + +class MockDelegate : public OAuth2MintTokenFlow::Delegate { + public: + MockDelegate() {} + ~MockDelegate() {} + + MOCK_METHOD2(OnMintTokenSuccess, void(const std::string& access_token, + int time_to_live)); + MOCK_METHOD1(OnIssueAdviceSuccess, + void (const IssueAdviceInfo& issue_advice)); + MOCK_METHOD1(OnMintTokenFailure, + void(const GoogleServiceAuthError& error)); +}; + +class MockMintTokenFlow : public OAuth2MintTokenFlow { + public: + explicit MockMintTokenFlow(MockDelegate* delegate, + const OAuth2MintTokenFlow::Parameters& parameters ) + : OAuth2MintTokenFlow(NULL, delegate, parameters) {} + ~MockMintTokenFlow() {} + + MOCK_METHOD0(CreateAccessTokenFetcher, OAuth2AccessTokenFetcher*()); +}; + +} // namespace + +class OAuth2MintTokenFlowTest : public testing::Test { + public: + OAuth2MintTokenFlowTest() {} + virtual ~OAuth2MintTokenFlowTest() { } + + protected: + void CreateFlow(OAuth2MintTokenFlow::Mode mode) { + return CreateFlow(&delegate_, mode); + } + + void CreateFlow(MockDelegate* delegate, + OAuth2MintTokenFlow::Mode mode) { + std::string rt = "refresh_token"; + std::string ext_id = "ext1"; + std::string client_id = "client1"; + std::vector<std::string> scopes(CreateTestScopes()); + flow_.reset(new MockMintTokenFlow( + delegate, + OAuth2MintTokenFlow::Parameters(rt, ext_id, client_id, scopes, mode))); + } + + // Helper to parse the given string to DictionaryValue. + static base::DictionaryValue* ParseJson(const std::string& str) { + scoped_ptr<Value> value(base::JSONReader::Read(str)); + EXPECT_TRUE(value.get()); + EXPECT_EQ(Value::TYPE_DICTIONARY, value->GetType()); + return static_cast<base::DictionaryValue*>(value.release()); + } + + scoped_ptr<MockMintTokenFlow> flow_; + StrictMock<MockDelegate> delegate_; +}; + +TEST_F(OAuth2MintTokenFlowTest, CreateApiCallBody) { + { // Issue advice mode. + CreateFlow(OAuth2MintTokenFlow::MODE_ISSUE_ADVICE); + std::string body = flow_->CreateApiCallBody(); + std::string expected_body( + "force=false" + "&response_type=none" + "&scope=http://scope1+http://scope2" + "&client_id=client1" + "&origin=ext1"); + EXPECT_EQ(expected_body, body); + } + { // Record grant mode. + CreateFlow(OAuth2MintTokenFlow::MODE_RECORD_GRANT); + std::string body = flow_->CreateApiCallBody(); + std::string expected_body( + "force=true" + "&response_type=none" + "&scope=http://scope1+http://scope2" + "&client_id=client1" + "&origin=ext1"); + EXPECT_EQ(expected_body, body); + } + { // Mint token no force mode. + CreateFlow(OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE); + std::string body = flow_->CreateApiCallBody(); + std::string expected_body( + "force=false" + "&response_type=token" + "&scope=http://scope1+http://scope2" + "&client_id=client1" + "&origin=ext1"); + EXPECT_EQ(expected_body, body); + } + { // Mint token force mode. + CreateFlow(OAuth2MintTokenFlow::MODE_MINT_TOKEN_FORCE); + std::string body = flow_->CreateApiCallBody(); + std::string expected_body( + "force=true" + "&response_type=token" + "&scope=http://scope1+http://scope2" + "&client_id=client1" + "&origin=ext1"); + EXPECT_EQ(expected_body, body); + } +} + +TEST_F(OAuth2MintTokenFlowTest, ParseMintTokenResponse) { + { // Access token missing. + scoped_ptr<base::DictionaryValue> json( + ParseJson(kTokenResponseNoAccessToken)); + std::string at; + int ttl; + EXPECT_FALSE(OAuth2MintTokenFlow::ParseMintTokenResponse(json.get(), &at, + &ttl)); + EXPECT_TRUE(at.empty()); + } + { // All good. + scoped_ptr<base::DictionaryValue> json(ParseJson(kValidTokenResponse)); + std::string at; + int ttl; + EXPECT_TRUE(OAuth2MintTokenFlow::ParseMintTokenResponse(json.get(), &at, + &ttl)); + EXPECT_EQ("at1", at); + EXPECT_EQ(3600, ttl); + } +} + +TEST_F(OAuth2MintTokenFlowTest, ParseIssueAdviceResponse) { + { // Description missing. + scoped_ptr<base::DictionaryValue> json( + ParseJson(kIssueAdviceResponseNoDescription)); + IssueAdviceInfo ia; + EXPECT_FALSE(OAuth2MintTokenFlow::ParseIssueAdviceResponse( + json.get(), &ia)); + EXPECT_TRUE(ia.empty()); + } + { // Detail missing. + scoped_ptr<base::DictionaryValue> json( + ParseJson(kIssueAdviceResponseNoDetail)); + IssueAdviceInfo ia; + EXPECT_FALSE(OAuth2MintTokenFlow::ParseIssueAdviceResponse( + json.get(), &ia)); + EXPECT_TRUE(ia.empty()); + } + { // All good. + scoped_ptr<base::DictionaryValue> json( + ParseJson(kValidIssueAdviceResponse)); + IssueAdviceInfo ia; + EXPECT_TRUE(OAuth2MintTokenFlow::ParseIssueAdviceResponse( + json.get(), &ia)); + IssueAdviceInfo ia_expected(CreateIssueAdvice()); + EXPECT_EQ(ia_expected, ia); + } +} + +TEST_F(OAuth2MintTokenFlowTest, ProcessApiCallSuccess) { + { // No body. + TestURLFetcher url_fetcher(1, GURL("http://www.google.com"), NULL); + url_fetcher.SetResponseString(std::string()); + CreateFlow(OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE); + EXPECT_CALL(delegate_, OnMintTokenFailure(_)); + flow_->ProcessApiCallSuccess(&url_fetcher); + } + { // Bad json. + TestURLFetcher url_fetcher(1, GURL("http://www.google.com"), NULL); + url_fetcher.SetResponseString("foo"); + CreateFlow(OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE); + EXPECT_CALL(delegate_, OnMintTokenFailure(_)); + flow_->ProcessApiCallSuccess(&url_fetcher); + } + { // Valid json: no access token. + TestURLFetcher url_fetcher(1, GURL("http://www.google.com"), NULL); + url_fetcher.SetResponseString(kTokenResponseNoAccessToken); + CreateFlow(OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE); + EXPECT_CALL(delegate_, OnMintTokenFailure(_)); + flow_->ProcessApiCallSuccess(&url_fetcher); + } + { // Valid json: good token response. + TestURLFetcher url_fetcher(1, GURL("http://www.google.com"), NULL); + url_fetcher.SetResponseString(kValidTokenResponse); + CreateFlow(OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE); + EXPECT_CALL(delegate_, OnMintTokenSuccess("at1", 3600)); + flow_->ProcessApiCallSuccess(&url_fetcher); + } + { // Valid json: no description. + TestURLFetcher url_fetcher(1, GURL("http://www.google.com"), NULL); + url_fetcher.SetResponseString(kIssueAdviceResponseNoDescription); + CreateFlow(OAuth2MintTokenFlow::MODE_ISSUE_ADVICE); + EXPECT_CALL(delegate_, OnMintTokenFailure(_)); + flow_->ProcessApiCallSuccess(&url_fetcher); + } + { // Valid json: no detail. + TestURLFetcher url_fetcher(1, GURL("http://www.google.com"), NULL); + url_fetcher.SetResponseString(kIssueAdviceResponseNoDetail); + CreateFlow(OAuth2MintTokenFlow::MODE_ISSUE_ADVICE); + EXPECT_CALL(delegate_, OnMintTokenFailure(_)); + flow_->ProcessApiCallSuccess(&url_fetcher); + } + { // Valid json: good issue advice response. + TestURLFetcher url_fetcher(1, GURL("http://www.google.com"), NULL); + url_fetcher.SetResponseString(kValidIssueAdviceResponse); + CreateFlow(OAuth2MintTokenFlow::MODE_ISSUE_ADVICE); + IssueAdviceInfo ia(CreateIssueAdvice()); + EXPECT_CALL(delegate_, OnIssueAdviceSuccess(ia)); + flow_->ProcessApiCallSuccess(&url_fetcher); + } +} + +TEST_F(OAuth2MintTokenFlowTest, ProcessApiCallFailure) { + { // Null delegate should work fine. + TestURLFetcher url_fetcher(1, GURL("http://www.google.com"), NULL); + url_fetcher.set_status(URLRequestStatus(URLRequestStatus::FAILED, 101)); + CreateFlow(NULL, OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE); + flow_->ProcessApiCallFailure(&url_fetcher); + } + + { // Non-null delegate. + TestURLFetcher url_fetcher(1, GURL("http://www.google.com"), NULL); + url_fetcher.set_status(URLRequestStatus(URLRequestStatus::FAILED, 101)); + CreateFlow(OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE); + EXPECT_CALL(delegate_, OnMintTokenFailure(_)); + flow_->ProcessApiCallFailure(&url_fetcher); + } +} + +TEST_F(OAuth2MintTokenFlowTest, ProcessMintAccessTokenFailure) { + { // Null delegate should work fine. + GoogleServiceAuthError error( + GoogleServiceAuthError::FromConnectionError(101)); + CreateFlow(NULL, OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE); + flow_->ProcessMintAccessTokenFailure(error); + } + + { // Non-null delegate. + GoogleServiceAuthError error( + GoogleServiceAuthError::FromConnectionError(101)); + CreateFlow(OAuth2MintTokenFlow::MODE_MINT_TOKEN_NO_FORCE); + EXPECT_CALL(delegate_, OnMintTokenFailure(error)); + flow_->ProcessMintAccessTokenFailure(error); + } +} diff --git a/chromium/google_apis/gaia/oauth_request_signer.cc b/chromium/google_apis/gaia/oauth_request_signer.cc new file mode 100644 index 00000000000..b16744cbccd --- /dev/null +++ b/chromium/google_apis/gaia/oauth_request_signer.cc @@ -0,0 +1,459 @@ +// Copyright (c) 2012 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 "google_apis/gaia/oauth_request_signer.h" + +#include <cctype> +#include <cstddef> +#include <cstdlib> +#include <cstring> +#include <ctime> +#include <map> +#include <string> + +#include "base/base64.h" +#include "base/format_macros.h" +#include "base/logging.h" +#include "base/rand_util.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "base/time/time.h" +#include "crypto/hmac.h" +#include "url/gurl.h" + +namespace { + +static const int kHexBase = 16; +static char kHexDigits[] = "0123456789ABCDEF"; +static const size_t kHmacDigestLength = 20; +static const int kMaxNonceLength = 30; +static const int kMinNonceLength = 15; + +static const char kOAuthConsumerKeyLabel[] = "oauth_consumer_key"; +static const char kOAuthConsumerSecretLabel[] = "oauth_consumer_secret"; +static const char kOAuthNonceCharacters[] = + "abcdefghijklmnopqrstuvwyz" + "ABCDEFGHIJKLMNOPQRSTUVWYZ" + "0123456789_"; +static const char kOAuthNonceLabel[] = "oauth_nonce"; +static const char kOAuthSignatureLabel[] = "oauth_signature"; +static const char kOAuthSignatureMethodLabel[] = "oauth_signature_method"; +static const char kOAuthTimestampLabel[] = "oauth_timestamp"; +static const char kOAuthTokenLabel[] = "oauth_token"; +static const char kOAuthTokenSecretLabel[] = "oauth_token_secret"; +static const char kOAuthVersion[] = "1.0"; +static const char kOAuthVersionLabel[] = "oauth_version"; + +enum ParseQueryState { + START_STATE, + KEYWORD_STATE, + VALUE_STATE, +}; + +const std::string HttpMethodName(OAuthRequestSigner::HttpMethod method) { + switch (method) { + case OAuthRequestSigner::GET_METHOD: + return "GET"; + case OAuthRequestSigner::POST_METHOD: + return "POST"; + } + NOTREACHED(); + return std::string(); +} + +const std::string SignatureMethodName( + OAuthRequestSigner::SignatureMethod method) { + switch (method) { + case OAuthRequestSigner::HMAC_SHA1_SIGNATURE: + return "HMAC-SHA1"; + case OAuthRequestSigner::RSA_SHA1_SIGNATURE: + return "RSA-SHA1"; + case OAuthRequestSigner::PLAINTEXT_SIGNATURE: + return "PLAINTEXT"; + } + NOTREACHED(); + return std::string(); +} + +std::string BuildBaseString(const GURL& request_base_url, + OAuthRequestSigner::HttpMethod http_method, + const std::string& base_parameters) { + return base::StringPrintf("%s&%s&%s", + HttpMethodName(http_method).c_str(), + OAuthRequestSigner::Encode( + request_base_url.spec()).c_str(), + OAuthRequestSigner::Encode( + base_parameters).c_str()); +} + +std::string BuildBaseStringParameters( + const OAuthRequestSigner::Parameters& parameters) { + std::string result; + OAuthRequestSigner::Parameters::const_iterator cursor; + OAuthRequestSigner::Parameters::const_iterator limit; + bool first = true; + for (cursor = parameters.begin(), limit = parameters.end(); + cursor != limit; + ++cursor) { + if (first) + first = false; + else + result += '&'; + result += OAuthRequestSigner::Encode(cursor->first); + result += '='; + result += OAuthRequestSigner::Encode(cursor->second); + } + return result; +} + +std::string GenerateNonce() { + char result[kMaxNonceLength + 1]; + int length = base::RandUint64() % (kMaxNonceLength - kMinNonceLength + 1) + + kMinNonceLength; + result[length] = '\0'; + for (int index = 0; index < length; ++index) + result[index] = kOAuthNonceCharacters[ + base::RandUint64() % (sizeof(kOAuthNonceCharacters) - 1)]; + return result; +} + +std::string GenerateTimestamp() { + return base::StringPrintf( + "%" PRId64, + (base::Time::NowFromSystemTime() - base::Time::UnixEpoch()).InSeconds()); +} + +// Creates a string-to-string, keyword-value map from a parameter/query string +// that uses ampersand (&) to seperate paris and equals (=) to seperate +// keyword from value. +bool ParseQuery(const std::string& query, + OAuthRequestSigner::Parameters* parameters_result) { + std::string::const_iterator cursor; + std::string keyword; + std::string::const_iterator limit; + OAuthRequestSigner::Parameters parameters; + ParseQueryState state; + std::string value; + + state = START_STATE; + for (cursor = query.begin(), limit = query.end(); + cursor != limit; + ++cursor) { + char character = *cursor; + switch (state) { + case KEYWORD_STATE: + switch (character) { + case '&': + parameters[keyword] = value; + keyword = ""; + value = ""; + state = START_STATE; + break; + case '=': + state = VALUE_STATE; + break; + default: + keyword += character; + } + break; + case START_STATE: + switch (character) { + case '&': // Intentionally falling through + case '=': + return false; + default: + keyword += character; + state = KEYWORD_STATE; + } + break; + case VALUE_STATE: + switch (character) { + case '=': + return false; + case '&': + parameters[keyword] = value; + keyword = ""; + value = ""; + state = START_STATE; + break; + default: + value += character; + } + break; + } + } + switch (state) { + case START_STATE: + break; + case KEYWORD_STATE: // Intentionally falling through + case VALUE_STATE: + parameters[keyword] = value; + break; + default: + NOTREACHED(); + } + *parameters_result = parameters; + return true; +} + +// Creates the value for the oauth_signature parameter when the +// oauth_signature_method is HMAC-SHA1. +bool SignHmacSha1(const std::string& text, + const std::string& key, + std::string* signature_return) { + crypto::HMAC hmac(crypto::HMAC::SHA1); + DCHECK(hmac.DigestLength() == kHmacDigestLength); + unsigned char digest[kHmacDigestLength]; + bool result = hmac.Init(key) && + hmac.Sign(text, digest, kHmacDigestLength) && + base::Base64Encode(std::string(reinterpret_cast<const char*>(digest), + kHmacDigestLength), + signature_return); + return result; +} + +// Creates the value for the oauth_signature parameter when the +// oauth_signature_method is PLAINTEXT. +// +// Not yet implemented, and might never be. +bool SignPlaintext(const std::string& text, + const std::string& key, + std::string* result) { + NOTIMPLEMENTED(); + return false; +} + +// Creates the value for the oauth_signature parameter when the +// oauth_signature_method is RSA-SHA1. +// +// Not yet implemented, and might never be. +bool SignRsaSha1(const std::string& text, + const std::string& key, + std::string* result) { + NOTIMPLEMENTED(); + return false; +} + +// Adds parameters that are required by OAuth added as needed to |parameters|. +void PrepareParameters(OAuthRequestSigner::Parameters* parameters, + OAuthRequestSigner::SignatureMethod signature_method, + OAuthRequestSigner::HttpMethod http_method, + const std::string& consumer_key, + const std::string& token_key) { + if (parameters->find(kOAuthNonceLabel) == parameters->end()) + (*parameters)[kOAuthNonceLabel] = GenerateNonce(); + + if (parameters->find(kOAuthTimestampLabel) == parameters->end()) + (*parameters)[kOAuthTimestampLabel] = GenerateTimestamp(); + + (*parameters)[kOAuthConsumerKeyLabel] = consumer_key; + (*parameters)[kOAuthSignatureMethodLabel] = + SignatureMethodName(signature_method); + (*parameters)[kOAuthTokenLabel] = token_key; + (*parameters)[kOAuthVersionLabel] = kOAuthVersion; +} + +// Implements shared signing logic, generating the signature and storing it in +// |parameters|. Returns true if the signature has been generated succesfully. +bool SignParameters(const GURL& request_base_url, + OAuthRequestSigner::SignatureMethod signature_method, + OAuthRequestSigner::HttpMethod http_method, + const std::string& consumer_key, + const std::string& consumer_secret, + const std::string& token_key, + const std::string& token_secret, + OAuthRequestSigner::Parameters* parameters) { + DCHECK(request_base_url.is_valid()); + PrepareParameters(parameters, signature_method, http_method, + consumer_key, token_key); + std::string base_parameters = BuildBaseStringParameters(*parameters); + std::string base = BuildBaseString(request_base_url, http_method, + base_parameters); + std::string key = consumer_secret + '&' + token_secret; + bool is_signed = false; + std::string signature; + switch (signature_method) { + case OAuthRequestSigner::HMAC_SHA1_SIGNATURE: + is_signed = SignHmacSha1(base, key, &signature); + break; + case OAuthRequestSigner::RSA_SHA1_SIGNATURE: + is_signed = SignRsaSha1(base, key, &signature); + break; + case OAuthRequestSigner::PLAINTEXT_SIGNATURE: + is_signed = SignPlaintext(base, key, &signature); + break; + default: + NOTREACHED(); + } + if (is_signed) + (*parameters)[kOAuthSignatureLabel] = signature; + return is_signed; +} + + +} // namespace + +// static +bool OAuthRequestSigner::Decode(const std::string& text, + std::string* decoded_text) { + std::string accumulator; + std::string::const_iterator cursor; + std::string::const_iterator limit; + for (limit = text.end(), cursor = text.begin(); cursor != limit; ++cursor) { + char character = *cursor; + if (character == '%') { + ++cursor; + if (cursor == limit) + return false; + char* first = strchr(kHexDigits, *cursor); + if (!first) + return false; + int high = first - kHexDigits; + DCHECK(high >= 0 && high < kHexBase); + + ++cursor; + if (cursor == limit) + return false; + char* second = strchr(kHexDigits, *cursor); + if (!second) + return false; + int low = second - kHexDigits; + DCHECK(low >= 0 || low < kHexBase); + + char decoded = static_cast<char>(high * kHexBase + low); + DCHECK(!(IsAsciiAlpha(decoded) || IsAsciiDigit(decoded))); + DCHECK(!(decoded && strchr("-._~", decoded))); + accumulator += decoded; + } else { + accumulator += character; + } + } + *decoded_text = accumulator; + return true; +} + +// static +std::string OAuthRequestSigner::Encode(const std::string& text) { + std::string result; + std::string::const_iterator cursor; + std::string::const_iterator limit; + for (limit = text.end(), cursor = text.begin(); cursor != limit; ++cursor) { + char character = *cursor; + if (IsAsciiAlpha(character) || IsAsciiDigit(character)) { + result += character; + } else { + switch (character) { + case '-': + case '.': + case '_': + case '~': + result += character; + break; + default: + unsigned char byte = static_cast<unsigned char>(character); + result = result + '%' + kHexDigits[byte / kHexBase] + + kHexDigits[byte % kHexBase]; + } + } + } + return result; +} + +// static +bool OAuthRequestSigner::ParseAndSign(const GURL& request_url_with_parameters, + SignatureMethod signature_method, + HttpMethod http_method, + const std::string& consumer_key, + const std::string& consumer_secret, + const std::string& token_key, + const std::string& token_secret, + std::string* result) { + DCHECK(request_url_with_parameters.is_valid()); + Parameters parameters; + if (request_url_with_parameters.has_query()) { + const std::string& query = request_url_with_parameters.query(); + if (!query.empty()) { + if (!ParseQuery(query, ¶meters)) + return false; + } + } + std::string spec = request_url_with_parameters.spec(); + std::string url_without_parameters = spec; + std::string::size_type question = spec.find("?"); + if (question != std::string::npos) + url_without_parameters = spec.substr(0,question); + return SignURL(GURL(url_without_parameters), parameters, signature_method, + http_method, consumer_key, consumer_secret, token_key, + token_secret, result); +} + +// static +bool OAuthRequestSigner::SignURL( + const GURL& request_base_url, + const Parameters& request_parameters, + SignatureMethod signature_method, + HttpMethod http_method, + const std::string& consumer_key, + const std::string& consumer_secret, + const std::string& token_key, + const std::string& token_secret, + std::string* signed_text_return) { + DCHECK(request_base_url.is_valid()); + Parameters parameters(request_parameters); + bool is_signed = SignParameters(request_base_url, signature_method, + http_method, consumer_key, consumer_secret, + token_key, token_secret, ¶meters); + if (is_signed) { + std::string signed_text; + switch (http_method) { + case GET_METHOD: + signed_text = request_base_url.spec() + '?'; + // Intentionally falling through + case POST_METHOD: + signed_text += BuildBaseStringParameters(parameters); + break; + default: + NOTREACHED(); + } + *signed_text_return = signed_text; + } + return is_signed; +} + +// static +bool OAuthRequestSigner::SignAuthHeader( + const GURL& request_base_url, + const Parameters& request_parameters, + SignatureMethod signature_method, + HttpMethod http_method, + const std::string& consumer_key, + const std::string& consumer_secret, + const std::string& token_key, + const std::string& token_secret, + std::string* signed_text_return) { + DCHECK(request_base_url.is_valid()); + Parameters parameters(request_parameters); + bool is_signed = SignParameters(request_base_url, signature_method, + http_method, consumer_key, consumer_secret, + token_key, token_secret, ¶meters); + if (is_signed) { + std::string signed_text = "OAuth "; + bool first = true; + for (Parameters::const_iterator param = parameters.begin(); + param != parameters.end(); + ++param) { + if (first) + first = false; + else + signed_text += ", "; + signed_text += + base::StringPrintf( + "%s=\"%s\"", + OAuthRequestSigner::Encode(param->first).c_str(), + OAuthRequestSigner::Encode(param->second).c_str()); + } + *signed_text_return = signed_text; + } + return is_signed; +} diff --git a/chromium/google_apis/gaia/oauth_request_signer.h b/chromium/google_apis/gaia/oauth_request_signer.h new file mode 100644 index 00000000000..3b91d4dbace --- /dev/null +++ b/chromium/google_apis/gaia/oauth_request_signer.h @@ -0,0 +1,100 @@ +// Copyright (c) 2011 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 GOOGLE_APIS_GAIA_OAUTH_REQUEST_SIGNER_H_ +#define GOOGLE_APIS_GAIA_OAUTH_REQUEST_SIGNER_H_ + +#include <map> +#include <string> + +#include "base/basictypes.h" + +class GURL; + +// Implements the OAuth request signing process as described here: +// http://oauth.net/core/1.0/#signing_process +// +// NOTE: Currently the only supported SignatureMethod is HMAC_SHA1_SIGNATURE +class OAuthRequestSigner { + public: + enum SignatureMethod { + HMAC_SHA1_SIGNATURE, + RSA_SHA1_SIGNATURE, + PLAINTEXT_SIGNATURE + }; + + enum HttpMethod { + GET_METHOD, + POST_METHOD + }; + + typedef std::map<std::string,std::string> Parameters; + + // Percent encoding and decoding for OAuth. + // + // The form of percent encoding used for OAuth request signing is very + // specific and strict. See http://oauth.net/core/1.0/#encoding_parameters. + // This definition is considered the current standard as of January 2005. + // While as of July 2011 many systems to do not comply, any valid OAuth + // implementation must comply. + // + // Any character which is in the "unreserved set" MUST NOT be encoded. + // All other characters MUST be encoded. + // + // The unreserved set is comprised of the alphanumeric characters and these + // others: + // - minus (-) + // - period (.) + // - underscore (_) + // - tilde (~) + static bool Decode(const std::string& text, std::string* decoded_text); + static std::string Encode(const std::string& text); + + // Signs a request specified as URL string, complete with parameters. + // + // If HttpMethod is GET_METHOD, the signed result is the full URL, otherwise + // it is the request parameters, including the oauth_signature field. + static bool ParseAndSign(const GURL& request_url_with_parameters, + SignatureMethod signature_method, + HttpMethod http_method, + const std::string& consumer_key, + const std::string& consumer_secret, + const std::string& token_key, + const std::string& token_secret, + std::string* signed_result); + + // Signs a request specified as the combination of a base URL string, with + // parameters included in a separate map data structure. NOTE: The base URL + // string must not contain a question mark (?) character. If it does, + // you can use ParseAndSign() instead. + // + // If HttpMethod is GET_METHOD, the signed result is the full URL, otherwise + // it is the request parameters, including the oauth_signature field. + static bool SignURL(const GURL& request_base_url, + const Parameters& parameters, + SignatureMethod signature_method, + HttpMethod http_method, + const std::string& consumer_key, + const std::string& consumer_secret, + const std::string& token_key, + const std::string& token_secret, + std::string* signed_result); + + // Similar to SignURL(), but the returned string is not a URL, but the payload + // to for an HTTP Authorization header. + static bool SignAuthHeader(const GURL& request_base_url, + const Parameters& parameters, + SignatureMethod signature_method, + HttpMethod http_method, + const std::string& consumer_key, + const std::string& consumer_secret, + const std::string& token_key, + const std::string& token_secret, + std::string* signed_result); + + private: + DISALLOW_IMPLICIT_CONSTRUCTORS(OAuthRequestSigner); +}; + +#endif // GOOGLE_APIS_GAIA_OAUTH_REQUEST_SIGNER_H_ diff --git a/chromium/google_apis/gaia/oauth_request_signer_unittest.cc b/chromium/google_apis/gaia/oauth_request_signer_unittest.cc new file mode 100644 index 00000000000..30358e0aa54 --- /dev/null +++ b/chromium/google_apis/gaia/oauth_request_signer_unittest.cc @@ -0,0 +1,323 @@ +// Copyright (c) 2011 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 "google_apis/gaia/oauth_request_signer.h" + +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +// This value is used to seed the PRNG at the beginning of a sequence of +// operations to produce a repeatable sequence. +#define RANDOM_SEED (0x69E3C47D) + +TEST(OAuthRequestSignerTest, Encode) { + ASSERT_EQ(OAuthRequestSigner::Encode("ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789" + "-._~"), + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789" + "-._~"); + ASSERT_EQ(OAuthRequestSigner::Encode( + "https://accounts.google.com/OAuthLogin"), + "https%3A%2F%2Faccounts.google.com%2FOAuthLogin"); + ASSERT_EQ(OAuthRequestSigner::Encode("%"), "%25"); + ASSERT_EQ(OAuthRequestSigner::Encode("%25"), "%2525"); + ASSERT_EQ(OAuthRequestSigner::Encode( + "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed " + "do eiusmod tempor incididunt ut labore et dolore magna " + "aliqua. Ut enim ad minim veniam, quis nostrud exercitation " + "ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis " + "aute irure dolor in reprehenderit in voluptate velit esse " + "cillum dolore eu fugiat nulla pariatur. Excepteur sint " + "occaecat cupidatat non proident, sunt in culpa qui officia " + "deserunt mollit anim id est laborum."), + "Lorem%20ipsum%20dolor%20sit%20amet%2C%20consectetur%20" + "adipisicing%20elit%2C%20sed%20do%20eiusmod%20tempor%20" + "incididunt%20ut%20labore%20et%20dolore%20magna%20aliqua.%20Ut%20" + "enim%20ad%20minim%20veniam%2C%20quis%20nostrud%20exercitation%20" + "ullamco%20laboris%20nisi%20ut%20aliquip%20ex%20ea%20commodo%20" + "consequat.%20Duis%20aute%20irure%20dolor%20in%20reprehenderit%20" + "in%20voluptate%20velit%20esse%20cillum%20dolore%20eu%20fugiat%20" + "nulla%20pariatur.%20Excepteur%20sint%20occaecat%20cupidatat%20" + "non%20proident%2C%20sunt%20in%20culpa%20qui%20officia%20" + "deserunt%20mollit%20anim%20id%20est%20laborum."); + ASSERT_EQ(OAuthRequestSigner::Encode("!5}&QF~0R-Ecy[?2Cig>6g=;hH!\\Ju4K%UK;"), + "%215%7D%26QF~0R-Ecy%5B%3F2Cig%3E6g%3D%3BhH%21%5CJu4K%25UK%3B"); + ASSERT_EQ(OAuthRequestSigner::Encode("1UgHf(r)SkMRS`fRZ/8PsTcXT0:\\<9I=6{|:"), + "1UgHf%28r%29SkMRS%60fRZ%2F8PsTcXT0%3A%5C%3C9I%3D6%7B%7C%3A"); + ASSERT_EQ(OAuthRequestSigner::Encode("|<XIy1?o`r\"RuGSX#!:MeP&RLZQM@:\\';2X"), + "%7C%3CXIy1%3Fo%60r%22RuGSX%23%21%3AMeP%26RLZQM%40%3A%5C%27%3B2X"); + ASSERT_EQ(OAuthRequestSigner::Encode("#a@A>ZtcQ/yb.~^Q_]daRT?ffK>@A:afWuZL"), + "%23a%40A%3EZtcQ%2Fyb.~%5EQ_%5DdaRT%3FffK%3E%40A%3AafWuZL"); +} + +TEST(OAuthRequestSignerTest, DecodeEncoded) { + srand(RANDOM_SEED); + static const int kIterations = 500; + static const int kLengthLimit = 500; + for (int iteration = 0; iteration < kIterations; ++iteration) { + std::string text; + int length = rand() % kLengthLimit; + for (int position = 0; position < length; ++position) { + text += static_cast<char>(rand() % 256); + } + std::string encoded = OAuthRequestSigner::Encode(text); + std::string decoded; + ASSERT_TRUE(OAuthRequestSigner::Decode(encoded, &decoded)); + ASSERT_EQ(decoded, text); + } +} + +TEST(OAuthRequestSignerTest, SignGet1) { + GURL request_url("https://www.google.com/accounts/o8/GetOAuthToken"); + OAuthRequestSigner::Parameters parameters; + parameters["scope"] = "https://accounts.google.com/OAuthLogin"; + parameters["oauth_nonce"] = "2oiE_aHdk5qRTz0L9C8Lq0g"; + parameters["xaouth_display_name"] = "Chromium"; + parameters["oauth_timestamp"] = "1308152953"; + std::string signed_text; + ASSERT_TRUE(OAuthRequestSigner::SignURL( + request_url, + parameters, + OAuthRequestSigner::HMAC_SHA1_SIGNATURE, + OAuthRequestSigner::GET_METHOD, + "johndoe", // oauth_consumer_key + "53cR3t", // consumer secret + "4/VGY0MsQadcmO8VnCv9gnhoEooq1v", // oauth_token + "c5e0531ff55dfbb4054e", // token secret + &signed_text)); + ASSERT_EQ("https://www.google.com/accounts/o8/GetOAuthToken" + "?oauth_consumer_key=johndoe" + "&oauth_nonce=2oiE_aHdk5qRTz0L9C8Lq0g" + "&oauth_signature=PFqDTaiyey1UObcvOyI4Ng2HXW0%3D" + "&oauth_signature_method=HMAC-SHA1" + "&oauth_timestamp=1308152953" + "&oauth_token=4%2FVGY0MsQadcmO8VnCv9gnhoEooq1v" + "&oauth_version=1.0" + "&scope=https%3A%2F%2Faccounts.google.com%2FOAuthLogin" + "&xaouth_display_name=Chromium", + signed_text); +} + +TEST(OAuthRequestSignerTest, SignGet2) { + GURL request_url("https://accounts.google.com/OAuthGetAccessToken"); + OAuthRequestSigner::Parameters parameters; + parameters["oauth_timestamp"] = "1308147831"; + parameters["oauth_nonce"] = "4d4hZW9DygWQujP2tz06UN"; + std::string signed_text; + ASSERT_TRUE(OAuthRequestSigner::SignURL( + request_url, + parameters, + OAuthRequestSigner::HMAC_SHA1_SIGNATURE, + OAuthRequestSigner::GET_METHOD, + "anonymous", // oauth_consumer_key + "anonymous", // consumer secret + "4/CcC-hgdj1TNnWaX8NTQ76YDXCBEK", // oauth_token + std::string(), // token secret + &signed_text)); + ASSERT_EQ(signed_text, + "https://accounts.google.com/OAuthGetAccessToken" + "?oauth_consumer_key=anonymous" + "&oauth_nonce=4d4hZW9DygWQujP2tz06UN" + "&oauth_signature=YiJv%2BEOWsvCDCi13%2FhQBFrr0J7c%3D" + "&oauth_signature_method=HMAC-SHA1" + "&oauth_timestamp=1308147831" + "&oauth_token=4%2FCcC-hgdj1TNnWaX8NTQ76YDXCBEK" + "&oauth_version=1.0"); +} + +TEST(OAuthRequestSignerTest, ParseAndSignGet1) { + GURL request_url("https://www.google.com/accounts/o8/GetOAuthToken" + "?scope=https://accounts.google.com/OAuthLogin" + "&oauth_nonce=2oiE_aHdk5qRTz0L9C8Lq0g" + "&xaouth_display_name=Chromium" + "&oauth_timestamp=1308152953"); + std::string signed_text; + ASSERT_TRUE(OAuthRequestSigner::ParseAndSign( + request_url, + OAuthRequestSigner::HMAC_SHA1_SIGNATURE, + OAuthRequestSigner::GET_METHOD, + "anonymous", // oauth_consumer_key + "anonymous", // consumer secret + "4/CcC-hgdj1TNnWaX8NTQ76YDXCBEK", // oauth_token + std::string(), // token secret + &signed_text)); + ASSERT_EQ("https://www.google.com/accounts/o8/GetOAuthToken" + "?oauth_consumer_key=anonymous" + "&oauth_nonce=2oiE_aHdk5qRTz0L9C8Lq0g" + "&oauth_signature=PH7KP6cP%2BzZ1SJ6WGqBgXwQP9Mc%3D" + "&oauth_signature_method=HMAC-SHA1" + "&oauth_timestamp=1308152953" + "&oauth_token=4%2FCcC-hgdj1TNnWaX8NTQ76YDXCBEK" + "&oauth_version=1.0" + "&scope=https%3A%2F%2Faccounts.google.com%2FOAuthLogin" + "&xaouth_display_name=Chromium", + signed_text); +} + +TEST(OAuthRequestSignerTest, ParseAndSignGet2) { + GURL request_url("https://accounts.google.com/OAuthGetAccessToken" + "?oauth_timestamp=1308147831" + "&oauth_nonce=4d4hZW9DygWQujP2tz06UN"); + std::string signed_text; + ASSERT_TRUE(OAuthRequestSigner::ParseAndSign( + request_url, + OAuthRequestSigner::HMAC_SHA1_SIGNATURE, + OAuthRequestSigner::GET_METHOD, + "anonymous", // oauth_consumer_key + "anonymous", // consumer secret + "4/CcC-hgdj1TNnWaX8NTQ76YDXCBEK", // oauth_token + std::string(), // token secret + &signed_text)); + ASSERT_EQ(signed_text, + "https://accounts.google.com/OAuthGetAccessToken" + "?oauth_consumer_key=anonymous" + "&oauth_nonce=4d4hZW9DygWQujP2tz06UN" + "&oauth_signature=YiJv%2BEOWsvCDCi13%2FhQBFrr0J7c%3D" + "&oauth_signature_method=HMAC-SHA1" + "&oauth_timestamp=1308147831" + "&oauth_token=4%2FCcC-hgdj1TNnWaX8NTQ76YDXCBEK" + "&oauth_version=1.0"); +} + +TEST(OAuthRequestSignerTest, SignPost1) { + GURL request_url("https://www.google.com/accounts/o8/GetOAuthToken"); + OAuthRequestSigner::Parameters parameters; + parameters["scope"] = "https://accounts.google.com/OAuthLogin"; + parameters["oauth_nonce"] = "2oiE_aHdk5qRTz0L9C8Lq0g"; + parameters["xaouth_display_name"] = "Chromium"; + parameters["oauth_timestamp"] = "1308152953"; + std::string signed_text; + ASSERT_TRUE(OAuthRequestSigner::SignURL( + request_url, + parameters, + OAuthRequestSigner::HMAC_SHA1_SIGNATURE, + OAuthRequestSigner::POST_METHOD, + "anonymous", // oauth_consumer_key + "anonymous", // consumer secret + "4/X8x0r7bHif_VNCLjUMutxGkzo13d", // oauth_token + "b7120598d47594bd3522", // token secret + &signed_text)); + ASSERT_EQ("oauth_consumer_key=anonymous" + "&oauth_nonce=2oiE_aHdk5qRTz0L9C8Lq0g" + "&oauth_signature=vVlfv6dnV2%2Fx7TozS0Gf83zS2%2BQ%3D" + "&oauth_signature_method=HMAC-SHA1" + "&oauth_timestamp=1308152953" + "&oauth_token=4%2FX8x0r7bHif_VNCLjUMutxGkzo13d" + "&oauth_version=1.0" + "&scope=https%3A%2F%2Faccounts.google.com%2FOAuthLogin" + "&xaouth_display_name=Chromium", + signed_text); +} + +TEST(OAuthRequestSignerTest, SignPost2) { + GURL request_url("https://accounts.google.com/OAuthGetAccessToken"); + OAuthRequestSigner::Parameters parameters; + parameters["oauth_timestamp"] = "1234567890"; + parameters["oauth_nonce"] = "17171717171717171"; + std::string signed_text; + ASSERT_TRUE(OAuthRequestSigner::SignURL( + request_url, + parameters, + OAuthRequestSigner::HMAC_SHA1_SIGNATURE, + OAuthRequestSigner::POST_METHOD, + "anonymous", // oauth_consumer_key + "anonymous", // consumer secret + "4/CcC-hgdj1TNnWaX8NTQ76YDXCBEK", // oauth_token + std::string(), // token secret + &signed_text)); + ASSERT_EQ(signed_text, + "oauth_consumer_key=anonymous" + "&oauth_nonce=17171717171717171" + "&oauth_signature=tPX2XqKQICWzopZ80CFGX%2F53DLo%3D" + "&oauth_signature_method=HMAC-SHA1" + "&oauth_timestamp=1234567890" + "&oauth_token=4%2FCcC-hgdj1TNnWaX8NTQ76YDXCBEK" + "&oauth_version=1.0"); +} + +TEST(OAuthRequestSignerTest, ParseAndSignPost1) { + GURL request_url("https://www.google.com/accounts/o8/GetOAuthToken" + "?scope=https://accounts.google.com/OAuthLogin" + "&oauth_nonce=2oiE_aHdk5qRTz0L9C8Lq0g" + "&xaouth_display_name=Chromium" + "&oauth_timestamp=1308152953"); + std::string signed_text; + ASSERT_TRUE(OAuthRequestSigner::ParseAndSign( + request_url, + OAuthRequestSigner::HMAC_SHA1_SIGNATURE, + OAuthRequestSigner::POST_METHOD, + "anonymous", // oauth_consumer_key + "anonymous", // consumer secret + "4/X8x0r7bHif_VNCLjUMutxGkzo13d", // oauth_token + "b7120598d47594bd3522", // token secret + &signed_text)); + ASSERT_EQ("oauth_consumer_key=anonymous" + "&oauth_nonce=2oiE_aHdk5qRTz0L9C8Lq0g" + "&oauth_signature=vVlfv6dnV2%2Fx7TozS0Gf83zS2%2BQ%3D" + "&oauth_signature_method=HMAC-SHA1" + "&oauth_timestamp=1308152953" + "&oauth_token=4%2FX8x0r7bHif_VNCLjUMutxGkzo13d" + "&oauth_version=1.0" + "&scope=https%3A%2F%2Faccounts.google.com%2FOAuthLogin" + "&xaouth_display_name=Chromium", + signed_text); +} + +TEST(OAuthRequestSignerTest, ParseAndSignPost2) { + GURL request_url("https://accounts.google.com/OAuthGetAccessToken" + "?oauth_timestamp=1234567890" + "&oauth_nonce=17171717171717171"); + std::string signed_text; + ASSERT_TRUE(OAuthRequestSigner::ParseAndSign( + request_url, + OAuthRequestSigner::HMAC_SHA1_SIGNATURE, + OAuthRequestSigner::POST_METHOD, + "anonymous", // oauth_consumer_key + "anonymous", // consumer secret + "4/CcC-hgdj1TNnWaX8NTQ76YDXCBEK", // oauth_token + std::string(), // token secret + &signed_text)); + ASSERT_EQ(signed_text, + "oauth_consumer_key=anonymous" + "&oauth_nonce=17171717171717171" + "&oauth_signature=tPX2XqKQICWzopZ80CFGX%2F53DLo%3D" + "&oauth_signature_method=HMAC-SHA1" + "&oauth_timestamp=1234567890" + "&oauth_token=4%2FCcC-hgdj1TNnWaX8NTQ76YDXCBEK" + "&oauth_version=1.0"); +} + +TEST(OAuthRequestSignerTest, SignAuthHeader) { + GURL request_url("https://www.google.com/accounts/o8/GetOAuthToken"); + OAuthRequestSigner::Parameters parameters; + parameters["scope"] = "https://accounts.google.com/OAuthLogin"; + parameters["oauth_nonce"] = "2oiE_aHdk5qRTz0L9C8Lq0g"; + parameters["xaouth_display_name"] = "Chromium"; + parameters["oauth_timestamp"] = "1308152953"; + std::string signed_text; + ASSERT_TRUE(OAuthRequestSigner::SignAuthHeader( + request_url, + parameters, + OAuthRequestSigner::HMAC_SHA1_SIGNATURE, + OAuthRequestSigner::GET_METHOD, + "johndoe", // oauth_consumer_key + "53cR3t", // consumer secret + "4/VGY0MsQadcmO8VnCv9gnhoEooq1v", // oauth_token + "c5e0531ff55dfbb4054e", // token secret + &signed_text)); + ASSERT_EQ("OAuth " + "oauth_consumer_key=\"johndoe\", " + "oauth_nonce=\"2oiE_aHdk5qRTz0L9C8Lq0g\", " + "oauth_signature=\"PFqDTaiyey1UObcvOyI4Ng2HXW0%3D\", " + "oauth_signature_method=\"HMAC-SHA1\", " + "oauth_timestamp=\"1308152953\", " + "oauth_token=\"4%2FVGY0MsQadcmO8VnCv9gnhoEooq1v\", " + "oauth_version=\"1.0\", " + "scope=\"https%3A%2F%2Faccounts.google.com%2FOAuthLogin\", " + "xaouth_display_name=\"Chromium\"", + signed_text); +} diff --git a/chromium/google_apis/google_api_keys.cc b/chromium/google_apis/google_api_keys.cc new file mode 100644 index 00000000000..fc4b54735b4 --- /dev/null +++ b/chromium/google_apis/google_api_keys.cc @@ -0,0 +1,249 @@ +// Copyright (c) 2012 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 "google_apis/google_api_keys.h" + +// If you add more includes to this list, you also need to add them to +// google_api_keys_unittest.cc. +#include "base/command_line.h" +#include "base/environment.h" +#include "base/lazy_instance.h" +#include "base/logging.h" +#include "base/memory/scoped_ptr.h" +#include "base/strings/stringize_macros.h" + +#if defined(GOOGLE_CHROME_BUILD) || defined(USE_OFFICIAL_GOOGLE_API_KEYS) +#include "google_apis/internal/google_chrome_api_keys.h" +#endif + +// Used to indicate an unset key/id/secret. This works better with +// various unit tests than leaving the token empty. +#define DUMMY_API_TOKEN "dummytoken" + +#if !defined(GOOGLE_API_KEY) +#define GOOGLE_API_KEY DUMMY_API_TOKEN +#endif + +#if !defined(GOOGLE_CLIENT_ID_MAIN) +#define GOOGLE_CLIENT_ID_MAIN DUMMY_API_TOKEN +#endif + +#if !defined(GOOGLE_CLIENT_SECRET_MAIN) +#define GOOGLE_CLIENT_SECRET_MAIN DUMMY_API_TOKEN +#endif + +#if !defined(GOOGLE_CLIENT_ID_CLOUD_PRINT) +#define GOOGLE_CLIENT_ID_CLOUD_PRINT DUMMY_API_TOKEN +#endif + +#if !defined(GOOGLE_CLIENT_SECRET_CLOUD_PRINT) +#define GOOGLE_CLIENT_SECRET_CLOUD_PRINT DUMMY_API_TOKEN +#endif + +#if !defined(GOOGLE_CLIENT_ID_REMOTING) +#define GOOGLE_CLIENT_ID_REMOTING DUMMY_API_TOKEN +#endif + +#if !defined(GOOGLE_CLIENT_SECRET_REMOTING) +#define GOOGLE_CLIENT_SECRET_REMOTING DUMMY_API_TOKEN +#endif + +// These are used as shortcuts for developers and users providing +// OAuth credentials via preprocessor defines or environment +// variables. If set, they will be used to replace any of the client +// IDs and secrets above that have not been set (and only those; they +// will not override already-set values). +#if !defined(GOOGLE_DEFAULT_CLIENT_ID) +#define GOOGLE_DEFAULT_CLIENT_ID "" +#endif +#if !defined(GOOGLE_DEFAULT_CLIENT_SECRET) +#define GOOGLE_DEFAULT_CLIENT_SECRET "" +#endif + +namespace switches { + +// Specifies custom OAuth2 client id for testing purposes. +const char kOAuth2ClientID[] = "oauth2-client-id"; + +// Specifies custom OAuth2 client secret for testing purposes. +const char kOAuth2ClientSecret[] = "oauth2-client-secret"; + +} // namespace switches + +namespace google_apis { + +// This is used as a lazy instance to determine keys once and cache them. +class APIKeyCache { + public: + APIKeyCache() { + scoped_ptr<base::Environment> environment(base::Environment::Create()); + CommandLine* command_line = CommandLine::ForCurrentProcess(); + + api_key_ = CalculateKeyValue(GOOGLE_API_KEY, + STRINGIZE_NO_EXPANSION(GOOGLE_API_KEY), + NULL, + std::string(), + environment.get(), + command_line); + + std::string default_client_id = + CalculateKeyValue(GOOGLE_DEFAULT_CLIENT_ID, + STRINGIZE_NO_EXPANSION(GOOGLE_DEFAULT_CLIENT_ID), + NULL, + std::string(), + environment.get(), + command_line); + std::string default_client_secret = + CalculateKeyValue(GOOGLE_DEFAULT_CLIENT_SECRET, + STRINGIZE_NO_EXPANSION(GOOGLE_DEFAULT_CLIENT_SECRET), + NULL, + std::string(), + environment.get(), + command_line); + + // We currently only allow overriding the baked-in values for the + // default OAuth2 client ID and secret using a command-line + // argument, since that is useful to enable testing against + // staging servers, and since that was what was possible and + // likely practiced by the QA team before this implementation was + // written. + client_ids_[CLIENT_MAIN] = CalculateKeyValue( + GOOGLE_CLIENT_ID_MAIN, + STRINGIZE_NO_EXPANSION(GOOGLE_CLIENT_ID_MAIN), + switches::kOAuth2ClientID, + default_client_id, + environment.get(), + command_line); + client_secrets_[CLIENT_MAIN] = CalculateKeyValue( + GOOGLE_CLIENT_SECRET_MAIN, + STRINGIZE_NO_EXPANSION(GOOGLE_CLIENT_SECRET_MAIN), + switches::kOAuth2ClientSecret, + default_client_secret, + environment.get(), + command_line); + + client_ids_[CLIENT_CLOUD_PRINT] = CalculateKeyValue( + GOOGLE_CLIENT_ID_CLOUD_PRINT, + STRINGIZE_NO_EXPANSION(GOOGLE_CLIENT_ID_CLOUD_PRINT), + NULL, + default_client_id, + environment.get(), + command_line); + client_secrets_[CLIENT_CLOUD_PRINT] = CalculateKeyValue( + GOOGLE_CLIENT_SECRET_CLOUD_PRINT, + STRINGIZE_NO_EXPANSION(GOOGLE_CLIENT_SECRET_CLOUD_PRINT), + NULL, + default_client_secret, + environment.get(), + command_line); + + client_ids_[CLIENT_REMOTING] = CalculateKeyValue( + GOOGLE_CLIENT_ID_REMOTING, + STRINGIZE_NO_EXPANSION(GOOGLE_CLIENT_ID_REMOTING), + NULL, + default_client_id, + environment.get(), + command_line); + client_secrets_[CLIENT_REMOTING] = CalculateKeyValue( + GOOGLE_CLIENT_SECRET_REMOTING, + STRINGIZE_NO_EXPANSION(GOOGLE_CLIENT_SECRET_REMOTING), + NULL, + default_client_secret, + environment.get(), + command_line); + } + + std::string api_key() const { return api_key_; } + + std::string GetClientID(OAuth2Client client) const { + DCHECK_LT(client, CLIENT_NUM_ITEMS); + return client_ids_[client]; + } + + std::string GetClientSecret(OAuth2Client client) const { + DCHECK_LT(client, CLIENT_NUM_ITEMS); + return client_secrets_[client]; + } + + private: + // Gets a value for a key. In priority order, this will be the value + // provided via a command-line switch, the value provided via an + // environment variable, or finally a value baked into the build. + // |command_line_switch| may be NULL. + static std::string CalculateKeyValue(const char* baked_in_value, + const char* environment_variable_name, + const char* command_line_switch, + const std::string& default_if_unset, + base::Environment* environment, + CommandLine* command_line) { + std::string key_value = baked_in_value; + std::string temp; + if (environment->GetVar(environment_variable_name, &temp)) { + key_value = temp; + LOG(INFO) << "Overriding API key " << environment_variable_name + << " with value " << key_value << " from environment variable."; + } + + if (command_line_switch && command_line->HasSwitch(command_line_switch)) { + key_value = command_line->GetSwitchValueASCII(command_line_switch); + LOG(INFO) << "Overriding API key " << environment_variable_name + << " with value " << key_value << " from command-line switch."; + } + + if (key_value == DUMMY_API_TOKEN) { +#if defined(GOOGLE_CHROME_BUILD) + // No key should be unset in an official build except the + // GOOGLE_DEFAULT_* keys. The default keys don't trigger this + // check as their "unset" value is not DUMMY_API_TOKEN. + CHECK(false); +#endif + if (default_if_unset.size() > 0) { + LOG(INFO) << "Using default value \"" << default_if_unset + << "\" for API key " << environment_variable_name; + key_value = default_if_unset; + } + } + + // This should remain a debug-only log. + DVLOG(1) << "API key " << environment_variable_name << "=" << key_value; + + return key_value; + } + + std::string api_key_; + std::string client_ids_[CLIENT_NUM_ITEMS]; + std::string client_secrets_[CLIENT_NUM_ITEMS]; +}; + +static base::LazyInstance<APIKeyCache> g_api_key_cache = + LAZY_INSTANCE_INITIALIZER; + +bool HasKeysConfigured() { + if (GetAPIKey() == DUMMY_API_TOKEN) + return false; + + for (size_t client_id = 0; client_id < CLIENT_NUM_ITEMS; ++client_id) { + OAuth2Client client = static_cast<OAuth2Client>(client_id); + if (GetOAuth2ClientID(client) == DUMMY_API_TOKEN || + GetOAuth2ClientSecret(client) == DUMMY_API_TOKEN) { + return false; + } + } + + return true; +} + +std::string GetAPIKey() { + return g_api_key_cache.Get().api_key(); +} + +std::string GetOAuth2ClientID(OAuth2Client client) { + return g_api_key_cache.Get().GetClientID(client); +} + +std::string GetOAuth2ClientSecret(OAuth2Client client) { + return g_api_key_cache.Get().GetClientSecret(client); +} + +} // namespace google_apis diff --git a/chromium/google_apis/google_api_keys.h b/chromium/google_apis/google_api_keys.h new file mode 100644 index 00000000000..28421e8e222 --- /dev/null +++ b/chromium/google_apis/google_api_keys.h @@ -0,0 +1,94 @@ +// Copyright (c) 2012 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 GOOGLE_APIS_GOOGLE_API_KEYS_H_ +#define GOOGLE_APIS_GOOGLE_API_KEYS_H_ + +// If you add more includes to this file, you also need to add them to +// google_api_keys_unittest.cc. +#include <string> + +// These functions enable you to retrieve keys to use for Google APIs +// such as Translate and Safe Browsing. +// +// You can retrieve either an "API key" (sometimes called a developer +// key) which identifies you (or the company you work for) as a +// developer, or you can retrieve the "client ID" and "client secret" +// used by you (or the company you work for) to generate OAuth2 +// requests. +// +// Each developer (or group of developers working together for a +// single company) must request a Google API key and the client ID and +// client secret for OAuth2. See +// https://developers.google.com/console/help/ and +// https://developers.google.com/console/. +// +// The keys must either be provided using preprocessor variables (set +// via e.g. ~/.gyp/include.gypi). Alternatively, they can be +// overridden at runtime using environment variables of the same name. +// +// The names of the preprocessor variables (or environment variables +// to override them at runtime) are as follows: +// - GOOGLE_API_KEY: The API key, a.k.a. developer key. +// - GOOGLE_DEFAULT_CLIENT_ID: If set, this is used as the default for +// all client IDs not otherwise set. This is intended only for +// development. +// - GOOGLE_DEFAULT_CLIENT_SECRET: If set, this is used as the default +// for all client secrets. This is intended only for development. +// - GOOGLE_CLIENT_ID_[client name] +// (e.g. GOOGLE_CLIENT_ID_CLOUD_PRINT, i.e. one for each item in the +// OAuth2Client enumeration below) +// - GOOGLE_CLIENT_SECRET_[client name] +// (e.g. GOOGLE_CLIENT_SECRET_CLOUD_PRINT, i.e. one for each item in +// the OAuth2Client enumeration below) +// +// The GOOGLE_CLIENT_ID_MAIN and GOOGLE_CLIENT_SECRET_MAIN values can +// also be set via the command line (this overrides any other +// setting). The command-line parameters are --oauth2-client-id and +// --oauth2-client-secret. +// +// If some of the parameters mentioned above are not provided, +// Chromium will still build and run, but services that require them +// may fail to work without warning. They should do so gracefully, +// similar to what would happen when a network connection is +// unavailable. + +namespace google_apis { + +// Returns true if no dummy API keys or OAuth2 tokens are set. +bool HasKeysConfigured(); + +// Retrieves the API key, a.k.a. developer key, or a dummy string +// if not set. +// +// Note that the key should be escaped for the context you use it in, +// e.g. URL-escaped if you use it in a URL. +std::string GetAPIKey(); + +// Represents the different sets of client IDs and secrets in use. +enum OAuth2Client { + CLIENT_MAIN, // Several different features use this. + CLIENT_CLOUD_PRINT, + CLIENT_REMOTING, + + CLIENT_NUM_ITEMS // Must be last item. +}; + +// Retrieves the OAuth2 client ID for the specified client, or the +// empty string if not set. +// +// Note that the ID should be escaped for the context you use it in, +// e.g. URL-escaped if you use it in a URL. +std::string GetOAuth2ClientID(OAuth2Client client); + +// Retrieves the OAuth2 client secret for the specified client, or the +// empty string if not set. +// +// Note that the secret should be escaped for the context you use it +// in, e.g. URL-escaped if you use it in a URL. +std::string GetOAuth2ClientSecret(OAuth2Client client); + +} // namespace google_apis + +#endif // GOOGLE_APIS_GOOGLE_API_KEYS_H_ diff --git a/chromium/google_apis/google_api_keys.py b/chromium/google_apis/google_api_keys.py new file mode 100755 index 00000000000..6776aaec343 --- /dev/null +++ b/chromium/google_apis/google_api_keys.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python +# Copyright (c) 2012 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. + +"""Python API for retrieving API keys. + +Note that this does not have the exact same semantics as the C++ API +in google_api_keys.h, since it does not have access to gyp variables +or preprocessor defines. +""" + +import os +import re +import sys + + +# The token returned when an API key is unset. +DUMMY_TOKEN = 'dummytoken' + + +def _GetTokenFromOfficialFile(token_name): + """Parses the token from the official file if it exists, else returns None.""" + official_path = os.path.join(os.path.dirname(__file__), + 'internal/google_chrome_api_keys.h') + if not os.path.isfile(official_path): + return None + + line_regexp = '^#define\s*%s\s*"([^"]+)"' % token_name + line_pattern = re.compile(line_regexp) + def ParseLine(current_line): + result = line_pattern.match(current_line) + if result: + return result.group(1) + else: + return None + + with open(official_path) as f: + current_line = '' + for line in f: + if line.endswith('\\\n'): + current_line += line[:-2] + else: + # Last line in multi-line #define, or a line that is not a + # continuation line. + current_line += line + token = ParseLine(current_line) + if token: + if current_line.count('"') != 2: + raise Exception( + 'Embedded quotes and multi-line strings are not supported.') + return token + current_line = '' + return None + + +def _GetToken(token_name): + """Returns the API token with the given name, or DUMMY_TOKEN by default.""" + if token_name in os.environ: + return os.environ[token_name] + token = _GetTokenFromOfficialFile(token_name) + if token: + return token + else: + return DUMMY_TOKEN + + +def GetAPIKey(): + """Returns the simple API key.""" + return _GetToken('GOOGLE_API_KEY') + + +def GetClientID(client_name): + """Returns the OAuth 2.0 client ID for the client of the given name.""" + return _GetToken('GOOGLE_CLIENT_ID_%s' % client_name) + + +def GetClientSecret(client_name): + """Returns the OAuth 2.0 client secret for the client of the given name.""" + return _GetToken('GOOGLE_CLIENT_SECRET_%s' % client_name) + + +if __name__ == "__main__": + print 'GOOGLE_API_KEY=%s' % GetAPIKey() + print 'GOOGLE_CLIENT_ID_MAIN=%s' % GetClientID('MAIN') + print 'GOOGLE_CLIENT_SECRET_MAIN=%s' % GetClientSecret('MAIN') + print 'GOOGLE_CLIENT_ID_CLOUD_PRINT=%s' % GetClientID('CLOUD_PRINT') + print 'GOOGLE_CLIENT_SECRET_CLOUD_PRINT=%s' % GetClientSecret('CLOUD_PRINT') + print 'GOOGLE_CLIENT_ID_REMOTING=%s' % GetClientID('REMOTING') + print 'GOOGLE_CLIENT_SECRET_REMOTING=%s' % GetClientSecret('REMOTING') diff --git a/chromium/google_apis/google_api_keys_unittest.cc b/chromium/google_apis/google_api_keys_unittest.cc new file mode 100644 index 00000000000..4d338f5289b --- /dev/null +++ b/chromium/google_apis/google_api_keys_unittest.cc @@ -0,0 +1,438 @@ +// Copyright (c) 2012 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. + +// Unit tests for implementation of google_api_keys namespace. +// +// Because the file deals with a lot of preprocessor defines and +// optionally includes an internal header, the way we test is by +// including the .cc file multiple times with different defines set. +// This is a little unorthodox, but it lets us test the behavior as +// close to unmodified as possible. + +#include "google_apis/google_api_keys.h" + +#include "build/build_config.h" +#include "testing/gtest/include/gtest/gtest.h" + +// The Win builders fail (with a linker crash) when trying to link +// unit_tests, and the Android builders complain about multiply +// defined symbols (likely they don't do name decoration as well as +// the Mac and Linux linkers). Therefore these tests are only built +// and run on Mac and Linux, which should provide plenty of coverage +// since there are no platform-specific bits in this code. +#if defined(OS_LINUX) || defined(OS_MACOSX) + +// We need to include everything included by google_api_keys.cc once +// at global scope so that things like STL and classes from base don't +// get defined when we re-include the google_api_keys.cc file +// below. We used to include that file in its entirety here, but that +// can cause problems if the linker decides the version of symbols +// from that file included here is the "right" version. +#include <string> +#include "base/command_line.h" +#include "base/environment.h" +#include "base/lazy_instance.h" +#include "base/logging.h" +#include "base/memory/scoped_ptr.h" +#include "base/strings/stringize_macros.h" + +// This is the default baked-in value for OAuth IDs and secrets. +static const char kDummyToken[] = "dummytoken"; + +struct EnvironmentCache { + public: + EnvironmentCache() : variable_name(NULL), was_set(false) {} + + const char* variable_name; + bool was_set; + std::string value; +}; + +class GoogleAPIKeysTest : public testing::Test { + public: + GoogleAPIKeysTest() : env_(base::Environment::Create()) { + env_cache_[0].variable_name = "GOOGLE_API_KEY"; + env_cache_[1].variable_name = "GOOGLE_CLIENT_ID_MAIN"; + env_cache_[2].variable_name = "GOOGLE_CLIENT_SECRET_MAIN"; + env_cache_[3].variable_name = "GOOGLE_CLIENT_ID_CLOUD_PRINT"; + env_cache_[4].variable_name = "GOOGLE_CLIENT_SECRET_CLOUD_PRINT"; + env_cache_[5].variable_name = "GOOGLE_CLIENT_ID_REMOTING"; + env_cache_[6].variable_name = "GOOGLE_CLIENT_SECRET_REMOTING"; + env_cache_[7].variable_name = "GOOGLE_DEFAULT_CLIENT_ID"; + env_cache_[8].variable_name = "GOOGLE_DEFAULT_CLIENT_SECRET"; + } + + virtual void SetUp() { + // Unset all environment variables that can affect these tests, + // for the duration of the tests. + for (size_t i = 0; i < arraysize(env_cache_); ++i) { + EnvironmentCache& cache = env_cache_[i]; + cache.was_set = env_->HasVar(cache.variable_name); + cache.value.clear(); + if (cache.was_set) { + env_->GetVar(cache.variable_name, &cache.value); + env_->UnSetVar(cache.variable_name); + } + } + } + + virtual void TearDown() { + // Restore environment. + for (size_t i = 0; i < arraysize(env_cache_); ++i) { + EnvironmentCache& cache = env_cache_[i]; + if (cache.was_set) { + env_->SetVar(cache.variable_name, cache.value); + } + } + } + + private: + scoped_ptr<base::Environment> env_; + + // Why 3? It is for GOOGLE_API_KEY, GOOGLE_DEFAULT_CLIENT_ID and + // GOOGLE_DEFAULT_CLIENT_SECRET. + // + // Why 2 times CLIENT_NUM_ITEMS? This is the number of different + // clients in the OAuth2Client enumeration, and for each of these we + // have both an ID and a secret. + EnvironmentCache env_cache_[3 + 2 * google_apis::CLIENT_NUM_ITEMS]; +}; + +#if defined(GOOGLE_CHROME_BUILD) || defined(USE_OFFICIAL_GOOGLE_API_KEYS) +// Test official build behavior, since we are in a checkout where this +// is possible. +namespace official_build { + +// We start every test by creating a clean environment for the +// preprocessor defines used in google_api_keys.cc +#undef DUMMY_API_TOKEN +#undef GOOGLE_API_KEY +#undef GOOGLE_CLIENT_ID_MAIN +#undef GOOGLE_CLIENT_SECRET_MAIN +#undef GOOGLE_CLIENT_ID_CLOUD_PRINT +#undef GOOGLE_CLIENT_SECRET_CLOUD_PRINT +#undef GOOGLE_CLIENT_ID_REMOTING +#undef GOOGLE_CLIENT_SECRET_REMOTING +#undef GOOGLE_DEFAULT_CLIENT_ID +#undef GOOGLE_DEFAULT_CLIENT_SECRET + +// Try setting some keys, these should be ignored since it's a build +// with official keys. +#define GOOGLE_API_KEY "bogus api_key" +#define GOOGLE_CLIENT_ID_MAIN "bogus client_id_main" + +// Undef include guard so things get defined again, within this namespace. +#undef GOOGLE_APIS_GOOGLE_API_KEYS_H_ +#undef GOOGLE_APIS_INTERNAL_GOOGLE_CHROME_API_KEYS_ +#include "google_apis/google_api_keys.cc" + +} // namespace official_build + +TEST_F(GoogleAPIKeysTest, OfficialKeys) { + namespace testcase = official_build::google_apis; + + EXPECT_TRUE(testcase::HasKeysConfigured()); + + std::string api_key = testcase::g_api_key_cache.Get().api_key(); + std::string id_main = testcase::g_api_key_cache.Get().GetClientID( + testcase::CLIENT_MAIN); + std::string secret_main = testcase::g_api_key_cache.Get().GetClientSecret( + testcase::CLIENT_MAIN); + std::string id_cloud_print = + testcase::g_api_key_cache.Get().GetClientID( + testcase::CLIENT_CLOUD_PRINT); + std::string secret_cloud_print = + testcase::g_api_key_cache.Get().GetClientSecret( + testcase::CLIENT_CLOUD_PRINT); + std::string id_remoting = testcase::g_api_key_cache.Get().GetClientID( + testcase::CLIENT_REMOTING); + std::string secret_remoting = + testcase::g_api_key_cache.Get().GetClientSecret( + testcase::CLIENT_REMOTING); + + EXPECT_NE(0u, api_key.size()); + EXPECT_NE(DUMMY_API_TOKEN, api_key); + EXPECT_NE("bogus api_key", api_key); + EXPECT_NE(kDummyToken, api_key); + + EXPECT_NE(0u, id_main.size()); + EXPECT_NE(DUMMY_API_TOKEN, id_main); + EXPECT_NE("bogus client_id_main", id_main); + EXPECT_NE(kDummyToken, id_main); + + EXPECT_NE(0u, secret_main.size()); + EXPECT_NE(DUMMY_API_TOKEN, secret_main); + EXPECT_NE(kDummyToken, secret_main); + + EXPECT_NE(0u, id_cloud_print.size()); + EXPECT_NE(DUMMY_API_TOKEN, id_cloud_print); + EXPECT_NE(kDummyToken, id_cloud_print); + + EXPECT_NE(0u, secret_cloud_print.size()); + EXPECT_NE(DUMMY_API_TOKEN, secret_cloud_print); + EXPECT_NE(kDummyToken, secret_cloud_print); + + EXPECT_NE(0u, id_remoting.size()); + EXPECT_NE(DUMMY_API_TOKEN, id_remoting); + EXPECT_NE(kDummyToken, id_remoting); + + EXPECT_NE(0u, secret_remoting.size()); + EXPECT_NE(DUMMY_API_TOKEN, secret_remoting); + EXPECT_NE(kDummyToken, secret_remoting); +} +#endif // defined(GOOGLE_CHROME_BUILD) || defined(USE_OFFICIAL_GOOGLE_API_KEYS) + +// After this test, for the remainder of this compilation unit, we +// need official keys to not be used. +#undef GOOGLE_CHROME_BUILD +#undef USE_OFFICIAL_GOOGLE_API_KEYS + +// Test the set of keys temporarily baked into Chromium by default. +namespace default_keys { + +// We start every test by creating a clean environment for the +// preprocessor defines used in google_api_keys.cc +#undef DUMMY_API_TOKEN +#undef GOOGLE_API_KEY +#undef GOOGLE_CLIENT_ID_MAIN +#undef GOOGLE_CLIENT_SECRET_MAIN +#undef GOOGLE_CLIENT_ID_CLOUD_PRINT +#undef GOOGLE_CLIENT_SECRET_CLOUD_PRINT +#undef GOOGLE_CLIENT_ID_REMOTING +#undef GOOGLE_CLIENT_SECRET_REMOTING +#undef GOOGLE_DEFAULT_CLIENT_ID +#undef GOOGLE_DEFAULT_CLIENT_SECRET + +// Undef include guard so things get defined again, within this namespace. +#undef GOOGLE_APIS_GOOGLE_API_KEYS_H_ +#undef GOOGLE_APIS_INTERNAL_GOOGLE_CHROME_API_KEYS_ +#include "google_apis/google_api_keys.cc" + +} // namespace default_keys + +TEST_F(GoogleAPIKeysTest, DefaultKeys) { + namespace testcase = default_keys::google_apis; + + EXPECT_FALSE(testcase::HasKeysConfigured()); + + std::string api_key = testcase::g_api_key_cache.Get().api_key(); + std::string id_main = testcase::g_api_key_cache.Get().GetClientID( + testcase::CLIENT_MAIN); + std::string secret_main = testcase::g_api_key_cache.Get().GetClientSecret( + testcase::CLIENT_MAIN); + std::string id_cloud_print = + testcase::g_api_key_cache.Get().GetClientID( + testcase::CLIENT_CLOUD_PRINT); + std::string secret_cloud_print = + testcase::g_api_key_cache.Get().GetClientSecret( + testcase::CLIENT_CLOUD_PRINT); + std::string id_remoting = testcase::g_api_key_cache.Get().GetClientID( + testcase::CLIENT_REMOTING); + std::string secret_remoting = + testcase::g_api_key_cache.Get().GetClientSecret( + testcase::CLIENT_REMOTING); + + EXPECT_EQ(kDummyToken, api_key); + EXPECT_EQ(kDummyToken, id_main); + EXPECT_EQ(kDummyToken, secret_main); + EXPECT_EQ(kDummyToken, id_cloud_print); + EXPECT_EQ(kDummyToken, secret_cloud_print); + EXPECT_EQ(kDummyToken, id_remoting); + EXPECT_EQ(kDummyToken, secret_remoting); +} + +// Override a couple of keys, leave the rest default. +namespace override_some_keys { + +// We start every test by creating a clean environment for the +// preprocessor defines used in google_api_keys.cc +#undef DUMMY_API_TOKEN +#undef GOOGLE_API_KEY +#undef GOOGLE_CLIENT_ID_MAIN +#undef GOOGLE_CLIENT_SECRET_MAIN +#undef GOOGLE_CLIENT_ID_CLOUD_PRINT +#undef GOOGLE_CLIENT_SECRET_CLOUD_PRINT +#undef GOOGLE_CLIENT_ID_REMOTING +#undef GOOGLE_CLIENT_SECRET_REMOTING +#undef GOOGLE_DEFAULT_CLIENT_ID +#undef GOOGLE_DEFAULT_CLIENT_SECRET + +#define GOOGLE_API_KEY "API_KEY override" +#define GOOGLE_CLIENT_ID_REMOTING "CLIENT_ID_REMOTING override" + +// Undef include guard so things get defined again, within this namespace. +#undef GOOGLE_APIS_GOOGLE_API_KEYS_H_ +#undef GOOGLE_APIS_INTERNAL_GOOGLE_CHROME_API_KEYS_ +#include "google_apis/google_api_keys.cc" + +} // namespace override_some_keys + +TEST_F(GoogleAPIKeysTest, OverrideSomeKeys) { + namespace testcase = override_some_keys::google_apis; + + EXPECT_FALSE(testcase::HasKeysConfigured()); + + std::string api_key = testcase::g_api_key_cache.Get().api_key(); + std::string id_main = testcase::g_api_key_cache.Get().GetClientID( + testcase::CLIENT_MAIN); + std::string secret_main = testcase::g_api_key_cache.Get().GetClientSecret( + testcase::CLIENT_MAIN); + std::string id_cloud_print = + testcase::g_api_key_cache.Get().GetClientID( + testcase::CLIENT_CLOUD_PRINT); + std::string secret_cloud_print = + testcase::g_api_key_cache.Get().GetClientSecret( + testcase::CLIENT_CLOUD_PRINT); + std::string id_remoting = testcase::g_api_key_cache.Get().GetClientID( + testcase::CLIENT_REMOTING); + std::string secret_remoting = + testcase::g_api_key_cache.Get().GetClientSecret( + testcase::CLIENT_REMOTING); + + EXPECT_EQ("API_KEY override", api_key); + EXPECT_EQ(kDummyToken, id_main); + EXPECT_EQ(kDummyToken, secret_main); + EXPECT_EQ(kDummyToken, id_cloud_print); + EXPECT_EQ(kDummyToken, secret_cloud_print); + EXPECT_EQ("CLIENT_ID_REMOTING override", id_remoting); + EXPECT_EQ(kDummyToken, secret_remoting); +} + +// Override all keys. +namespace override_all_keys { + +// We start every test by creating a clean environment for the +// preprocessor defines used in google_api_keys.cc +#undef DUMMY_API_TOKEN +#undef GOOGLE_API_KEY +#undef GOOGLE_CLIENT_ID_MAIN +#undef GOOGLE_CLIENT_SECRET_MAIN +#undef GOOGLE_CLIENT_ID_CLOUD_PRINT +#undef GOOGLE_CLIENT_SECRET_CLOUD_PRINT +#undef GOOGLE_CLIENT_ID_REMOTING +#undef GOOGLE_CLIENT_SECRET_REMOTING +#undef GOOGLE_DEFAULT_CLIENT_ID +#undef GOOGLE_DEFAULT_CLIENT_SECRET + +#define GOOGLE_API_KEY "API_KEY" +#define GOOGLE_CLIENT_ID_MAIN "ID_MAIN" +#define GOOGLE_CLIENT_SECRET_MAIN "SECRET_MAIN" +#define GOOGLE_CLIENT_ID_CLOUD_PRINT "ID_CLOUD_PRINT" +#define GOOGLE_CLIENT_SECRET_CLOUD_PRINT "SECRET_CLOUD_PRINT" +#define GOOGLE_CLIENT_ID_REMOTING "ID_REMOTING" +#define GOOGLE_CLIENT_SECRET_REMOTING "SECRET_REMOTING" + +// Undef include guard so things get defined again, within this namespace. +#undef GOOGLE_APIS_GOOGLE_API_KEYS_H_ +#undef GOOGLE_APIS_INTERNAL_GOOGLE_CHROME_API_KEYS_ +#include "google_apis/google_api_keys.cc" + +} // namespace override_all_keys + +TEST_F(GoogleAPIKeysTest, OverrideAllKeys) { + namespace testcase = override_all_keys::google_apis; + + EXPECT_TRUE(testcase::HasKeysConfigured()); + + std::string api_key = testcase::g_api_key_cache.Get().api_key(); + std::string id_main = testcase::g_api_key_cache.Get().GetClientID( + testcase::CLIENT_MAIN); + std::string secret_main = testcase::g_api_key_cache.Get().GetClientSecret( + testcase::CLIENT_MAIN); + std::string id_cloud_print = + testcase::g_api_key_cache.Get().GetClientID( + testcase::CLIENT_CLOUD_PRINT); + std::string secret_cloud_print = + testcase::g_api_key_cache.Get().GetClientSecret( + testcase::CLIENT_CLOUD_PRINT); + std::string id_remoting = testcase::g_api_key_cache.Get().GetClientID( + testcase::CLIENT_REMOTING); + std::string secret_remoting = + testcase::g_api_key_cache.Get().GetClientSecret( + testcase::CLIENT_REMOTING); + + EXPECT_EQ("API_KEY", api_key); + EXPECT_EQ("ID_MAIN", id_main); + EXPECT_EQ("SECRET_MAIN", secret_main); + EXPECT_EQ("ID_CLOUD_PRINT", id_cloud_print); + EXPECT_EQ("SECRET_CLOUD_PRINT", secret_cloud_print); + EXPECT_EQ("ID_REMOTING", id_remoting); + EXPECT_EQ("SECRET_REMOTING", secret_remoting); +} + +// Override all keys using both preprocessor defines and environment +// variables. The environment variables should win. +namespace override_all_keys_env { + +// We start every test by creating a clean environment for the +// preprocessor defines used in google_api_keys.cc +#undef DUMMY_API_TOKEN +#undef GOOGLE_API_KEY +#undef GOOGLE_CLIENT_ID_MAIN +#undef GOOGLE_CLIENT_SECRET_MAIN +#undef GOOGLE_CLIENT_ID_CLOUD_PRINT +#undef GOOGLE_CLIENT_SECRET_CLOUD_PRINT +#undef GOOGLE_CLIENT_ID_REMOTING +#undef GOOGLE_CLIENT_SECRET_REMOTING +#undef GOOGLE_DEFAULT_CLIENT_ID +#undef GOOGLE_DEFAULT_CLIENT_SECRET + +#define GOOGLE_API_KEY "API_KEY" +#define GOOGLE_CLIENT_ID_MAIN "ID_MAIN" +#define GOOGLE_CLIENT_SECRET_MAIN "SECRET_MAIN" +#define GOOGLE_CLIENT_ID_CLOUD_PRINT "ID_CLOUD_PRINT" +#define GOOGLE_CLIENT_SECRET_CLOUD_PRINT "SECRET_CLOUD_PRINT" +#define GOOGLE_CLIENT_ID_REMOTING "ID_REMOTING" +#define GOOGLE_CLIENT_SECRET_REMOTING "SECRET_REMOTING" + +// Undef include guard so things get defined again, within this namespace. +#undef GOOGLE_APIS_GOOGLE_API_KEYS_H_ +#undef GOOGLE_APIS_INTERNAL_GOOGLE_CHROME_API_KEYS_ +#include "google_apis/google_api_keys.cc" + +} // namespace override_all_keys_env + +TEST_F(GoogleAPIKeysTest, OverrideAllKeysUsingEnvironment) { + namespace testcase = override_all_keys_env::google_apis; + + scoped_ptr<base::Environment> env(base::Environment::Create()); + env->SetVar("GOOGLE_API_KEY", "env-API_KEY"); + env->SetVar("GOOGLE_CLIENT_ID_MAIN", "env-ID_MAIN"); + env->SetVar("GOOGLE_CLIENT_ID_CLOUD_PRINT", "env-ID_CLOUD_PRINT"); + env->SetVar("GOOGLE_CLIENT_ID_REMOTING", "env-ID_REMOTING"); + env->SetVar("GOOGLE_CLIENT_SECRET_MAIN", "env-SECRET_MAIN"); + env->SetVar("GOOGLE_CLIENT_SECRET_CLOUD_PRINT", "env-SECRET_CLOUD_PRINT"); + env->SetVar("GOOGLE_CLIENT_SECRET_REMOTING", "env-SECRET_REMOTING"); + + EXPECT_TRUE(testcase::HasKeysConfigured()); + + // It's important that the first call to Get() only happen after the + // environment variables have been set. + std::string api_key = testcase::g_api_key_cache.Get().api_key(); + std::string id_main = testcase::g_api_key_cache.Get().GetClientID( + testcase::CLIENT_MAIN); + std::string secret_main = testcase::g_api_key_cache.Get().GetClientSecret( + testcase::CLIENT_MAIN); + std::string id_cloud_print = + testcase::g_api_key_cache.Get().GetClientID( + testcase::CLIENT_CLOUD_PRINT); + std::string secret_cloud_print = + testcase::g_api_key_cache.Get().GetClientSecret( + testcase::CLIENT_CLOUD_PRINT); + std::string id_remoting = testcase::g_api_key_cache.Get().GetClientID( + testcase::CLIENT_REMOTING); + std::string secret_remoting = + testcase::g_api_key_cache.Get().GetClientSecret( + testcase::CLIENT_REMOTING); + + EXPECT_EQ("env-API_KEY", api_key); + EXPECT_EQ("env-ID_MAIN", id_main); + EXPECT_EQ("env-SECRET_MAIN", secret_main); + EXPECT_EQ("env-ID_CLOUD_PRINT", id_cloud_print); + EXPECT_EQ("env-SECRET_CLOUD_PRINT", secret_cloud_print); + EXPECT_EQ("env-ID_REMOTING", id_remoting); + EXPECT_EQ("env-SECRET_REMOTING", secret_remoting); +} + +#endif // defined(OS_LINUX) || defined(OS_MACOSX) diff --git a/chromium/google_apis/google_apis.gyp b/chromium/google_apis/google_apis.gyp new file mode 100644 index 00000000000..c67232ffb5f --- /dev/null +++ b/chromium/google_apis/google_apis.gyp @@ -0,0 +1,93 @@ +# Copyright (c) 2012 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. + +{ + 'variables': { + 'chromium_code': 1, # Use higher warning level. + }, + 'includes': [ + '../build/win_precompile.gypi', + ], + 'targets': [ + { + 'target_name': 'google_apis', + 'type': 'static_library', + 'includes': [ + 'determine_use_official_keys.gypi', + ], + 'dependencies': [ + '../base/base.gyp:base', + '../crypto/crypto.gyp:crypto', + '../net/net.gyp:net', + ], + 'conditions': [ + ['google_api_key!=""', { + 'defines': ['GOOGLE_API_KEY="<(google_api_key)"'], + }], + ['google_default_client_id!=""', { + 'defines': [ + 'GOOGLE_DEFAULT_CLIENT_ID="<(google_default_client_id)"', + ] + }], + ['google_default_client_secret!=""', { + 'defines': [ + 'GOOGLE_DEFAULT_CLIENT_SECRET="<(google_default_client_secret)"', + ] + }], + [ 'OS == "android"', { + 'dependencies': [ + '../third_party/openssl/openssl.gyp:openssl', + ], + 'sources/': [ + ['exclude', 'cup/client_update_protocol_nss\.cc$'], + ], + }], + [ 'use_openssl==1', { + 'sources!': [ + 'cup/client_update_protocol_nss.cc', + ], + }, { + 'sources!': [ + 'cup/client_update_protocol_openssl.cc', + ], + },], + ], + 'sources': [ + 'cup/client_update_protocol.cc', + 'cup/client_update_protocol.h', + 'cup/client_update_protocol_nss.cc', + 'cup/client_update_protocol_openssl.cc', + 'gaia/gaia_auth_consumer.cc', + 'gaia/gaia_auth_consumer.h', + 'gaia/gaia_auth_fetcher.cc', + 'gaia/gaia_auth_fetcher.h', + 'gaia/gaia_auth_util.cc', + 'gaia/gaia_auth_util.h', + 'gaia/gaia_constants.cc', + 'gaia/gaia_constants.h', + 'gaia/gaia_oauth_client.cc', + 'gaia/gaia_oauth_client.h', + 'gaia/gaia_switches.cc', + 'gaia/gaia_switches.h', + 'gaia/gaia_urls.cc', + 'gaia/gaia_urls.h', + 'gaia/google_service_auth_error.cc', + 'gaia/google_service_auth_error.h', + 'gaia/oauth_request_signer.cc', + 'gaia/oauth_request_signer.h', + 'gaia/oauth2_access_token_consumer.h', + 'gaia/oauth2_access_token_fetcher.cc', + 'gaia/oauth2_access_token_fetcher.h', + 'gaia/oauth2_api_call_flow.cc', + 'gaia/oauth2_api_call_flow.h', + 'gaia/oauth2_mint_token_flow.cc', + 'gaia/oauth2_mint_token_flow.h', + 'google_api_keys.cc', + 'google_api_keys.h', + ], + # TODO(jschuh): crbug.com/167187 fix size_t to int truncations. + 'msvs_disabled_warnings': [4267, ], + }, + ], +} |