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.
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
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.
27 * BackgroundBridgeManager maintains an array of BackgroundBridge, indexed by
28 * the associated tab id.
30 function BackgroundBridgeManager() {
34 BackgroundBridgeManager
.prototype = {
35 CONTINUE_URL_BASE
: 'chrome-extension://mfffpogegjflfpflabcdkioaeobkgjik' +
37 // Maps a tab id to its associated BackgroundBridge.
41 chrome
.runtime
.onConnect
.addListener(this.onConnect_
.bind(this));
43 chrome
.webRequest
.onBeforeRequest
.addListener(
45 if (this.bridges_
[details
.tabId
])
46 return this.bridges_
[details
.tabId
].onInsecureRequest(details
.url
);
48 {urls
: ['http://*/*', 'file://*/*', 'ftp://*/*']},
51 chrome
.webRequest
.onBeforeSendHeaders
.addListener(
53 if (this.bridges_
[details
.tabId
])
54 return this.bridges_
[details
.tabId
].onBeforeSendHeaders(details
);
56 return {requestHeaders
: details
.requestHeaders
};
58 {urls
: ['*://*/*'], types
: ['sub_frame']},
59 ['blocking', 'requestHeaders']);
61 chrome
.webRequest
.onHeadersReceived
.addListener(
63 if (this.bridges_
[details
.tabId
])
64 return this.bridges_
[details
.tabId
].onHeadersReceived(details
);
66 {urls
: ['*://*/*'], types
: ['sub_frame']},
67 ['blocking', 'responseHeaders']);
69 chrome
.webRequest
.onCompleted
.addListener(
71 if (this.bridges_
[details
.tabId
])
72 this.bridges_
[details
.tabId
].onCompleted(details
);
74 {urls
: ['*://*/*', this.CONTINUE_URL_BASE
+ '*'], types
: ['sub_frame']},
78 onConnect_: function(port
) {
79 var tabId
= this.getTabIdFromPort_(port
);
80 if (!this.bridges_
[tabId
])
81 this.bridges_
[tabId
] = new BackgroundBridge(tabId
);
82 if (port
.name
== 'authMain') {
83 this.bridges_
[tabId
].setupForAuthMain(port
);
84 port
.onDisconnect
.addListener(function() {
85 delete this.bridges_
[tabId
];
87 } else if (port
.name
== 'injected') {
88 this.bridges_
[tabId
].setupForInjected(port
);
90 console
.error('Unexpected connection, port.name=' + port
.name
);
94 getTabIdFromPort_: function(port
) {
95 return port
.sender
.tab
? port
.sender
.tab
.id
: -1;
100 * BackgroundBridge allows the main script and the injected script to
101 * collaborate. It forwards credentials API calls to the main script and
102 * maintains a list of scraped passwords.
103 * @param {string} tabId The associated tab ID.
105 function BackgroundBridge(tabId
) {
107 this.passwordStore_
= {};
110 BackgroundBridge
.prototype = {
111 // The associated tab ID. Only used for debugging now.
114 // The initial URL loaded in the gaia iframe. We only want to handle
115 // onCompleted() for the frame that loaded this URL.
116 initialFrameUrlWithoutParams
: null,
118 // On process onCompleted() requests that come from this frame Id.
121 isDesktopFlow_
: false,
123 // Whether the extension is loaded in a constrained window.
124 // Set from main auth script.
125 isConstrainedWindow_
: null,
127 // Email of the newly authenticated user based on the gaia response header
128 // 'google-accounts-signin'.
131 // Gaia Id of the newly authenticated user based on the gaia response
132 // header 'google-accounts-signin'.
135 // Session index of the newly authenticated user based on the gaia response
136 // header 'google-accounts-signin'.
139 // Gaia URL base that is set from main auth script.
142 // Whether to abort the authentication flow and show an error messagen when
143 // content served over an unencrypted connection is detected.
144 blockInsecureContent_
: false,
146 // Whether auth flow has started. It is used as a signal of whether the
147 // injected script should scrape passwords.
150 // Whether SAML flow is going.
153 passwordStore_
: null,
156 channelInjected_
: null,
159 * Sets up the communication channel with the main script.
161 setupForAuthMain: function(port
) {
162 this.channelMain_
= new Channel();
163 this.channelMain_
.init(port
);
165 // Registers for desktop related messages.
166 this.channelMain_
.registerMessage(
167 'initDesktopFlow', this.onInitDesktopFlow_
.bind(this));
169 // Registers for SAML related messages.
170 this.channelMain_
.registerMessage(
171 'setGaiaUrl', this.onSetGaiaUrl_
.bind(this));
172 this.channelMain_
.registerMessage(
173 'setBlockInsecureContent', this.onSetBlockInsecureContent_
.bind(this));
174 this.channelMain_
.registerMessage(
175 'resetAuth', this.onResetAuth_
.bind(this));
176 this.channelMain_
.registerMessage(
177 'startAuth', this.onAuthStarted_
.bind(this));
178 this.channelMain_
.registerMessage(
179 'getScrapedPasswords',
180 this.onGetScrapedPasswords_
.bind(this));
181 this.channelMain_
.registerMessage(
182 'apiResponse', this.onAPIResponse_
.bind(this));
184 this.channelMain_
.send({
185 'name': 'channelConnected'
190 * Sets up the communication channel with the injected script.
192 setupForInjected: function(port
) {
193 this.channelInjected_
= new Channel();
194 this.channelInjected_
.init(port
);
196 this.channelInjected_
.registerMessage(
197 'apiCall', this.onAPICall_
.bind(this));
198 this.channelInjected_
.registerMessage(
199 'updatePassword', this.onUpdatePassword_
.bind(this));
200 this.channelInjected_
.registerMessage(
201 'pageLoaded', this.onPageLoaded_
.bind(this));
202 this.channelInjected_
.registerMessage(
203 'getSAMLFlag', this.onGetSAMLFlag_
.bind(this));
207 * Handler for 'initDesktopFlow' signal sent from the main script.
208 * Only called in desktop mode.
210 onInitDesktopFlow_: function(msg
) {
211 this.isDesktopFlow_
= true;
212 this.gaiaUrl_
= msg
.gaiaUrl
;
213 this.isConstrainedWindow_
= msg
.isConstrainedWindow
;
214 this.initialFrameUrlWithoutParams
= msg
.initialFrameUrlWithoutParams
;
218 * Handler for webRequest.onCompleted. It 1) detects loading of continue URL
219 * and notifies the main script of signin completion; 2) detects if the
220 * current page could be loaded in a constrained window and signals the main
221 * script of switching to full tab if necessary.
223 onCompleted: function(details
) {
224 // Only monitors requests in the gaia frame. The gaia frame is the one
225 // where the initial frame URL completes.
226 if (details
.url
.lastIndexOf(
227 this.initialFrameUrlWithoutParams
, 0) == 0) {
228 this.frameId
= details
.frameId
;
230 if (this.frameId
== -1) {
231 // If for some reason the frameId could not be set above, just make sure
232 // the frame is more than two levels deep (since the gaia frame is at
233 // least three levels deep).
234 if (details
.parentFrameId
<= 0)
236 } else if (details
.frameId
!= this.frameId
) {
240 if (details
.url
.lastIndexOf(backgroundBridgeManager
.CONTINUE_URL_BASE
, 0) ==
242 var skipForNow
= false;
243 if (details
.url
.indexOf('ntp=1') >= 0)
246 // TOOD(guohui): For desktop SAML flow, show password confirmation UI.
247 var passwords
= this.onGetScrapedPasswords_();
249 'name': 'completeLogin',
250 'email': this.email_
,
251 'gaiaId': this.gaiaId_
,
252 'password': passwords
[0],
253 'sessionIndex': this.sessionIndex_
,
254 'skipForNow': skipForNow
256 this.channelMain_
.send(msg
);
257 } else if (this.isConstrainedWindow_
) {
258 // The header google-accounts-embedded is only set on gaia domain.
259 if (this.gaiaUrl_
&& details
.url
.lastIndexOf(this.gaiaUrl_
) == 0) {
260 var headers
= details
.responseHeaders
;
261 for (var i
= 0; headers
&& i
< headers
.length
; ++i
) {
262 if (headers
[i
].name
.toLowerCase() == 'google-accounts-embedded')
267 'name': 'switchToFullTab',
270 this.channelMain_
.send(msg
);
275 * Handler for webRequest.onBeforeRequest, invoked when content served over an
276 * unencrypted connection is detected. Determines whether the request should
277 * be blocked and if so, signals that an error message needs to be shown.
278 * @param {string} url The URL that was blocked.
279 * @return {!Object} Decision whether to block the request.
281 onInsecureRequest: function(url
) {
282 if (!this.blockInsecureContent_
)
284 this.channelMain_
.send({name
: 'onInsecureContentBlocked', url
: url
});
285 return {cancel
: true};
289 * Handler or webRequest.onHeadersReceived. It reads the authenticated user
290 * email from google-accounts-signin-header.
291 * @return {!Object} Modified request headers.
293 onHeadersReceived: function(details
) {
294 var headers
= details
.responseHeaders
;
296 if (this.gaiaUrl_
&& details
.url
.lastIndexOf(this.gaiaUrl_
) == 0) {
297 for (var i
= 0; headers
&& i
< headers
.length
; ++i
) {
298 if (headers
[i
].name
.toLowerCase() == 'google-accounts-signin') {
299 var headerValues
= headers
[i
].value
.toLowerCase().split(',');
300 var signinDetails
= {};
301 headerValues
.forEach(function(e
) {
302 var pair
= e
.split('=');
303 signinDetails
[pair
[0].trim()] = pair
[1].trim();
306 this.email_
= signinDetails
['email'].slice(1, -1);
307 this.gaiaId_
= signinDetails
['obfuscatedid'].slice(1, -1);
308 this.sessionIndex_
= signinDetails
['sessionindex'];
314 if (!this.isDesktopFlow_
) {
315 // Check whether GAIA headers indicating the start or end of a SAML
316 // redirect are present. If so, synthesize cookies to mark these points.
317 for (var i
= 0; headers
&& i
< headers
.length
; ++i
) {
318 if (headers
[i
].name
.toLowerCase() == 'google-accounts-saml') {
319 var action
= headers
[i
].value
.toLowerCase();
320 if (action
== 'start') {
322 // GAIA is redirecting to a SAML IdP. Any cookies contained in the
323 // current |headers| were set by GAIA. Any cookies set in future
324 // requests will be coming from the IdP. Append a cookie to the
325 // current |headers| that marks the point at which the redirect
327 headers
.push({name
: 'Set-Cookie',
328 value
: 'google-accounts-saml-start=now'});
329 return {responseHeaders
: headers
};
330 } else if (action
== 'end') {
331 this.isSAML_
= false;
332 // The SAML IdP has redirected back to GAIA. Add a cookie that marks
333 // the point at which the redirect occurred occurred. It is
334 // important that this cookie be prepended to the current |headers|
335 // because any cookies contained in the |headers| were already set
336 // by GAIA, not the IdP. Due to limitations in the webRequest API,
337 // it is not trivial to prepend a cookie:
339 // The webRequest API only allows for deleting and appending
340 // headers. To prepend a cookie (C), three steps are needed:
341 // 1) Delete any headers that set cookies (e.g., A, B).
342 // 2) Append a header which sets the cookie (C).
343 // 3) Append the original headers (A, B).
345 // Due to a further limitation of the webRequest API, it is not
346 // possible to delete a header in step 1) and append an identical
347 // header in step 3). To work around this, a trailing semicolon is
348 // added to each header before appending it. Trailing semicolons are
349 // ignored by Chrome in cookie headers, causing the modified headers
350 // to actually set the original cookies.
351 var otherHeaders
= [];
352 var cookies
= [{name
: 'Set-Cookie',
353 value
: 'google-accounts-saml-end=now'}];
354 for (var j
= 0; j
< headers
.length
; ++j
) {
355 if (headers
[j
].name
.toLowerCase().indexOf('set-cookie') == 0) {
356 var header
= headers
[j
];
358 cookies
.push(header
);
360 otherHeaders
.push(headers
[j
]);
363 return {responseHeaders
: otherHeaders
.concat(cookies
)};
373 * Handler for webRequest.onBeforeSendHeaders.
374 * @return {!Object} Modified request headers.
376 onBeforeSendHeaders: function(details
) {
377 if (!this.isDesktopFlow_
&& this.gaiaUrl_
&&
378 details
.url
.indexOf(this.gaiaUrl_
) == 0) {
379 details
.requestHeaders
.push({
380 name
: 'X-Cros-Auth-Ext-Support',
384 return {requestHeaders
: details
.requestHeaders
};
388 * Handler for 'setGaiaUrl' signal sent from the main script.
390 onSetGaiaUrl_: function(msg
) {
391 this.gaiaUrl_
= msg
.gaiaUrl
;
395 * Handler for 'setBlockInsecureContent' signal sent from the main script.
397 onSetBlockInsecureContent_: function(msg
) {
398 this.blockInsecureContent_
= msg
.blockInsecureContent
;
402 * Handler for 'resetAuth' signal sent from the main script.
404 onResetAuth_: function() {
405 this.authStarted_
= false;
406 this.passwordStore_
= {};
407 this.isSAML_
= false;
411 * Handler for 'authStarted' signal sent from the main script.
413 onAuthStarted_: function() {
414 this.authStarted_
= true;
415 this.passwordStore_
= {};
416 this.isSAML_
= false;
420 * Handler for 'getScrapedPasswords' request sent from the main script.
421 * @return {Array<string>} The array with de-duped scraped passwords.
423 onGetScrapedPasswords_: function() {
425 for (var property
in this.passwordStore_
) {
426 passwords
[this.passwordStore_
[property
]] = true;
428 return Object
.keys(passwords
);
432 * Handler for 'apiResponse' signal sent from the main script. Passes on the
433 * |msg| to the injected script.
435 onAPIResponse_: function(msg
) {
436 this.channelInjected_
.send(msg
);
439 onAPICall_: function(msg
) {
440 this.channelMain_
.send(msg
);
443 onUpdatePassword_: function(msg
) {
444 if (!this.authStarted_
)
447 this.passwordStore_
[msg
.id
] = msg
.password
;
450 onPageLoaded_: function(msg
) {
451 if (this.channelMain_
)
452 this.channelMain_
.send({name
: 'onAuthPageLoaded',
454 isSAMLPage
: this.isSAML_
});
457 onGetSAMLFlag_: function(msg
) {
462 var backgroundBridgeManager
= new BackgroundBridgeManager();
463 backgroundBridgeManager
.run();