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() {
33 BackgroundBridgeManager
.prototype = {
34 // Maps a tab id to its associated BackgroundBridge.
38 chrome
.runtime
.onConnect
.addListener(this.onConnect_
.bind(this));
40 chrome
.webRequest
.onBeforeRequest
.addListener(
42 if (this.bridges_
[details
.tabId
])
43 return this.bridges_
[details
.tabId
].onInsecureRequest(details
.url
);
45 {urls
: ['http://*/*', 'file://*/*', 'ftp://*/*']},
48 chrome
.webRequest
.onBeforeSendHeaders
.addListener(
50 if (this.bridges_
[details
.tabId
])
51 return this.bridges_
[details
.tabId
].onBeforeSendHeaders(details
);
53 return {requestHeaders
: details
.requestHeaders
};
55 {urls
: ['*://*/*'], types
: ['sub_frame']},
56 ['blocking', 'requestHeaders']);
58 chrome
.webRequest
.onHeadersReceived
.addListener(
60 if (this.bridges_
[details
.tabId
])
61 this.bridges_
[details
.tabId
].onHeadersReceived(details
);
63 {urls
: ['*://*/*'], types
: ['sub_frame']},
66 chrome
.webRequest
.onCompleted
.addListener(
68 if (this.bridges_
[details
.tabId
])
69 this.bridges_
[details
.tabId
].onCompleted(details
);
71 {urls
: ['*://*/*'], types
: ['sub_frame']},
75 onConnect_: function(port
) {
76 var tabId
= this.getTabIdFromPort_(port
);
77 if (!this.bridges_
[tabId
])
78 this.bridges_
[tabId
] = new BackgroundBridge(tabId
);
79 if (port
.name
== 'authMain') {
80 this.bridges_
[tabId
].setupForAuthMain(port
);
81 port
.onDisconnect
.addListener(function() {
82 delete this.bridges_
[tabId
];
84 } else if (port
.name
== 'injected') {
85 this.bridges_
[tabId
].setupForInjected(port
);
87 console
.error('Unexpected connection, port.name=' + port
.name
);
91 getTabIdFromPort_: function(port
) {
92 return port
.sender
.tab
? port
.sender
.tab
.id
: -1;
97 * BackgroundBridge allows the main script and the injected script to
98 * collaborate. It forwards credentials API calls to the main script and
99 * maintains a list of scraped passwords.
100 * @param {string} tabId The associated tab ID.
102 function BackgroundBridge(tabId
) {
106 BackgroundBridge
.prototype = {
107 // The associated tab ID. Only used for debugging now.
110 isDesktopFlow_
: false,
112 // Continue URL that is set from main auth script.
115 // Whether the extension is loaded in a constrained window.
116 // Set from main auth script.
117 isConstrainedWindow_
: null,
119 // Email of the newly authenticated user based on the gaia response header
120 // 'google-accounts-signin'.
123 // Session index of the newly authenticated user based on the gaia response
124 // header 'google-accounts-signin'.
127 // Gaia URL base that is set from main auth script.
130 // Whether to abort the authentication flow and show an error messagen when
131 // content served over an unencrypted connection is detected.
132 blockInsecureContent_
: false,
134 // Whether auth flow has started. It is used as a signal of whether the
135 // injected script should scrape passwords.
141 channelInjected_
: null,
144 * Sets up the communication channel with the main script.
146 setupForAuthMain: function(port
) {
147 this.channelMain_
= new Channel();
148 this.channelMain_
.init(port
);
150 // Registers for desktop related messages.
151 this.channelMain_
.registerMessage(
152 'initDesktopFlow', this.onInitDesktopFlow_
.bind(this));
154 // Registers for SAML related messages.
155 this.channelMain_
.registerMessage(
156 'setGaiaUrl', this.onSetGaiaUrl_
.bind(this));
157 this.channelMain_
.registerMessage(
158 'setBlockInsecureContent', this.onSetBlockInsecureContent_
.bind(this));
159 this.channelMain_
.registerMessage(
160 'resetAuth', this.onResetAuth_
.bind(this));
161 this.channelMain_
.registerMessage(
162 'startAuth', this.onAuthStarted_
.bind(this));
163 this.channelMain_
.registerMessage(
164 'getScrapedPasswords',
165 this.onGetScrapedPasswords_
.bind(this));
166 this.channelMain_
.registerMessage(
167 'apiResponse', this.onAPIResponse_
.bind(this));
169 this.channelMain_
.send({
170 'name': 'channelConnected'
175 * Sets up the communication channel with the injected script.
177 setupForInjected: function(port
) {
178 this.channelInjected_
= new Channel();
179 this.channelInjected_
.init(port
);
181 this.channelInjected_
.registerMessage(
182 'apiCall', this.onAPICall_
.bind(this));
183 this.channelInjected_
.registerMessage(
184 'updatePassword', this.onUpdatePassword_
.bind(this));
185 this.channelInjected_
.registerMessage(
186 'pageLoaded', this.onPageLoaded_
.bind(this));
190 * Handler for 'initDesktopFlow' signal sent from the main script.
191 * Only called in desktop mode.
193 onInitDesktopFlow_: function(msg
) {
194 this.isDesktopFlow_
= true;
195 this.gaiaUrl_
= msg
.gaiaUrl
;
196 this.continueUrl_
= msg
.continueUrl
;
197 this.isConstrainedWindow_
= msg
.isConstrainedWindow
;
201 * Handler for webRequest.onCompleted. It 1) detects loading of continue URL
202 * and notifies the main script of signin completion; 2) detects if the
203 * current page could be loaded in a constrained window and signals the main
204 * script of switching to full tab if necessary.
206 onCompleted: function(details
) {
207 // Only monitors requests in the gaia frame whose parent frame ID must be
209 if (!this.isDesktopFlow_
|| details
.parentFrameId
<= 0)
213 if (this.continueUrl_
&&
214 details
.url
.lastIndexOf(this.continueUrl_
, 0) == 0) {
215 var skipForNow
= false;
216 if (details
.url
.indexOf('ntp=1') >= 0)
219 // TOOD(guohui): Show password confirmation UI.
220 var passwords
= this.onGetScrapedPasswords_();
222 'name': 'completeLogin',
223 'email': this.email_
,
224 'password': passwords
[0],
225 'sessionIndex': this.sessionIndex_
,
226 'skipForNow': skipForNow
228 this.channelMain_
.send(msg
);
229 } else if (this.isConstrainedWindow_
) {
230 // The header google-accounts-embedded is only set on gaia domain.
231 if (this.gaiaUrl_
&& details
.url
.lastIndexOf(this.gaiaUrl_
) == 0) {
232 var headers
= details
.responseHeaders
;
233 for (var i
= 0; headers
&& i
< headers
.length
; ++i
) {
234 if (headers
[i
].name
.toLowerCase() == 'google-accounts-embedded')
239 'name': 'switchToFullTab',
242 this.channelMain_
.send(msg
);
247 * Handler for webRequest.onBeforeRequest, invoked when content served over an
248 * unencrypted connection is detected. Determines whether the request should
249 * be blocked and if so, signals that an error message needs to be shown.
250 * @param {string} url The URL that was blocked.
251 * @return {!Object} Decision whether to block the request.
253 onInsecureRequest: function(url
) {
254 if (!this.blockInsecureContent_
)
256 this.channelMain_
.send({name
: 'onInsecureContentBlocked', url
: url
});
257 return {cancel
: true};
261 * Handler or webRequest.onHeadersReceived. It reads the authenticated user
262 * email from google-accounts-signin-header.
264 onHeadersReceived: function(details
) {
265 if (!this.isDesktopFlow_
||
267 details
.url
.lastIndexOf(this.gaiaUrl_
) != 0) {
268 // TODO(xiyuan, guohui): CrOS should reuse the logic below for reading the
269 // email for SAML users and cut off the /ListAccount call.
273 var headers
= details
.responseHeaders
;
274 for (var i
= 0; headers
&& i
< headers
.length
; ++i
) {
275 if (headers
[i
].name
.toLowerCase() == 'google-accounts-signin') {
276 var headerValues
= headers
[i
].value
.toLowerCase().split(',');
277 var signinDetails
= {};
278 headerValues
.forEach(function(e
) {
279 var pair
= e
.split('=');
280 signinDetails
[pair
[0].trim()] = pair
[1].trim();
283 this.email_
= signinDetails
['email'].slice(1, -1);
284 this.sessionIndex_
= signinDetails
['sessionindex'];
291 * Handler for webRequest.onBeforeSendHeaders.
292 * @return {!Object} Modified request headers.
294 onBeforeSendHeaders: function(details
) {
295 if (!this.isDesktopFlow_
&& this.gaiaUrl_
&&
296 details
.url
.indexOf(this.gaiaUrl_
) == 0) {
297 details
.requestHeaders
.push({
298 name
: 'X-Cros-Auth-Ext-Support',
302 return {requestHeaders
: details
.requestHeaders
};
306 * Handler for 'setGaiaUrl' signal sent from the main script.
308 onSetGaiaUrl_: function(msg
) {
309 this.gaiaUrl_
= msg
.gaiaUrl
;
313 * Handler for 'setBlockInsecureContent' signal sent from the main script.
315 onSetBlockInsecureContent_: function(msg
) {
316 this.blockInsecureContent_
= msg
.blockInsecureContent
;
320 * Handler for 'resetAuth' signal sent from the main script.
322 onResetAuth_: function() {
323 this.authStarted_
= false;
324 this.passwordStore_
= {};
328 * Handler for 'authStarted' signal sent from the main script.
330 onAuthStarted_: function() {
331 this.authStarted_
= true;
332 this.passwordStore_
= {};
336 * Handler for 'getScrapedPasswords' request sent from the main script.
337 * @return {Array.<string>} The array with de-duped scraped passwords.
339 onGetScrapedPasswords_: function() {
341 for (var property
in this.passwordStore_
) {
342 passwords
[this.passwordStore_
[property
]] = true;
344 return Object
.keys(passwords
);
348 * Handler for 'apiResponse' signal sent from the main script. Passes on the
349 * |msg| to the injected script.
351 onAPIResponse_: function(msg
) {
352 this.channelInjected_
.send(msg
);
355 onAPICall_: function(msg
) {
356 this.channelMain_
.send(msg
);
359 onUpdatePassword_: function(msg
) {
360 if (!this.authStarted_
)
363 this.passwordStore_
[msg
.id
] = msg
.password
;
366 onPageLoaded_: function(msg
) {
367 if (this.channelMain_
)
368 this.channelMain_
.send({name
: 'onAuthPageLoaded', url
: msg
.url
});
372 var backgroundBridgeManager
= new BackgroundBridgeManager();
373 backgroundBridgeManager
.run();