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 backgroudn 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 * BackgroundBridge allows the main script and the injected script to
28 * collaborate. It forwards credentials API calls to the main script and
29 * maintains a list of scraped passwords.
31 function BackgroundBridge() {
34 BackgroundBridge.prototype = {
35 // Continue URL that is set from main auth script.
38 // Whether the extension is loaded in a constrained window.
39 // Set from main auth script.
40 isConstrainedWindow_: null,
42 // Email of the newly authenticated user based on the gaia response header
43 // 'google-accounts-signin'.
46 // Session index of the newly authenticated user based on the gaia response
47 // header 'google-accounts-signin'.
50 // Gaia URL base that is set from main auth script.
53 // Whether auth flow has started. It is used as a signal of whether the
54 // injected script should scrape passwords.
63 chrome.runtime.onConnect.addListener(this.onConnect_.bind(this));
65 // Workarounds for loading SAML page in an iframe.
66 chrome.webRequest.onHeadersReceived.addListener(
68 if (!this.authStarted_)
71 var headers = details.responseHeaders;
72 for (var i = 0; headers && i < headers.length; ++i) {
73 if (headers[i].name.toLowerCase() == 'x-frame-options') {
78 return {responseHeaders: headers};
80 {urls: ['<all_urls>'], types: ['sub_frame']},
81 ['blocking', 'responseHeaders']);
84 onConnect_: function(port) {
85 if (port.name == 'authMain')
86 this.setupForAuthMain_(port);
87 else if (port.name == 'injected')
88 this.setupForInjected_(port);
90 console.error('Unexpected connection, port.name=' + port.name);
94 * Sets up the communication channel with the main script.
96 setupForAuthMain_: function(port) {
97 var currentChannel = new Channel();
98 currentChannel.init(port);
100 // Registers for desktop related messages.
101 currentChannel.registerMessage(
102 'initDesktopFlow', this.onInitDesktopFlow_.bind(this));
104 // Registers for SAML related messages.
105 currentChannel.registerMessage(
106 'setGaiaUrl', this.onSetGaiaUrl_.bind(this));
107 currentChannel.registerMessage(
108 'resetAuth', this.onResetAuth_.bind(this));
109 currentChannel.registerMessage(
110 'startAuth', this.onAuthStarted_.bind(this));
111 currentChannel.registerMessage(
112 'getScrapedPasswords',
113 this.onGetScrapedPasswords_.bind(this));
115 currentChannel.send({
116 'name': 'channelConnected'
118 this.channelMain_[this.getTabIdFromPort_(port)] = currentChannel;
122 * Sets up the communication channel with the injected script.
124 setupForInjected_: function(port) {
125 var currentChannel = new Channel();
126 currentChannel.init(port);
128 var tabId = this.getTabIdFromPort_(port);
129 currentChannel.registerMessage(
130 'apiCall', this.onAPICall_.bind(this, tabId));
131 currentChannel.registerMessage(
132 'updatePassword', this.onUpdatePassword_.bind(this));
133 currentChannel.registerMessage(
134 'pageLoaded', this.onPageLoaded_.bind(this, tabId));
136 this.channelInjected_[this.getTabIdFromPort_(port)] = currentChannel;
139 getTabIdFromPort_: function(port) {
140 return port.sender.tab ? port.sender.tab.id : -1;
144 * Handler for 'initDesktopFlow' signal sent from the main script.
145 * Only called in desktop mode.
147 onInitDesktopFlow_: function(msg) {
148 this.gaiaUrl_ = msg.gaiaUrl;
149 this.continueUrl_ = msg.continueUrl;
150 this.isConstrainedWindow_ = msg.isConstrainedWindow;
153 var filter = {urls: urls, types: ['sub_frame']};
154 var optExtraInfoSpec = [];
155 if (msg.isConstrainedWindow) {
156 urls.push('<all_urls>');
157 optExtraInfoSpec.push('responseHeaders');
159 urls.push(this.continueUrl_ + '*');
162 chrome.webRequest.onCompleted.addListener(
163 this.onRequestCompletedInDesktopMode_.bind(this),
164 filter, optExtraInfoSpec);
165 chrome.webRequest.onHeadersReceived.addListener(
166 this.onHeadersReceivedInDesktopMode_.bind(this),
167 {urls: [this.gaiaUrl_ + '*'], types: ['sub_frame']},
168 ['responseHeaders']);
172 * Event listener for webRequest.onCompleted in desktop mode.
174 onRequestCompletedInDesktopMode_: function(details) {
176 if (details.url.lastIndexOf(this.continueUrl_, 0) == 0) {
177 var skipForNow = false;
178 if (details.url.indexOf('ntp=1') >= 0) {
182 'name': 'completeLogin',
183 'email': this.email_,
184 'sessionIndex': this.sessionIndex_,
185 'skipForNow': skipForNow
187 } else if (this.isConstrainedWindow_) {
188 var headers = details.responseHeaders;
189 for (var i = 0; headers && i < headers.length; ++i) {
190 if (headers[i].name.toLowerCase() == 'google-accounts-embedded') {
195 'name': 'switchToFullTab',
201 this.channelMain_[details.tabId].send(msg);
205 * Event listener for webRequest.onHeadersReceived in desktop mode.
207 onHeadersReceivedInDesktopMode_: function(details) {
208 var headers = details.responseHeaders;
209 for (var i = 0; headers && i < headers.length; ++i) {
210 if (headers[i].name.toLowerCase() == 'google-accounts-signin') {
211 var headerValues = headers[i].value.toLowerCase().split(',');
212 var signinDetails = {};
213 headerValues.forEach(function(e) {
214 var pair = e.split('=');
215 signinDetails[pair[0].trim()] = pair[1].trim();
217 this.email_ = signinDetails['email'].slice(1, -1); // Remove "" around.
218 this.sessionIndex_ = signinDetails['sessionindex'];
225 * Handler for 'setGaiaUrl' signal sent from the main script.
227 onSetGaiaUrl_: function(msg) {
228 this.gaiaUrl_ = msg.gaiaUrl;
230 // Set request header to let Gaia know that saml support is on.
231 chrome.webRequest.onBeforeSendHeaders.addListener(
233 details.requestHeaders.push({
234 name: 'X-Cros-Auth-Ext-Support',
237 return {requestHeaders: details.requestHeaders};
239 {urls: [this.gaiaUrl_ + '*'], types: ['sub_frame']},
240 ['blocking', 'requestHeaders']);
244 * Handler for 'resetAuth' signal sent from the main script.
246 onResetAuth_: function() {
247 this.authStarted_ = false;
248 this.passwordStore_ = {};
252 * Handler for 'authStarted' signal sent from the main script.
254 onAuthStarted_: function() {
255 this.authStarted_ = true;
256 this.passwordStore_ = {};
260 * Handler for 'getScrapedPasswords' request sent from the main script.
261 * @return {Array.<string>} The array with de-duped scraped passwords.
263 onGetScrapedPasswords_: function() {
265 for (var property in this.passwordStore_) {
266 passwords[this.passwordStore_[property]] = true;
268 return Object.keys(passwords);
271 onAPICall_: function(tabId, msg) {
272 if (tabId in this.channelMain_) {
273 this.channelMain_[tabId].send(msg);
277 onUpdatePassword_: function(msg) {
278 if (!this.authStarted_)
281 this.passwordStore_[msg.id] = msg.password;
284 onPageLoaded_: function(tabId, msg) {
285 if (tabId in this.channelMain_) {
286 this.channelMain_[tabId].send({name: 'onAuthPageLoaded', url: msg.url});
291 var backgroundBridge = new BackgroundBridge();
292 backgroundBridge.run();