Roll src/third_party/WebKit bf18a82:a9cee16 (svn 185297:185304)
[chromium-blink-merge.git] / chrome / browser / resources / gaia_auth / background.js
blob09be3427a242a1e33da7d295b03f317ac90dbdd4
1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 /**
6 * @fileoverview
7 * A background script of the auth extension that bridges the communication
8 * between the main and injected scripts.
10 * Here is an overview of the communication flow when SAML is being used:
11 * 1. The main script sends the |startAuth| signal to this background script,
12 * indicating that the authentication flow has started and SAML pages may be
13 * loaded from now on.
14 * 2. A script is injected into each SAML page. The injected script sends three
15 * main types of messages to this background script:
16 * a) A |pageLoaded| message is sent when the page has been loaded. This is
17 * forwarded to the main script as |onAuthPageLoaded|.
18 * b) If the SAML provider supports the credential passing API, the API calls
19 * are sent to this background script as |apiCall| messages. These
20 * messages are forwarded unmodified to the main script.
21 * c) The injected script scrapes passwords. They are sent to this background
22 * script in |updatePassword| messages. The main script can request a list
23 * of the scraped passwords by sending the |getScrapedPasswords| message.
26 /**
27 * BackgroundBridgeManager maintains an array of BackgroundBridge, indexed by
28 * the associated tab id.
30 function BackgroundBridgeManager() {
33 BackgroundBridgeManager.prototype = {
34 CONTINUE_URL_BASE: 'chrome-extension://mfffpogegjflfpflabcdkioaeobkgjik' +
35 '/success.html',
36 // Maps a tab id to its associated BackgroundBridge.
37 bridges_: {},
39 run: function() {
40 chrome.runtime.onConnect.addListener(this.onConnect_.bind(this));
42 chrome.webRequest.onBeforeRequest.addListener(
43 function(details) {
44 if (this.bridges_[details.tabId])
45 return this.bridges_[details.tabId].onInsecureRequest(details.url);
46 }.bind(this),
47 {urls: ['http://*/*', 'file://*/*', 'ftp://*/*']},
48 ['blocking']);
50 chrome.webRequest.onBeforeSendHeaders.addListener(
51 function(details) {
52 if (this.bridges_[details.tabId])
53 return this.bridges_[details.tabId].onBeforeSendHeaders(details);
54 else
55 return {requestHeaders: details.requestHeaders};
56 }.bind(this),
57 {urls: ['*://*/*'], types: ['sub_frame']},
58 ['blocking', 'requestHeaders']);
60 chrome.webRequest.onHeadersReceived.addListener(
61 function(details) {
62 if (this.bridges_[details.tabId])
63 return this.bridges_[details.tabId].onHeadersReceived(details);
64 }.bind(this),
65 {urls: ['*://*/*'], types: ['sub_frame']},
66 ['blocking', 'responseHeaders']);
68 chrome.webRequest.onCompleted.addListener(
69 function(details) {
70 if (this.bridges_[details.tabId])
71 this.bridges_[details.tabId].onCompleted(details);
72 }.bind(this),
73 {urls: ['*://*/*', this.CONTINUE_URL_BASE + '*'], types: ['sub_frame']},
74 ['responseHeaders']);
77 onConnect_: function(port) {
78 var tabId = this.getTabIdFromPort_(port);
79 if (!this.bridges_[tabId])
80 this.bridges_[tabId] = new BackgroundBridge(tabId);
81 if (port.name == 'authMain') {
82 this.bridges_[tabId].setupForAuthMain(port);
83 port.onDisconnect.addListener(function() {
84 delete this.bridges_[tabId];
85 }.bind(this));
86 } else if (port.name == 'injected') {
87 this.bridges_[tabId].setupForInjected(port);
88 } else {
89 console.error('Unexpected connection, port.name=' + port.name);
93 getTabIdFromPort_: function(port) {
94 return port.sender.tab ? port.sender.tab.id : -1;
98 /**
99 * BackgroundBridge allows the main script and the injected script to
100 * collaborate. It forwards credentials API calls to the main script and
101 * maintains a list of scraped passwords.
102 * @param {string} tabId The associated tab ID.
104 function BackgroundBridge(tabId) {
105 this.tabId_ = tabId;
108 BackgroundBridge.prototype = {
109 // The associated tab ID. Only used for debugging now.
110 tabId: null,
112 isDesktopFlow_: false,
114 // Whether the extension is loaded in a constrained window.
115 // Set from main auth script.
116 isConstrainedWindow_: null,
118 // Email of the newly authenticated user based on the gaia response header
119 // 'google-accounts-signin'.
120 email_: null,
122 // Gaia Id of the newly authenticated user based on the gaia response
123 // header 'google-accounts-signin'.
124 gaiaId_: null,
126 // Session index of the newly authenticated user based on the gaia response
127 // header 'google-accounts-signin'.
128 sessionIndex_: null,
130 // Gaia URL base that is set from main auth script.
131 gaiaUrl_: null,
133 // Whether to abort the authentication flow and show an error messagen when
134 // content served over an unencrypted connection is detected.
135 blockInsecureContent_: false,
137 // Whether auth flow has started. It is used as a signal of whether the
138 // injected script should scrape passwords.
139 authStarted_: false,
141 // Whether SAML flow is going.
142 isSAML_: false,
144 passwordStore_: {},
146 channelMain_: null,
147 channelInjected_: null,
150 * Sets up the communication channel with the main script.
152 setupForAuthMain: function(port) {
153 this.channelMain_ = new Channel();
154 this.channelMain_.init(port);
156 // Registers for desktop related messages.
157 this.channelMain_.registerMessage(
158 'initDesktopFlow', this.onInitDesktopFlow_.bind(this));
160 // Registers for SAML related messages.
161 this.channelMain_.registerMessage(
162 'setGaiaUrl', this.onSetGaiaUrl_.bind(this));
163 this.channelMain_.registerMessage(
164 'setBlockInsecureContent', this.onSetBlockInsecureContent_.bind(this));
165 this.channelMain_.registerMessage(
166 'resetAuth', this.onResetAuth_.bind(this));
167 this.channelMain_.registerMessage(
168 'startAuth', this.onAuthStarted_.bind(this));
169 this.channelMain_.registerMessage(
170 'getScrapedPasswords',
171 this.onGetScrapedPasswords_.bind(this));
172 this.channelMain_.registerMessage(
173 'apiResponse', this.onAPIResponse_.bind(this));
175 this.channelMain_.send({
176 'name': 'channelConnected'
181 * Sets up the communication channel with the injected script.
183 setupForInjected: function(port) {
184 this.channelInjected_ = new Channel();
185 this.channelInjected_.init(port);
187 this.channelInjected_.registerMessage(
188 'apiCall', this.onAPICall_.bind(this));
189 this.channelInjected_.registerMessage(
190 'updatePassword', this.onUpdatePassword_.bind(this));
191 this.channelInjected_.registerMessage(
192 'pageLoaded', this.onPageLoaded_.bind(this));
193 this.channelInjected_.registerMessage(
194 'getSAMLFlag', this.onGetSAMLFlag_.bind(this));
198 * Handler for 'initDesktopFlow' signal sent from the main script.
199 * Only called in desktop mode.
201 onInitDesktopFlow_: function(msg) {
202 this.isDesktopFlow_ = true;
203 this.gaiaUrl_ = msg.gaiaUrl;
204 this.isConstrainedWindow_ = msg.isConstrainedWindow;
208 * Handler for webRequest.onCompleted. It 1) detects loading of continue URL
209 * and notifies the main script of signin completion; 2) detects if the
210 * current page could be loaded in a constrained window and signals the main
211 * script of switching to full tab if necessary.
213 onCompleted: function(details) {
214 // Only monitors requests in the gaia frame whose parent frame ID must be
215 // positive.
216 if (details.parentFrameId <= 0)
217 return;
219 if (details.url.lastIndexOf(backgroundBridgeManager.CONTINUE_URL_BASE, 0) ==
220 0) {
221 var skipForNow = false;
222 if (details.url.indexOf('ntp=1') >= 0)
223 skipForNow = true;
225 // TOOD(guohui): For desktop SAML flow, show password confirmation UI.
226 var passwords = this.onGetScrapedPasswords_();
227 var msg = {
228 'name': 'completeLogin',
229 'email': this.email_,
230 'gaiaId': this.gaiaId_,
231 'password': passwords[0],
232 'sessionIndex': this.sessionIndex_,
233 'skipForNow': skipForNow
235 this.channelMain_.send(msg);
236 } else if (this.isConstrainedWindow_) {
237 // The header google-accounts-embedded is only set on gaia domain.
238 if (this.gaiaUrl_ && details.url.lastIndexOf(this.gaiaUrl_) == 0) {
239 var headers = details.responseHeaders;
240 for (var i = 0; headers && i < headers.length; ++i) {
241 if (headers[i].name.toLowerCase() == 'google-accounts-embedded')
242 return;
245 var msg = {
246 'name': 'switchToFullTab',
247 'url': details.url
249 this.channelMain_.send(msg);
254 * Handler for webRequest.onBeforeRequest, invoked when content served over an
255 * unencrypted connection is detected. Determines whether the request should
256 * be blocked and if so, signals that an error message needs to be shown.
257 * @param {string} url The URL that was blocked.
258 * @return {!Object} Decision whether to block the request.
260 onInsecureRequest: function(url) {
261 if (!this.blockInsecureContent_)
262 return {};
263 this.channelMain_.send({name: 'onInsecureContentBlocked', url: url});
264 return {cancel: true};
268 * Handler or webRequest.onHeadersReceived. It reads the authenticated user
269 * email from google-accounts-signin-header.
270 * @return {!Object} Modified request headers.
272 onHeadersReceived: function(details) {
273 var headers = details.responseHeaders;
275 if (this.gaiaUrl_ && details.url.lastIndexOf(this.gaiaUrl_) == 0) {
276 for (var i = 0; headers && i < headers.length; ++i) {
277 if (headers[i].name.toLowerCase() == 'google-accounts-signin') {
278 var headerValues = headers[i].value.toLowerCase().split(',');
279 var signinDetails = {};
280 headerValues.forEach(function(e) {
281 var pair = e.split('=');
282 signinDetails[pair[0].trim()] = pair[1].trim();
284 // Remove "" around.
285 this.email_ = signinDetails['email'].slice(1, -1);
286 this.gaiaId_ = signinDetails['obfuscatedid'].slice(1, -1);
287 this.sessionIndex_ = signinDetails['sessionindex'];
288 break;
293 if (!this.isDesktopFlow_) {
294 // Check whether GAIA headers indicating the start or end of a SAML
295 // redirect are present. If so, synthesize cookies to mark these points.
296 for (var i = 0; headers && i < headers.length; ++i) {
297 if (headers[i].name.toLowerCase() == 'google-accounts-saml') {
298 var action = headers[i].value.toLowerCase();
299 if (action == 'start') {
300 this.isSAML_ = true;
301 // GAIA is redirecting to a SAML IdP. Any cookies contained in the
302 // current |headers| were set by GAIA. Any cookies set in future
303 // requests will be coming from the IdP. Append a cookie to the
304 // current |headers| that marks the point at which the redirect
305 // occurred.
306 headers.push({name: 'Set-Cookie',
307 value: 'google-accounts-saml-start=now'});
308 return {responseHeaders: headers};
309 } else if (action == 'end') {
310 this.isSAML_ = false;
311 // The SAML IdP has redirected back to GAIA. Add a cookie that marks
312 // the point at which the redirect occurred occurred. It is
313 // important that this cookie be prepended to the current |headers|
314 // because any cookies contained in the |headers| were already set
315 // by GAIA, not the IdP. Due to limitations in the webRequest API,
316 // it is not trivial to prepend a cookie:
318 // The webRequest API only allows for deleting and appending
319 // headers. To prepend a cookie (C), three steps are needed:
320 // 1) Delete any headers that set cookies (e.g., A, B).
321 // 2) Append a header which sets the cookie (C).
322 // 3) Append the original headers (A, B).
324 // Due to a further limitation of the webRequest API, it is not
325 // possible to delete a header in step 1) and append an identical
326 // header in step 3). To work around this, a trailing semicolon is
327 // added to each header before appending it. Trailing semicolons are
328 // ignored by Chrome in cookie headers, causing the modified headers
329 // to actually set the original cookies.
330 var otherHeaders = [];
331 var cookies = [{name: 'Set-Cookie',
332 value: 'google-accounts-saml-end=now'}];
333 for (var j = 0; j < headers.length; ++j) {
334 if (headers[j].name.toLowerCase().indexOf('set-cookie') == 0) {
335 var header = headers[j];
336 header.value += ';';
337 cookies.push(header);
338 } else {
339 otherHeaders.push(headers[j]);
342 return {responseHeaders: otherHeaders.concat(cookies)};
348 return {};
352 * Handler for webRequest.onBeforeSendHeaders.
353 * @return {!Object} Modified request headers.
355 onBeforeSendHeaders: function(details) {
356 if (!this.isDesktopFlow_ && this.gaiaUrl_ &&
357 details.url.indexOf(this.gaiaUrl_) == 0) {
358 details.requestHeaders.push({
359 name: 'X-Cros-Auth-Ext-Support',
360 value: 'SAML'
363 return {requestHeaders: details.requestHeaders};
367 * Handler for 'setGaiaUrl' signal sent from the main script.
369 onSetGaiaUrl_: function(msg) {
370 this.gaiaUrl_ = msg.gaiaUrl;
374 * Handler for 'setBlockInsecureContent' signal sent from the main script.
376 onSetBlockInsecureContent_: function(msg) {
377 this.blockInsecureContent_ = msg.blockInsecureContent;
381 * Handler for 'resetAuth' signal sent from the main script.
383 onResetAuth_: function() {
384 this.authStarted_ = false;
385 this.passwordStore_ = {};
389 * Handler for 'authStarted' signal sent from the main script.
391 onAuthStarted_: function() {
392 this.authStarted_ = true;
393 this.passwordStore_ = {};
397 * Handler for 'getScrapedPasswords' request sent from the main script.
398 * @return {Array.<string>} The array with de-duped scraped passwords.
400 onGetScrapedPasswords_: function() {
401 var passwords = {};
402 for (var property in this.passwordStore_) {
403 passwords[this.passwordStore_[property]] = true;
405 return Object.keys(passwords);
409 * Handler for 'apiResponse' signal sent from the main script. Passes on the
410 * |msg| to the injected script.
412 onAPIResponse_: function(msg) {
413 this.channelInjected_.send(msg);
416 onAPICall_: function(msg) {
417 this.channelMain_.send(msg);
420 onUpdatePassword_: function(msg) {
421 if (!this.authStarted_)
422 return;
424 this.passwordStore_[msg.id] = msg.password;
427 onPageLoaded_: function(msg) {
428 if (this.channelMain_)
429 this.channelMain_.send({name: 'onAuthPageLoaded',
430 url: msg.url,
431 isSAMLPage: this.isSAML_});
434 onGetSAMLFlag_: function(msg) {
435 return this.isSAML_;
439 var backgroundBridgeManager = new BackgroundBridgeManager();
440 backgroundBridgeManager.run();