Home | History | Annotate | Download | only in gaia_auth
      1 // Copyright (c) 2012 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  * Authenticator class wraps the communications between Gaia and its host.
      7  */
      8 function Authenticator() {
      9 }
     10 
     11 /**
     12  * Gaia auth extension url origin.
     13  * @type {string}
     14  */
     15 Authenticator.THIS_EXTENSION_ORIGIN =
     16     'chrome-extension://mfffpogegjflfpflabcdkioaeobkgjik';
     17 
     18 /**
     19  * The lowest version of the credentials passing API supported.
     20  * @type {number}
     21  */
     22 Authenticator.MIN_API_VERSION_VERSION = 1;
     23 
     24 /**
     25  * The highest version of the credentials passing API supported.
     26  * @type {number}
     27  */
     28 Authenticator.MAX_API_VERSION_VERSION = 1;
     29 
     30 /**
     31  * The key types supported by the credentials passing API.
     32  * @type {Array} Array of strings.
     33  */
     34 Authenticator.API_KEY_TYPES = [
     35   'KEY_TYPE_PASSWORD_PLAIN',
     36 ];
     37 
     38 /**
     39  * Singleton getter of Authenticator.
     40  * @return {Object} The singleton instance of Authenticator.
     41  */
     42 Authenticator.getInstance = function() {
     43   if (!Authenticator.instance_) {
     44     Authenticator.instance_ = new Authenticator();
     45   }
     46   return Authenticator.instance_;
     47 };
     48 
     49 Authenticator.prototype = {
     50   email_: null,
     51 
     52   // Depending on the key type chosen, this will contain the plain text password
     53   // or a credential derived from it along with the information required to
     54   // repeat the derivation, such as a salt. The information will be encoded so
     55   // that it contains printable ASCII characters only. The exact encoding is TBD
     56   // when support for key types other than plain text password is added.
     57   passwordBytes_: null,
     58 
     59   attemptToken_: null,
     60 
     61   // Input params from extension initialization URL.
     62   inputLang_: undefined,
     63   intputEmail_: undefined,
     64 
     65   isSAMLFlow_: false,
     66   isSAMLEnabled_: false,
     67   supportChannel_: null,
     68 
     69   GAIA_URL: 'https://accounts.google.com/',
     70   GAIA_PAGE_PATH: 'ServiceLogin?skipvpage=true&sarp=1&rm=hide',
     71   PARENT_PAGE: 'chrome://oobe/',
     72   SERVICE_ID: 'chromeoslogin',
     73   CONTINUE_URL: Authenticator.THIS_EXTENSION_ORIGIN + '/success.html',
     74   CONSTRAINED_FLOW_SOURCE: 'chrome',
     75 
     76   initialize: function() {
     77     var params = getUrlSearchParams(location.search);
     78     this.parentPage_ = params.parentPage || this.PARENT_PAGE;
     79     this.gaiaUrl_ = params.gaiaUrl || this.GAIA_URL;
     80     this.gaiaPath_ = params.gaiaPath || this.GAIA_PAGE_PATH;
     81     this.inputLang_ = params.hl;
     82     this.inputEmail_ = params.email;
     83     this.service_ = params.service || this.SERVICE_ID;
     84     this.continueUrl_ = params.continueUrl || this.CONTINUE_URL;
     85     this.desktopMode_ = params.desktopMode == '1';
     86     this.isConstrainedWindow_ = params.constrained == '1';
     87     this.initialFrameUrl_ = params.frameUrl || this.constructInitialFrameUrl_();
     88     this.initialFrameUrlWithoutParams_ = stripParams(this.initialFrameUrl_);
     89 
     90     document.addEventListener('DOMContentLoaded', this.onPageLoad_.bind(this));
     91     if (!this.desktopMode_) {
     92       // SAML is always enabled in desktop mode, thus no need to listen for
     93       // enableSAML event.
     94       document.addEventListener('enableSAML', this.onEnableSAML_.bind(this));
     95     }
     96   },
     97 
     98   isGaiaMessage_: function(msg) {
     99     // Not quite right, but good enough.
    100     return this.gaiaUrl_.indexOf(msg.origin) == 0 ||
    101            this.GAIA_URL.indexOf(msg.origin) == 0;
    102   },
    103 
    104   isInternalMessage_: function(msg) {
    105     return msg.origin == Authenticator.THIS_EXTENSION_ORIGIN;
    106   },
    107 
    108   isParentMessage_: function(msg) {
    109     return msg.origin == this.parentPage_;
    110   },
    111 
    112   constructInitialFrameUrl_: function() {
    113     var url = this.gaiaUrl_ + this.gaiaPath_;
    114 
    115     url = appendParam(url, 'service', this.service_);
    116     url = appendParam(url, 'continue', this.continueUrl_);
    117     if (this.inputLang_)
    118       url = appendParam(url, 'hl', this.inputLang_);
    119     if (this.inputEmail_)
    120       url = appendParam(url, 'Email', this.inputEmail_);
    121     if (this.isConstrainedWindow_)
    122       url = appendParam(url, 'source', this.CONSTRAINED_FLOW_SOURCE);
    123     return url;
    124   },
    125 
    126   onPageLoad_: function() {
    127     window.addEventListener('message', this.onMessage.bind(this), false);
    128 
    129     var gaiaFrame = $('gaia-frame');
    130     gaiaFrame.src = this.initialFrameUrl_;
    131 
    132     if (this.desktopMode_) {
    133       var handler = function() {
    134         this.onLoginUILoaded_();
    135         gaiaFrame.removeEventListener('load', handler);
    136 
    137         this.initDesktopChannel_();
    138       }.bind(this);
    139       gaiaFrame.addEventListener('load', handler);
    140     }
    141   },
    142 
    143   initDesktopChannel_: function() {
    144     this.supportChannel_ = new Channel();
    145     this.supportChannel_.connect('authMain');
    146 
    147     var channelConnected = false;
    148     this.supportChannel_.registerMessage('channelConnected', function() {
    149       channelConnected = true;
    150 
    151       this.supportChannel_.send({
    152         name: 'initDesktopFlow',
    153         gaiaUrl: this.gaiaUrl_,
    154         continueUrl: stripParams(this.continueUrl_),
    155         isConstrainedWindow: this.isConstrainedWindow_
    156       });
    157       this.supportChannel_.registerMessage(
    158           'switchToFullTab', this.switchToFullTab_.bind(this));
    159       this.supportChannel_.registerMessage(
    160           'completeLogin', this.completeLogin_.bind(this));
    161 
    162       this.onEnableSAML_();
    163     }.bind(this));
    164 
    165     window.setTimeout(function() {
    166       if (!channelConnected) {
    167         // Re-initialize the channel if it is not connected properly, e.g.
    168         // connect may be called before background script started running.
    169         this.initDesktopChannel_();
    170       }
    171     }.bind(this), 200);
    172   },
    173 
    174   /**
    175    * Invoked when the login UI is initialized or reset.
    176    */
    177   onLoginUILoaded_: function() {
    178     var msg = {
    179       'method': 'loginUILoaded'
    180     };
    181     window.parent.postMessage(msg, this.parentPage_);
    182   },
    183 
    184   /**
    185    * Invoked when the background script sends a message to indicate that the
    186    * current content does not fit in a constrained window.
    187    * @param {Object=} opt_extraMsg Optional extra info to send.
    188    */
    189   switchToFullTab_: function(msg) {
    190     var parentMsg = {
    191       'method': 'switchToFullTab',
    192       'url': msg.url
    193     };
    194     window.parent.postMessage(parentMsg, this.parentPage_);
    195   },
    196 
    197   /**
    198    * Invoked when the signin flow is complete.
    199    * @param {Object=} opt_extraMsg Optional extra info to send.
    200    */
    201   completeLogin_: function(opt_extraMsg) {
    202     var msg = {
    203       'method': 'completeLogin',
    204       'email': (opt_extraMsg && opt_extraMsg.email) || this.email_,
    205       'password': (opt_extraMsg && opt_extraMsg.password) ||
    206                   this.passwordBytes_,
    207       'usingSAML': this.isSAMLFlow_,
    208       'chooseWhatToSync': this.chooseWhatToSync_ || false,
    209       'skipForNow': opt_extraMsg && opt_extraMsg.skipForNow,
    210       'sessionIndex': opt_extraMsg && opt_extraMsg.sessionIndex
    211     };
    212     window.parent.postMessage(msg, this.parentPage_);
    213     if (this.isSAMLEnabled_)
    214       this.supportChannel_.send({name: 'resetAuth'});
    215   },
    216 
    217   /**
    218    * Invoked when 'enableSAML' event is received to initialize SAML support on
    219    * Chrome OS, or when initDesktopChannel_ is called on desktop.
    220    */
    221   onEnableSAML_: function() {
    222     this.isSAMLEnabled_ = true;
    223     this.isSAMLFlow_ = false;
    224 
    225     if (!this.supportChannel_) {
    226       this.supportChannel_ = new Channel();
    227       this.supportChannel_.connect('authMain');
    228     }
    229 
    230     this.supportChannel_.registerMessage(
    231         'onAuthPageLoaded', this.onAuthPageLoaded_.bind(this));
    232     this.supportChannel_.registerMessage(
    233         'onInsecureContentBlocked', this.onInsecureContentBlocked_.bind(this));
    234     this.supportChannel_.registerMessage(
    235         'apiCall', this.onAPICall_.bind(this));
    236     this.supportChannel_.send({
    237       name: 'setGaiaUrl',
    238       gaiaUrl: this.gaiaUrl_
    239     });
    240     if (!this.desktopMode_ && this.gaiaUrl_.indexOf('https://') == 0) {
    241       // Abort the login flow when content served over an unencrypted connection
    242       // is detected on Chrome OS. This does not apply to tests that explicitly
    243       // set a non-https GAIA URL and want to perform all authentication over
    244       // http.
    245       this.supportChannel_.send({
    246         name: 'setBlockInsecureContent',
    247         blockInsecureContent: true
    248       });
    249     }
    250   },
    251 
    252   /**
    253    * Invoked when the background page sends 'onHostedPageLoaded' message.
    254    * @param {!Object} msg Details sent with the message.
    255    */
    256   onAuthPageLoaded_: function(msg) {
    257     var isSAMLPage = msg.url.indexOf(this.gaiaUrl_) != 0;
    258 
    259     if (isSAMLPage && !this.isSAMLFlow_) {
    260       // GAIA redirected to a SAML login page. The credentials provided to this
    261       // page will determine what user gets logged in. The credentials obtained
    262       // from the GAIA login form are no longer relevant and can be discarded.
    263       this.isSAMLFlow_ = true;
    264       this.email_ = null;
    265       this.passwordBytes_ = null;
    266     }
    267 
    268     window.parent.postMessage({
    269       'method': 'authPageLoaded',
    270       'isSAML': this.isSAMLFlow_,
    271       'domain': extractDomain(msg.url)
    272     }, this.parentPage_);
    273   },
    274 
    275   /**
    276    * Invoked when the background page sends an 'onInsecureContentBlocked'
    277    * message.
    278    * @param {!Object} msg Details sent with the message.
    279    */
    280   onInsecureContentBlocked_: function(msg) {
    281     window.parent.postMessage({
    282       'method': 'insecureContentBlocked',
    283       'url': stripParams(msg.url)
    284     }, this.parentPage_);
    285   },
    286 
    287   /**
    288    * Invoked when one of the credential passing API methods is called by a SAML
    289    * provider.
    290    * @param {!Object} msg Details of the API call.
    291    */
    292   onAPICall_: function(msg) {
    293     var call = msg.call;
    294     if (call.method == 'initialize') {
    295       if (!Number.isInteger(call.requestedVersion) ||
    296           call.requestedVersion < Authenticator.MIN_API_VERSION_VERSION) {
    297         this.sendInitializationFailure_();
    298         return;
    299       }
    300 
    301       this.apiVersion_ = Math.min(call.requestedVersion,
    302                                   Authenticator.MAX_API_VERSION_VERSION);
    303       this.initialized_ = true;
    304       this.sendInitializationSuccess_();
    305       return;
    306     }
    307 
    308     if (call.method == 'add') {
    309       if (Authenticator.API_KEY_TYPES.indexOf(call.keyType) == -1) {
    310         console.error('Authenticator.onAPICall_: unsupported key type');
    311         return;
    312       }
    313       this.apiToken_ = call.token;
    314       this.email_ = call.user;
    315       this.passwordBytes_ = call.passwordBytes;
    316     } else if (call.method == 'confirm') {
    317       if (call.token != this.apiToken_)
    318         console.error('Authenticator.onAPICall_: token mismatch');
    319     } else {
    320       console.error('Authenticator.onAPICall_: unknown message');
    321     }
    322   },
    323 
    324   sendInitializationSuccess_: function() {
    325     this.supportChannel_.send({name: 'apiResponse', response: {
    326       result: 'initialized',
    327       version: this.apiVersion_,
    328       keyTypes: Authenticator.API_KEY_TYPES
    329     }});
    330   },
    331 
    332   sendInitializationFailure_: function() {
    333     this.supportChannel_.send({
    334       name: 'apiResponse',
    335       response: {result: 'initialization_failed'}
    336     });
    337   },
    338 
    339   onConfirmLogin_: function() {
    340     if (!this.isSAMLFlow_) {
    341       this.completeLogin_();
    342       return;
    343     }
    344 
    345     var apiUsed = !!this.passwordBytes_;
    346 
    347     // Retrieve the e-mail address of the user who just authenticated from GAIA.
    348     window.parent.postMessage({method: 'retrieveAuthenticatedUserEmail',
    349                                attemptToken: this.attemptToken_,
    350                                apiUsed: apiUsed},
    351                               this.parentPage_);
    352 
    353     if (!apiUsed) {
    354       this.supportChannel_.sendWithCallback(
    355           {name: 'getScrapedPasswords'},
    356           function(passwords) {
    357             if (passwords.length == 0) {
    358               window.parent.postMessage(
    359                   {method: 'noPassword', email: this.email_},
    360                   this.parentPage_);
    361             } else {
    362               window.parent.postMessage({method: 'confirmPassword',
    363                                          email: this.email_,
    364                                          passwordCount: passwords.length},
    365                                         this.parentPage_);
    366             }
    367           }.bind(this));
    368     }
    369   },
    370 
    371   maybeCompleteSAMLLogin_: function() {
    372     // SAML login is complete when the user's e-mail address has been retrieved
    373     // from GAIA and the user has successfully confirmed the password.
    374     if (this.email_ !== null && this.passwordBytes_ !== null)
    375       this.completeLogin_();
    376   },
    377 
    378   onVerifyConfirmedPassword_: function(password) {
    379     this.supportChannel_.sendWithCallback(
    380         {name: 'getScrapedPasswords'},
    381         function(passwords) {
    382           for (var i = 0; i < passwords.length; ++i) {
    383             if (passwords[i] == password) {
    384               this.passwordBytes_ = passwords[i];
    385               this.maybeCompleteSAMLLogin_();
    386               return;
    387             }
    388           }
    389           window.parent.postMessage(
    390               {method: 'confirmPassword', email: this.email_},
    391               this.parentPage_);
    392         }.bind(this));
    393   },
    394 
    395   onMessage: function(e) {
    396     var msg = e.data;
    397     if (msg.method == 'attemptLogin' && this.isGaiaMessage_(e)) {
    398       this.email_ = msg.email;
    399       this.passwordBytes_ = msg.password;
    400       this.attemptToken_ = msg.attemptToken;
    401       this.chooseWhatToSync_ = msg.chooseWhatToSync;
    402       this.isSAMLFlow_ = false;
    403       if (this.isSAMLEnabled_)
    404         this.supportChannel_.send({name: 'startAuth'});
    405     } else if (msg.method == 'clearOldAttempts' && this.isGaiaMessage_(e)) {
    406       this.email_ = null;
    407       this.passwordBytes_ = null;
    408       this.attemptToken_ = null;
    409       this.isSAMLFlow_ = false;
    410       this.onLoginUILoaded_();
    411       if (this.isSAMLEnabled_)
    412         this.supportChannel_.send({name: 'resetAuth'});
    413     } else if (msg.method == 'setAuthenticatedUserEmail' &&
    414                this.isParentMessage_(e)) {
    415       if (this.attemptToken_ == msg.attemptToken) {
    416         this.email_ = msg.email;
    417         this.maybeCompleteSAMLLogin_();
    418       }
    419     } else if (msg.method == 'confirmLogin' && this.isInternalMessage_(e)) {
    420       if (this.attemptToken_ == msg.attemptToken)
    421         this.onConfirmLogin_();
    422       else
    423         console.error('Authenticator.onMessage: unexpected attemptToken!?');
    424     } else if (msg.method == 'verifyConfirmedPassword' &&
    425                this.isParentMessage_(e)) {
    426       this.onVerifyConfirmedPassword_(msg.password);
    427     } else if (msg.method == 'redirectToSignin' &&
    428                this.isParentMessage_(e)) {
    429       $('gaia-frame').src = this.constructInitialFrameUrl_();
    430     } else {
    431        console.error('Authenticator.onMessage: unknown message + origin!?');
    432     }
    433   }
    434 };
    435 
    436 Authenticator.getInstance().initialize();
    437