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  * Singleton getter of Authenticator.
     20  * @return {Object} The singleton instance of Authenticator.
     21  */
     22 Authenticator.getInstance = function() {
     23   if (!Authenticator.instance_) {
     24     Authenticator.instance_ = new Authenticator();
     25   }
     26   return Authenticator.instance_;
     27 };
     28 
     29 Authenticator.prototype = {
     30   email_: null,
     31   password_: null,
     32   attemptToken_: null,
     33 
     34   // Input params from extension initialization URL.
     35   inputLang_: undefined,
     36   intputEmail_: undefined,
     37 
     38   samlPageLoaded_: false,
     39   samlSupportChannel_: null,
     40 
     41   GAIA_URL: 'https://accounts.google.com/',
     42   GAIA_PAGE_PATH: 'ServiceLogin?skipvpage=true&sarp=1&rm=hide',
     43   PARENT_PAGE: 'chrome://oobe/',
     44   SERVICE_ID: 'chromeoslogin',
     45   CONTINUE_URL: Authenticator.THIS_EXTENSION_ORIGIN + '/success.html',
     46 
     47   initialize: function() {
     48     var params = getUrlSearchParams(location.search);
     49     this.parentPage_ = params.parentPage || this.PARENT_PAGE;
     50     this.gaiaUrl_ = params.gaiaUrl || this.GAIA_URL;
     51     this.gaiaPath_ = params.gaiaPath || this.GAIA_PAGE_PATH;
     52     this.inputLang_ = params.hl;
     53     this.inputEmail_ = params.email;
     54     this.service_ = params.service || this.SERVICE_ID;
     55     this.continueUrl_ = params.continueUrl || this.CONTINUE_URL;
     56     this.continueUrlWithoutParams_ = stripParams(this.continueUrl_);
     57     this.inlineMode_ = params.inlineMode == '1';
     58     this.constrained_ = params.constrained == '1';
     59     this.partitionId_ = params.partitionId || '';
     60     this.initialFrameUrl_ = params.frameUrl || this.constructInitialFrameUrl_();
     61     this.initialFrameUrlWithoutParams_ = stripParams(this.initialFrameUrl_);
     62     this.loaded_ = false;
     63 
     64     document.addEventListener('DOMContentLoaded', this.onPageLoad.bind(this));
     65     document.addEventListener('enableSAML', this.onEnableSAML_.bind(this));
     66   },
     67 
     68   isGaiaMessage_: function(msg) {
     69     // Not quite right, but good enough.
     70     return this.gaiaUrl_.indexOf(msg.origin) == 0 ||
     71            this.GAIA_URL.indexOf(msg.origin) == 0;
     72   },
     73 
     74   isInternalMessage_: function(msg) {
     75     return msg.origin == Authenticator.THIS_EXTENSION_ORIGIN;
     76   },
     77 
     78   isParentMessage_: function(msg) {
     79     return msg.origin == this.parentPage_;
     80   },
     81 
     82   constructInitialFrameUrl_: function() {
     83     var url = this.gaiaUrl_ + this.gaiaPath_;
     84 
     85     url = appendParam(url, 'service', this.service_);
     86     url = appendParam(url, 'continue', this.continueUrl_);
     87     if (this.inputLang_)
     88       url = appendParam(url, 'hl', this.inputLang_);
     89     if (this.inputEmail_)
     90       url = appendParam(url, 'Email', this.inputEmail_);
     91 
     92     return url;
     93   },
     94 
     95   /** Callback when all loads in the gaia webview is complete. */
     96   onWebviewLoadstop_: function(gaiaFrame) {
     97     // Report the current state to the parent which will then update the
     98     // browser history so that later it could respond properly to back/forward.
     99     var msg = {
    100       'method': 'reportState',
    101       'src': gaiaFrame.src
    102     };
    103     window.parent.postMessage(msg, this.parentPage_);
    104 
    105     if (gaiaFrame.src.lastIndexOf(
    106         this.continueUrlWithoutParams_, 0) == 0) {
    107       // Detect when login is finished by the load stop event of the continue
    108       // URL. Cannot reuse the login complete flow in success.html, because
    109       // webview does not support extension pages yet.
    110       gaiaFrame.hidden = true;
    111       msg = {'method': 'completeLogin'};
    112       window.parent.postMessage(msg, this.parentPage_);
    113       return;
    114     }
    115 
    116     if (gaiaFrame.src.lastIndexOf(this.gaiaUrl_, 0) == 0) {
    117       gaiaFrame.executeScript({file: 'inline_injected.js'}, function() {
    118         // Send an initial message to gaia so that it has an JavaScript
    119         // reference to the embedder.
    120         gaiaFrame.contentWindow.postMessage('', gaiaFrame.src);
    121       });
    122     }
    123 
    124     this.loaded_ || this.onLoginUILoaded();
    125   },
    126 
    127   /**
    128    * Callback when the gaia webview attempts to open a new window.
    129    */
    130   onWebviewNewWindow_: function(gaiaFrame, e) {
    131     window.open(e.targetUrl, '_blank');
    132     e.window.discard();
    133   },
    134 
    135   onWebviewRequestCompleted_: function(details) {
    136     if (details.url.lastIndexOf(this.continueUrlWithoutParams_, 0) == 0) {
    137       return;
    138     }
    139 
    140     var headers = details.responseHeaders;
    141     for (var i = 0; headers && i < headers.length; ++i) {
    142       if (headers[i].name.toLowerCase() == 'google-accounts-embedded') {
    143         return;
    144       }
    145     }
    146     var msg = {
    147       'method': 'switchToFullTab',
    148       'url': details.url
    149     };
    150     window.parent.postMessage(msg, this.parentPage_);
    151   },
    152 
    153   loadFrame_: function() {
    154     var gaiaFrame = $('gaia-frame');
    155     gaiaFrame.partition = this.partitionId_;
    156     gaiaFrame.src = this.initialFrameUrl_;
    157     if (this.inlineMode_) {
    158       gaiaFrame.addEventListener(
    159           'loadstop', this.onWebviewLoadstop_.bind(this, gaiaFrame));
    160       gaiaFrame.addEventListener(
    161           'newwindow', this.onWebviewNewWindow_.bind(this, gaiaFrame));
    162     }
    163     if (this.constrained_) {
    164       gaiaFrame.request.onCompleted.addListener(
    165           this.onWebviewRequestCompleted_.bind(this),
    166           {urls: ['<all_urls>'], types: ['main_frame']},
    167           ['responseHeaders']);
    168     }
    169   },
    170 
    171   completeLogin: function(username, password) {
    172     var msg = {
    173       'method': 'completeLogin',
    174       'email': username,
    175       'password': password
    176     };
    177     window.parent.postMessage(msg, this.parentPage_);
    178     if (this.samlSupportChannel_)
    179       this.samlSupportChannel_.send({name: 'resetAuth'});
    180   },
    181 
    182   onPageLoad: function(e) {
    183     window.addEventListener('message', this.onMessage.bind(this), false);
    184     this.loadFrame_();
    185   },
    186 
    187   /**
    188    * Invoked when 'enableSAML' event is received to initialize SAML support.
    189    */
    190   onEnableSAML_: function() {
    191     this.samlPageLoaded_ = false;
    192 
    193     this.samlSupportChannel_ = new Channel();
    194     this.samlSupportChannel_.connect('authMain');
    195     this.samlSupportChannel_.registerMessage(
    196         'onAuthPageLoaded', this.onAuthPageLoaded_.bind(this));
    197     this.samlSupportChannel_.send({
    198       name: 'setGaiaUrl',
    199       gaiaUrl: this.gaiaUrl_
    200     });
    201   },
    202 
    203   /**
    204    * Invoked when the background page sends 'onHostedPageLoaded' message.
    205    * @param {!Object} msg Details sent with the message.
    206    */
    207   onAuthPageLoaded_: function(msg) {
    208     this.samlPageLoaded_ = msg.url.indexOf(this.gaiaUrl_) != 0;
    209     window.parent.postMessage({
    210       'method': 'authPageLoaded',
    211       'isSAML': this.samlPageLoaded_
    212     }, this.parentPage_);
    213   },
    214 
    215   onLoginUILoaded: function() {
    216     var msg = {
    217       'method': 'loginUILoaded'
    218     };
    219     window.parent.postMessage(msg, this.parentPage_);
    220     if (this.inlineMode_) {
    221       $('gaia-frame').focus();
    222     }
    223     this.loaded_ = true;
    224   },
    225 
    226   onConfirmLogin_: function() {
    227     if (!this.samlPageLoaded_) {
    228       this.completeLogin(this.email_, this.password_);
    229       return;
    230     }
    231 
    232     this.samlSupportChannel_.sendWithCallback(
    233         {name: 'getScrapedPasswords'},
    234         function(passwords) {
    235           if (passwords.length == 0) {
    236             window.parent.postMessage(
    237                 {method: 'noPassword', email: this.email_},
    238                 this.parentPage_);
    239           } else {
    240             window.parent.postMessage(
    241                 {method: 'confirmPassword', email: this.email_},
    242                 this.parentPage_);
    243           }
    244         }.bind(this));
    245   },
    246 
    247   onVerifyConfirmedPassword_: function(password) {
    248     this.samlSupportChannel_.sendWithCallback(
    249         {name: 'getScrapedPasswords'},
    250         function(passwords) {
    251           for (var i = 0; i < passwords.length; ++i) {
    252             if (passwords[i] == password) {
    253               this.completeLogin(this.email_, passwords[i]);
    254               return;
    255             }
    256           }
    257           window.parent.postMessage(
    258               {method: 'confirmPassword', email: this.email_},
    259               this.parentPage_);
    260         }.bind(this));
    261   },
    262 
    263   onMessage: function(e) {
    264     var msg = e.data;
    265     if (msg.method == 'attemptLogin' && this.isGaiaMessage_(e)) {
    266       this.email_ = msg.email;
    267       this.password_ = msg.password;
    268       this.attemptToken_ = msg.attemptToken;
    269       this.samlPageLoaded_ = false;
    270       if (this.samlSupportChannel_)
    271         this.samlSupportChannel_.send({name: 'startAuth'});
    272     } else if (msg.method == 'clearOldAttempts' && this.isGaiaMessage_(e)) {
    273       this.email_ = null;
    274       this.password_ = null;
    275       this.attemptToken_ = null;
    276       this.samlPageLoaded_ = false;
    277       this.onLoginUILoaded();
    278       if (this.samlSupportChannel_)
    279         this.samlSupportChannel_.send({name: 'resetAuth'});
    280     } else if (msg.method == 'confirmLogin' && this.isInternalMessage_(e)) {
    281       if (this.attemptToken_ == msg.attemptToken)
    282         this.onConfirmLogin_();
    283       else
    284         console.error('Authenticator.onMessage: unexpected attemptToken!?');
    285     } else if (msg.method == 'verifyConfirmedPassword' &&
    286                this.isParentMessage_(e)) {
    287       this.onVerifyConfirmedPassword_(msg.password);
    288     } else if (msg.method == 'navigate' &&
    289                this.isParentMessage_(e)) {
    290        $('gaia-frame').src = msg.src;
    291     } else {
    292       console.error('Authenticator.onMessage: unknown message + origin!?');
    293     }
    294   }
    295 };
    296 
    297 Authenticator.getInstance().initialize();
    298