Home | History | Annotate | Download | only in gaia_auth
      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.
      4 
      5 /**
      6  * @fileoverview
      7  * A background script of the auth extension that bridges the communication
      8  * between the main and injected scripts.
      9  *
     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.
     24  */
     25 
     26 /**
     27  * BackgroundBridgeManager maintains an array of BackgroundBridge, indexed by
     28  * the associated tab id.
     29  */
     30 function BackgroundBridgeManager() {
     31 }
     32 
     33 BackgroundBridgeManager.prototype = {
     34   // Maps a tab id to its associated BackgroundBridge.
     35   bridges_: {},
     36 
     37   run: function() {
     38     chrome.runtime.onConnect.addListener(this.onConnect_.bind(this));
     39 
     40     chrome.webRequest.onBeforeRequest.addListener(
     41         function(details) {
     42           if (this.bridges_[details.tabId])
     43             return this.bridges_[details.tabId].onInsecureRequest(details.url);
     44         }.bind(this),
     45         {urls: ['http://*/*', 'file://*/*', 'ftp://*/*']},
     46         ['blocking']);
     47 
     48     chrome.webRequest.onBeforeSendHeaders.addListener(
     49         function(details) {
     50           if (this.bridges_[details.tabId])
     51             return this.bridges_[details.tabId].onBeforeSendHeaders(details);
     52           else
     53             return {requestHeaders: details.requestHeaders};
     54         }.bind(this),
     55         {urls: ['*://*/*'], types: ['sub_frame']},
     56         ['blocking', 'requestHeaders']);
     57 
     58     chrome.webRequest.onHeadersReceived.addListener(
     59         function(details) {
     60           if (this.bridges_[details.tabId])
     61             this.bridges_[details.tabId].onHeadersReceived(details);
     62         }.bind(this),
     63         {urls: ['*://*/*'], types: ['sub_frame']},
     64         ['responseHeaders']);
     65 
     66     chrome.webRequest.onCompleted.addListener(
     67         function(details) {
     68           if (this.bridges_[details.tabId])
     69             this.bridges_[details.tabId].onCompleted(details);
     70         }.bind(this),
     71         {urls: ['*://*/*'], types: ['sub_frame']},
     72         ['responseHeaders']);
     73   },
     74 
     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];
     83       }.bind(this));
     84     } else if (port.name == 'injected') {
     85       this.bridges_[tabId].setupForInjected(port);
     86     } else {
     87       console.error('Unexpected connection, port.name=' + port.name);
     88     }
     89   },
     90 
     91   getTabIdFromPort_: function(port) {
     92     return port.sender.tab ? port.sender.tab.id : -1;
     93   }
     94 };
     95 
     96 /**
     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.
    101  */
    102 function BackgroundBridge(tabId) {
    103   this.tabId_ = tabId;
    104 }
    105 
    106 BackgroundBridge.prototype = {
    107   // The associated tab ID. Only used for debugging now.
    108   tabId: null,
    109 
    110   isDesktopFlow_: false,
    111 
    112   // Continue URL that is set from main auth script.
    113   continueUrl_: null,
    114 
    115   // Whether the extension is loaded in a constrained window.
    116   // Set from main auth script.
    117   isConstrainedWindow_: null,
    118 
    119   // Email of the newly authenticated user based on the gaia response header
    120   // 'google-accounts-signin'.
    121   email_: null,
    122 
    123   // Session index of the newly authenticated user based on the gaia response
    124   // header 'google-accounts-signin'.
    125   sessionIndex_: null,
    126 
    127   // Gaia URL base that is set from main auth script.
    128   gaiaUrl_: null,
    129 
    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,
    133 
    134   // Whether auth flow has started. It is used as a signal of whether the
    135   // injected script should scrape passwords.
    136   authStarted_: false,
    137 
    138   passwordStore_: {},
    139 
    140   channelMain_: null,
    141   channelInjected_: null,
    142 
    143   /**
    144    * Sets up the communication channel with the main script.
    145    */
    146   setupForAuthMain: function(port) {
    147     this.channelMain_ = new Channel();
    148     this.channelMain_.init(port);
    149 
    150     // Registers for desktop related messages.
    151     this.channelMain_.registerMessage(
    152         'initDesktopFlow', this.onInitDesktopFlow_.bind(this));
    153 
    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));
    168 
    169     this.channelMain_.send({
    170       'name': 'channelConnected'
    171     });
    172   },
    173 
    174   /**
    175    * Sets up the communication channel with the injected script.
    176    */
    177   setupForInjected: function(port) {
    178     this.channelInjected_ = new Channel();
    179     this.channelInjected_.init(port);
    180 
    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));
    187   },
    188 
    189   /**
    190    * Handler for 'initDesktopFlow' signal sent from the main script.
    191    * Only called in desktop mode.
    192    */
    193   onInitDesktopFlow_: function(msg) {
    194     this.isDesktopFlow_ = true;
    195     this.gaiaUrl_ = msg.gaiaUrl;
    196     this.continueUrl_ = msg.continueUrl;
    197     this.isConstrainedWindow_ = msg.isConstrainedWindow;
    198   },
    199 
    200   /**
    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.
    205    */
    206   onCompleted: function(details) {
    207     // Only monitors requests in the gaia frame whose parent frame ID must be
    208     // positive.
    209     if (!this.isDesktopFlow_ || details.parentFrameId <= 0)
    210       return;
    211 
    212     var msg = null;
    213     if (this.continueUrl_ &&
    214         details.url.lastIndexOf(this.continueUrl_, 0) == 0) {
    215       var skipForNow = false;
    216       if (details.url.indexOf('ntp=1') >= 0)
    217         skipForNow = true;
    218 
    219       // TOOD(guohui): Show password confirmation UI.
    220       var passwords = this.onGetScrapedPasswords_();
    221       msg = {
    222         'name': 'completeLogin',
    223         'email': this.email_,
    224         'password': passwords[0],
    225         'sessionIndex': this.sessionIndex_,
    226         'skipForNow': skipForNow
    227       };
    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')
    235             return;
    236         }
    237       }
    238       msg = {
    239         'name': 'switchToFullTab',
    240         'url': details.url
    241       };
    242       this.channelMain_.send(msg);
    243     }
    244   },
    245 
    246   /**
    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.
    252    */
    253   onInsecureRequest: function(url) {
    254     if (!this.blockInsecureContent_)
    255       return {};
    256     this.channelMain_.send({name: 'onInsecureContentBlocked', url: url});
    257     return {cancel: true};
    258   },
    259 
    260   /**
    261    * Handler or webRequest.onHeadersReceived. It reads the authenticated user
    262    * email from google-accounts-signin-header.
    263    */
    264   onHeadersReceived: function(details) {
    265     if (!this.isDesktopFlow_ ||
    266         !this.gaiaUrl_ ||
    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.
    270       return;
    271     }
    272 
    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();
    281         });
    282         // Remove "" around.
    283         this.email_ = signinDetails['email'].slice(1, -1);
    284         this.sessionIndex_ = signinDetails['sessionindex'];
    285         return;
    286       }
    287     }
    288   },
    289 
    290   /**
    291    * Handler for webRequest.onBeforeSendHeaders.
    292    * @return {!Object} Modified request headers.
    293    */
    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',
    299         value: 'SAML'
    300       });
    301     }
    302     return {requestHeaders: details.requestHeaders};
    303   },
    304 
    305   /**
    306    * Handler for 'setGaiaUrl' signal sent from the main script.
    307    */
    308   onSetGaiaUrl_: function(msg) {
    309     this.gaiaUrl_ = msg.gaiaUrl;
    310   },
    311 
    312   /**
    313    * Handler for 'setBlockInsecureContent' signal sent from the main script.
    314    */
    315   onSetBlockInsecureContent_: function(msg) {
    316     this.blockInsecureContent_ = msg.blockInsecureContent;
    317   },
    318 
    319   /**
    320    * Handler for 'resetAuth' signal sent from the main script.
    321    */
    322   onResetAuth_: function() {
    323     this.authStarted_ = false;
    324     this.passwordStore_ = {};
    325   },
    326 
    327   /**
    328    * Handler for 'authStarted' signal sent from the main script.
    329    */
    330   onAuthStarted_: function() {
    331     this.authStarted_ = true;
    332     this.passwordStore_ = {};
    333   },
    334 
    335   /**
    336    * Handler for 'getScrapedPasswords' request sent from the main script.
    337    * @return {Array.<string>} The array with de-duped scraped passwords.
    338    */
    339   onGetScrapedPasswords_: function() {
    340     var passwords = {};
    341     for (var property in this.passwordStore_) {
    342       passwords[this.passwordStore_[property]] = true;
    343     }
    344     return Object.keys(passwords);
    345   },
    346 
    347   /**
    348    * Handler for 'apiResponse' signal sent from the main script. Passes on the
    349    * |msg| to the injected script.
    350    */
    351   onAPIResponse_: function(msg) {
    352     this.channelInjected_.send(msg);
    353   },
    354 
    355   onAPICall_: function(msg) {
    356     this.channelMain_.send(msg);
    357   },
    358 
    359   onUpdatePassword_: function(msg) {
    360     if (!this.authStarted_)
    361       return;
    362 
    363     this.passwordStore_[msg.id] = msg.password;
    364   },
    365 
    366   onPageLoaded_: function(msg) {
    367     if (this.channelMain_)
    368       this.channelMain_.send({name: 'onAuthPageLoaded', url: msg.url});
    369   }
    370 };
    371 
    372 var backgroundBridgeManager = new BackgroundBridgeManager();
    373 backgroundBridgeManager.run();
    374