Commit 008e0bfa authored by bartfab@chromium.org's avatar bartfab@chromium.org

Use GAIA headers to distinguish between GAIA and SAML IdP cookies

During login, GAIA sends the "Google-Accounts-SAML=Start" and
"Google-Accounts-SAML=End" headers to indicate when a redirect to a SAML
IdP starts and ends. This CL uses these headers to distinguish between
cookies set by GAIA and the IdP:
* Any cookies set between the start and the end of the redirect were
  created by the IdP.
* Any cookies set before the start or after the end of the redirect
  were created by GAIA.

This allows cookies set by the IdP to be copied to the user's session
while ensuring that GAIA cookies are not copied (these need to be
transferred to the session via GAIA's /MergeSession API).

BUG=381123
TEST=Extended browser test; also manual

Review URL: https://codereview.chromium.org/447013003

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@288068 0039d316-1c4b-4281-b951-d872f2087c98
parent 0f59f358
......@@ -13,6 +13,7 @@
#include "base/logging.h"
#include "base/memory/ref_counted.h"
#include "base/message_loop/message_loop.h"
#include "base/time/time.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "net/cookies/canonical_cookie.h"
......@@ -33,15 +34,8 @@ namespace chromeos {
namespace {
// Given a |cookie| set during login, returns true if the cookie may have been
// set by GAIA. While GAIA can set cookies for many different domains, the
// domain names it sets cookies for during Chrome OS login will always contain
// the strings "google" or "youtube".
bool IsGAIACookie(const net::CanonicalCookie& cookie) {
const std::string& domain = cookie.Domain();
return domain.find("google") != std::string::npos ||
domain.find("youtube") != std::string::npos;
}
const char kSAMLStartCookie[] = "google-accounts-saml-start";
const char kSAMLEndCookie[] = "google-accounts-saml-end";
class ProfileAuthDataTransferer {
public:
......@@ -86,6 +80,17 @@ class ProfileAuthDataTransferer {
void OnChannelIDsToTransferRetrieved(
const net::ChannelIDStore::ChannelIDList& channel_ids_to_transfer);
// Given a |cookie| set during login, returns true if the cookie may have been
// set by GAIA. The main criterion is the |cookie|'s creation date. The points
// in time at which redirects from GAIA to SAML IdP and back occur are stored
// in |saml_start_time_| and |saml_end_time_|. If the cookie was set between
// these two times, it was created by the SAML IdP. Otherwise, it was created
// by GAIA.
// As an additional precaution, the cookie's domain is checked. If the domain
// contains "google" or "youtube", the cookie is considered to have been set
// by GAIA as well.
bool IsGAIACookie(const net::CanonicalCookie& cookie);
// If all data to be transferred has been retrieved already, transfer it to
// |to_context_| and call Finish().
void MaybeTransferCookiesAndChannelIDs();
......@@ -103,6 +108,11 @@ class ProfileAuthDataTransferer {
net::CookieList cookies_to_transfer_;
net::ChannelIDStore::ChannelIDList channel_ids_to_transfer_;
// The time at which a redirect from GAIA to a SAML IdP occurred.
base::Time saml_start_time_;
// The time at which a redirect from a SAML IdP back to GAIA occurred.
base::Time saml_end_time_;
bool first_login_;
bool waiting_for_auth_cookies_;
bool waiting_for_channel_ids_;
......@@ -214,6 +224,25 @@ void ProfileAuthDataTransferer::OnCookiesToTransferRetrieved(
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
waiting_for_auth_cookies_ = false;
cookies_to_transfer_ = cookies_to_transfer;
// Look for cookies indicating the points in time at which redirects from GAIA
// to SAML IdP and back occurred. These cookies are synthesized by
// chrome/browser/resources/gaia_auth/background.js. If the cookies are found,
// their creation times are stored in |saml_start_time_| and
// |cookies_to_transfer_| and the cookies are deleted.
for (net::CookieList::iterator it = cookies_to_transfer_.begin();
it != cookies_to_transfer_.end(); ) {
if (it->Name() == kSAMLStartCookie) {
saml_start_time_ = it->CreationDate();
it = cookies_to_transfer_.erase(it);
} else if (it->Name() == kSAMLEndCookie) {
saml_end_time_ = it->CreationDate();
it = cookies_to_transfer_.erase(it);
} else {
++it;
}
}
MaybeTransferCookiesAndChannelIDs();
}
......@@ -235,6 +264,19 @@ void ProfileAuthDataTransferer::OnChannelIDsToTransferRetrieved(
MaybeTransferCookiesAndChannelIDs();
}
bool ProfileAuthDataTransferer::IsGAIACookie(
const net::CanonicalCookie& cookie) {
const base::Time& creation_date = cookie.CreationDate();
if (creation_date < saml_start_time_)
return true;
if (!saml_end_time_.is_null() && creation_date > saml_end_time_)
return true;
const std::string& domain = cookie.Domain();
return domain.find("google") != std::string::npos ||
domain.find("youtube") != std::string::npos;
}
void ProfileAuthDataTransferer::MaybeTransferCookiesAndChannelIDs() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
if (waiting_for_auth_cookies_ || waiting_for_channel_ids_)
......
......@@ -83,8 +83,13 @@ namespace chromeos {
namespace {
const char kTestAuthSIDCookie[] = "fake-auth-SID-cookie";
const char kTestAuthLSIDCookie[] = "fake-auth-LSID-cookie";
const char kGAIASIDCookieName[] = "SID";
const char kGAIALSIDCookieName[] = "LSID";
const char kTestAuthSIDCookie1[] = "fake-auth-SID-cookie-1";
const char kTestAuthSIDCookie2[] = "fake-auth-SID-cookie-2";
const char kTestAuthLSIDCookie1[] = "fake-auth-LSID-cookie-1";
const char kTestAuthLSIDCookie2[] = "fake-auth-LSID-cookie-2";
const char kTestAuthCode[] = "fake-auth-code";
const char kTestGaiaUberToken[] = "fake-uber-token";
const char kTestAuthLoginAccessToken[] = "fake-access-token";
......@@ -98,6 +103,7 @@ const char kHTTPSAMLUserEmail[] = "carol@example.com";
const char kNonSAMLUserEmail[] = "dan@example.com";
const char kDifferentDomainSAMLUserEmail[] = "eve@example.test";
const char kSAMLIdPCookieName[] = "saml";
const char kSAMLIdPCookieValue1[] = "value-1";
const char kSAMLIdPCookieValue2[] = "value-2";
......@@ -296,7 +302,9 @@ class SamlTest : public InProcessBrowserTest {
}
virtual void SetUpOnMainThread() OVERRIDE {
SetMergeSessionParams(kFirstSAMLUserEmail);
SetMergeSessionParams(kFirstSAMLUserEmail,
kTestAuthSIDCookie1,
kTestAuthLSIDCookie1);
embedded_test_server()->RegisterRequestHandler(
base::Bind(&FakeGaia::HandleRequest, base::Unretained(&fake_gaia_)));
......@@ -320,10 +328,12 @@ class SamlTest : public InProcessBrowserTest {
}
}
void SetMergeSessionParams(const std::string& email) {
void SetMergeSessionParams(const std::string& email,
const std::string& auth_sid_cookie,
const std::string& auth_lsid_cookie) {
FakeGaia::MergeSessionParams params;
params.auth_sid_cookie = kTestAuthSIDCookie;
params.auth_lsid_cookie = kTestAuthLSIDCookie;
params.auth_sid_cookie = auth_sid_cookie;
params.auth_lsid_cookie = auth_lsid_cookie;
params.auth_code = kTestAuthCode;
params.refresh_token = kTestRefreshToken;
params.access_token = kTestAuthLoginAccessToken;
......@@ -580,7 +590,7 @@ IN_PROC_BROWSER_TEST_F(SamlTest, FailToRetrieveAutenticatedUserEmailAddress) {
fake_saml_idp()->SetLoginHTMLTemplate("saml_login.html");
StartSamlAndWaitForIdpPageLoad(kFirstSAMLUserEmail);
SetMergeSessionParams("");
SetMergeSessionParams("", kTestAuthSIDCookie1, kTestAuthLSIDCookie1);
SetSignFormField("Email", "fake_user");
SetSignFormField("Password", "fake_password");
ExecuteJsInSigninFrame("document.getElementById('Submit').click();");
......@@ -662,16 +672,21 @@ class SAMLPolicyTest : public SamlTest {
void EnableTransferSAMLCookiesPolicy();
void ShowGAIALoginForm();
void LogInWithSAML(const std::string& user_id);
void VerifySAMLIdPCookieValue(const std::string& expected_cookie_value);
void LogInWithSAML(const std::string& user_id,
const std::string& auth_sid_cookie,
const std::string& auth_lsid_cookie);
std::string GetCookieValue(const std::string& name);
void GetCookies();
protected:
void GetCookiesOnIOThread(
const scoped_refptr<net::URLRequestContextGetter>& request_context,
const base::Closure& callback);
void StoreCookieList(const base::Closure& callback,
const net::CookieList& cookie_list);
protected:
policy::DevicePolicyCrosTestHelper test_helper_;
// FakeDBusThreadManager uses FakeSessionManagerClient.
......@@ -772,11 +787,13 @@ void SAMLPolicyTest::ShowGAIALoginForm() {
EXPECT_EQ("\"ready\"", message);
}
void SAMLPolicyTest::LogInWithSAML(const std::string& user_id) {
void SAMLPolicyTest::LogInWithSAML(const std::string& user_id,
const std::string& auth_sid_cookie,
const std::string& auth_lsid_cookie) {
fake_saml_idp()->SetLoginHTMLTemplate("saml_login.html");
StartSamlAndWaitForIdpPageLoad(user_id);
SetMergeSessionParams(user_id);
SetMergeSessionParams(user_id, auth_sid_cookie, auth_lsid_cookie);
SetSignFormField("Email", "fake_user");
SetSignFormField("Password", "fake_password");
ExecuteJsInSigninFrame("document.getElementById('Submit').click();");
......@@ -789,8 +806,16 @@ void SAMLPolicyTest::LogInWithSAML(const std::string& user_id) {
content::NotificationService::AllSources()).Wait();
}
void SAMLPolicyTest::VerifySAMLIdPCookieValue(
const std::string& expected_cookie_value) {
std::string SAMLPolicyTest::GetCookieValue(const std::string& name) {
for (net::CookieList::const_iterator it = cookie_list_.begin();
it != cookie_list_.end(); ++it) {
if (it->Name() == name)
return it->Value();
}
return std::string();
}
void SAMLPolicyTest::GetCookies() {
Profile* profile =chromeos::ProfileHelper::Get()->GetProfileByUser(
UserManager::Get()->GetActiveUser());
ASSERT_TRUE(profile);
......@@ -804,17 +829,6 @@ void SAMLPolicyTest::VerifySAMLIdPCookieValue(
profile->GetRequestContext()),
run_loop.QuitClosure()));
run_loop.Run();
net::CanonicalCookie const* saml_cookie = NULL;
for (net::CookieList::const_iterator it = cookie_list_.begin();
it != cookie_list_.end(); ++it) {
if (it->Name() == "saml") {
saml_cookie = &*it;
break;
}
}
ASSERT_TRUE(saml_cookie);
EXPECT_EQ(expected_cookie_value, saml_cookie->Value());
}
void SAMLPolicyTest::GetCookiesOnIOThread(
......@@ -836,7 +850,7 @@ void SAMLPolicyTest::StoreCookieList(
callback);
}
IN_PROC_BROWSER_TEST_F(SAMLPolicyTest, DISABLED_PRE_NoSAML) {
IN_PROC_BROWSER_TEST_F(SAMLPolicyTest, PRE_NoSAML) {
// Set the offline login time limit for SAML users to zero.
SetSAMLOfflineSigninTimeLimitPolicy(0);
......@@ -852,7 +866,7 @@ IN_PROC_BROWSER_TEST_F(SAMLPolicyTest, DISABLED_PRE_NoSAML) {
// Verifies that the offline login time limit does not affect a user who
// authenticated without SAML.
IN_PROC_BROWSER_TEST_F(SAMLPolicyTest, DISABLED_NoSAML) {
IN_PROC_BROWSER_TEST_F(SAMLPolicyTest, NoSAML) {
login_screen_load_observer_->Wait();
// Verify that offline login is allowed.
JsExpect("window.getComputedStyle(document.querySelector("
......@@ -863,7 +877,7 @@ IN_PROC_BROWSER_TEST_F(SAMLPolicyTest, PRE_SAMLNoLimit) {
// Remove the offline login time limit for SAML users.
SetSAMLOfflineSigninTimeLimitPolicy(-1);
LogInWithSAML(kFirstSAMLUserEmail);
LogInWithSAML(kFirstSAMLUserEmail, kTestAuthSIDCookie1, kTestAuthLSIDCookie1);
}
// Verifies that when no offline login time limit is set, a user who
......@@ -879,7 +893,7 @@ IN_PROC_BROWSER_TEST_F(SAMLPolicyTest, PRE_SAMLZeroLimit) {
// Set the offline login time limit for SAML users to zero.
SetSAMLOfflineSigninTimeLimitPolicy(0);
LogInWithSAML(kFirstSAMLUserEmail);
LogInWithSAML(kFirstSAMLUserEmail, kTestAuthSIDCookie1, kTestAuthLSIDCookie1);
}
// Verifies that when the offline login time limit is exceeded for a user who
......@@ -891,56 +905,80 @@ IN_PROC_BROWSER_TEST_F(SAMLPolicyTest, SAMLZeroLimit) {
" '#pod-row .signin-button-container')).display != 'none'");
}
IN_PROC_BROWSER_TEST_F(SAMLPolicyTest, DISABLED_PRE_PRE_TransferCookiesAffiliated) {
IN_PROC_BROWSER_TEST_F(SAMLPolicyTest, PRE_PRE_TransferCookiesAffiliated) {
fake_saml_idp()->SetCookieValue(kSAMLIdPCookieValue1);
LogInWithSAML(kFirstSAMLUserEmail);
VerifySAMLIdPCookieValue(kSAMLIdPCookieValue1);
LogInWithSAML(kFirstSAMLUserEmail, kTestAuthSIDCookie1, kTestAuthLSIDCookie1);
GetCookies();
EXPECT_EQ(kTestAuthSIDCookie1, GetCookieValue(kGAIASIDCookieName));
EXPECT_EQ(kTestAuthLSIDCookie1, GetCookieValue(kGAIALSIDCookieName));
EXPECT_EQ(kSAMLIdPCookieValue1, GetCookieValue(kSAMLIdPCookieName));
}
// Verifies that when the DeviceTransferSAMLCookies policy is not enabled, SAML
// IdP cookies are not transferred to a user's profile on subsequent login, even
// if the user belongs to the domain that the device is enrolled into.
IN_PROC_BROWSER_TEST_F(SAMLPolicyTest, DISABLED_PRE_TransferCookiesAffiliated) {
// if the user belongs to the domain that the device is enrolled into. Also
// verifies that GAIA cookies are not transferred.
IN_PROC_BROWSER_TEST_F(SAMLPolicyTest, PRE_TransferCookiesAffiliated) {
fake_saml_idp()->SetCookieValue(kSAMLIdPCookieValue2);
fake_saml_idp()->SetLoginHTMLTemplate("saml_login.html");
ShowGAIALoginForm();
LogInWithSAML(kFirstSAMLUserEmail, kTestAuthSIDCookie2, kTestAuthLSIDCookie2);
LogInWithSAML(kFirstSAMLUserEmail);
VerifySAMLIdPCookieValue(kSAMLIdPCookieValue1);
GetCookies();
EXPECT_EQ(kTestAuthSIDCookie1, GetCookieValue(kGAIASIDCookieName));
EXPECT_EQ(kTestAuthLSIDCookie1, GetCookieValue(kGAIALSIDCookieName));
EXPECT_EQ(kSAMLIdPCookieValue1, GetCookieValue(kSAMLIdPCookieName));
}
// Verifies that when the DeviceTransferSAMLCookies policy is enabled, SAML IdP
// cookies are transferred to a user's profile on subsequent login when the user
// belongs to the domain that the device is enrolled into.
IN_PROC_BROWSER_TEST_F(SAMLPolicyTest, DISABLED_TransferCookiesAffiliated) {
// belongs to the domain that the device is enrolled into. Also verifies that
// GAIA cookies are not transferred.
IN_PROC_BROWSER_TEST_F(SAMLPolicyTest, TransferCookiesAffiliated) {
fake_saml_idp()->SetCookieValue(kSAMLIdPCookieValue2);
fake_saml_idp()->SetLoginHTMLTemplate("saml_login.html");
ShowGAIALoginForm();
EnableTransferSAMLCookiesPolicy();
LogInWithSAML(kFirstSAMLUserEmail, kTestAuthSIDCookie2, kTestAuthLSIDCookie2);
LogInWithSAML(kFirstSAMLUserEmail);
VerifySAMLIdPCookieValue(kSAMLIdPCookieValue2);
GetCookies();
EXPECT_EQ(kTestAuthSIDCookie1, GetCookieValue(kGAIASIDCookieName));
EXPECT_EQ(kTestAuthLSIDCookie1, GetCookieValue(kGAIALSIDCookieName));
EXPECT_EQ(kSAMLIdPCookieValue2, GetCookieValue(kSAMLIdPCookieName));
}
IN_PROC_BROWSER_TEST_F(SAMLPolicyTest, PRE_TransferCookiesUnaffiliated) {
fake_saml_idp()->SetCookieValue(kSAMLIdPCookieValue1);
LogInWithSAML(kDifferentDomainSAMLUserEmail);
VerifySAMLIdPCookieValue(kSAMLIdPCookieValue1);
LogInWithSAML(kDifferentDomainSAMLUserEmail,
kTestAuthSIDCookie1,
kTestAuthLSIDCookie1);
GetCookies();
EXPECT_EQ(kTestAuthSIDCookie1, GetCookieValue(kGAIASIDCookieName));
EXPECT_EQ(kTestAuthLSIDCookie1, GetCookieValue(kGAIALSIDCookieName));
EXPECT_EQ(kSAMLIdPCookieValue1, GetCookieValue(kSAMLIdPCookieName));
}
// Verifies that even if the DeviceTransferSAMLCookies policy is enabled, SAML
// IdP are not transferred to a user's profile on subsequent login if the user
// does not belong to the domain that the device is enrolled into.
// does not belong to the domain that the device is enrolled into. Also verifies
// that GAIA cookies are not transferred.
IN_PROC_BROWSER_TEST_F(SAMLPolicyTest, TransferCookiesUnaffiliated) {
fake_saml_idp()->SetCookieValue(kSAMLIdPCookieValue2);
fake_saml_idp()->SetLoginHTMLTemplate("saml_login.html");
ShowGAIALoginForm();
EnableTransferSAMLCookiesPolicy();
LogInWithSAML(kDifferentDomainSAMLUserEmail);
VerifySAMLIdPCookieValue(kSAMLIdPCookieValue1);
LogInWithSAML(kDifferentDomainSAMLUserEmail,
kTestAuthSIDCookie1,
kTestAuthLSIDCookie1);
GetCookies();
EXPECT_EQ(kTestAuthSIDCookie1, GetCookieValue(kGAIASIDCookieName));
EXPECT_EQ(kTestAuthLSIDCookie1, GetCookieValue(kGAIALSIDCookieName));
EXPECT_EQ(kSAMLIdPCookieValue1, GetCookieValue(kSAMLIdPCookieName));
}
} // namespace chromeos
......@@ -58,10 +58,10 @@ BackgroundBridgeManager.prototype = {
chrome.webRequest.onHeadersReceived.addListener(
function(details) {
if (this.bridges_[details.tabId])
this.bridges_[details.tabId].onHeadersReceived(details);
return this.bridges_[details.tabId].onHeadersReceived(details);
}.bind(this),
{urls: ['*://*/*'], types: ['sub_frame']},
['responseHeaders']);
['blocking', 'responseHeaders']);
chrome.webRequest.onCompleted.addListener(
function(details) {
......@@ -260,17 +260,16 @@ BackgroundBridge.prototype = {
/**
* Handler or webRequest.onHeadersReceived. It reads the authenticated user
* email from google-accounts-signin-header.
* @return {!Object} Modified request headers.
*/
onHeadersReceived: function(details) {
if (!this.isDesktopFlow_ ||
!this.gaiaUrl_ ||
details.url.lastIndexOf(this.gaiaUrl_) != 0) {
var headers = details.responseHeaders;
if (this.isDesktopFlow_ &&
this.gaiaUrl_ &&
details.url.lastIndexOf(this.gaiaUrl_) == 0) {
// TODO(xiyuan, guohui): CrOS should reuse the logic below for reading the
// email for SAML users and cut off the /ListAccount call.
return;
}
var headers = details.responseHeaders;
for (var i = 0; headers && i < headers.length; ++i) {
if (headers[i].name.toLowerCase() == 'google-accounts-signin') {
var headerValues = headers[i].value.toLowerCase().split(',');
......@@ -282,9 +281,65 @@ BackgroundBridge.prototype = {
// Remove "" around.
this.email_ = signinDetails['email'].slice(1, -1);
this.sessionIndex_ = signinDetails['sessionindex'];
return;
break;
}
}
}
if (!this.isDesktopFlow_) {
// Check whether GAIA headers indicating the start or end of a SAML
// redirect are present. If so, synthesize cookies to mark these points.
for (var i = 0; headers && i < headers.length; ++i) {
if (headers[i].name.toLowerCase() == 'google-accounts-saml') {
var action = headers[i].value.toLowerCase();
if (action == 'start') {
// GAIA is redirecting to a SAML IdP. Any cookies contained in the
// current |headers| were set by GAIA. Any cookies set in future
// requests will be coming from the IdP. Append a cookie to the
// current |headers| that marks the point at which the redirect
// occurred.
headers.push({name: 'Set-Cookie',
value: 'google-accounts-saml-start=now'});
return {responseHeaders: headers};
} else if (action == 'end') {
// The SAML IdP has redirected back to GAIA. Add a cookie that marks
// the point at which the redirect occurred occurred. It is
// important that this cookie be prepended to the current |headers|
// because any cookies contained in the |headers| were already set
// by GAIA, not the IdP. Due to limitations in the webRequest API,
// it is not trivial to prepend a cookie:
//
// The webRequest API only allows for deleting and appending
// headers. To prepend a cookie (C), three steps are needed:
// 1) Delete any headers that set cookies (e.g., A, B).
// 2) Append a header which sets the cookie (C).
// 3) Append the original headers (A, B).
//
// Due to a further limitation of the webRequest API, it is not
// possible to delete a header in step 1) and append an identical
// header in step 3). To work around this, a trailing semicolon is
// added to each header before appending it. Trailing semicolons are
// ignored by Chrome in cookie headers, causing the modified headers
// to actually set the original cookies.
var otherHeaders = [];
var cookies = [{name: 'Set-Cookie',
value: 'google-accounts-saml-end=now'}];
for (var j = 0; j < headers.length; ++j) {
if (headers[j].name.toLowerCase().indexOf('set-cookie') == 0) {
var header = headers[j];
header.value += ';';
cookies.push(header);
} else {
otherHeaders.push(headers[j]);
}
}
return {responseHeaders: otherHeaders.concat(cookies)};
}
}
}
}
return {};
},
/**
......
......@@ -385,6 +385,7 @@ void FakeGaia::HandleServiceLoginAuth(const HttpRequest& request,
url = net::AppendQueryParameter(url, "SAMLRequest", "fake_request");
url = net::AppendQueryParameter(url, "RelayState", continue_url);
redirect_url = url.spec();
http_response->AddCustomHeader("Google-Accounts-SAML", "Start");
} else if (!merge_session_params_.auth_sid_cookie.empty() &&
!merge_session_params_.auth_lsid_cookie.empty()) {
SetCookies(http_response,
......@@ -409,6 +410,7 @@ void FakeGaia::HandleSSO(const HttpRequest& request,
std::string redirect_url = relay_state;
http_response->set_code(net::HTTP_TEMPORARY_REDIRECT);
http_response->AddCustomHeader("Location", redirect_url);
http_response->AddCustomHeader("Google-Accounts-SAML", "End");
}
void FakeGaia::HandleAuthToken(const HttpRequest& request,
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment