main.js revision 5d1f7b1de12d16ceb2c938c56701a3e8bfa558f7
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 */
8function Authenticator() {
9}
10
11/**
12 * Gaia auth extension url origin.
13 * @type {string}
14 */
15Authenticator.THIS_EXTENSION_ORIGIN =
16    'chrome-extension://mfffpogegjflfpflabcdkioaeobkgjik';
17
18/**
19 * Singleton getter of Authenticator.
20 * @return {Object} The singleton instance of Authenticator.
21 */
22Authenticator.getInstance = function() {
23  if (!Authenticator.instance_) {
24    Authenticator.instance_ = new Authenticator();
25  }
26  return Authenticator.instance_;
27};
28
29Authenticator.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  isSAMLFlow_: false,
39  isSAMLEnabled_: false,
40  supportChannel_: null,
41
42  GAIA_URL: 'https://accounts.google.com/',
43  GAIA_PAGE_PATH: 'ServiceLogin?skipvpage=true&sarp=1&rm=hide',
44  PARENT_PAGE: 'chrome://oobe/',
45  SERVICE_ID: 'chromeoslogin',
46  CONTINUE_URL: Authenticator.THIS_EXTENSION_ORIGIN + '/success.html',
47  CONSTRAINED_FLOW_SOURCE: 'chrome',
48
49  initialize: function() {
50    var params = getUrlSearchParams(location.search);
51    this.parentPage_ = params.parentPage || this.PARENT_PAGE;
52    this.gaiaUrl_ = params.gaiaUrl || this.GAIA_URL;
53    this.gaiaPath_ = params.gaiaPath || this.GAIA_PAGE_PATH;
54    this.inputLang_ = params.hl;
55    this.inputEmail_ = params.email;
56    this.service_ = params.service || this.SERVICE_ID;
57    this.continueUrl_ = params.continueUrl || this.CONTINUE_URL;
58    this.desktopMode_ = params.desktopMode == '1';
59    this.isConstrainedWindow_ = params.constrained == '1';
60    this.initialFrameUrl_ = params.frameUrl || this.constructInitialFrameUrl_();
61    this.initialFrameUrlWithoutParams_ = stripParams(this.initialFrameUrl_);
62
63    if (this.desktopMode_) {
64      this.supportChannel_ = new Channel();
65      this.supportChannel_.connect('authMain');
66
67      this.supportChannel_.send({
68        name: 'initDesktopFlow',
69        gaiaUrl: this.gaiaUrl_,
70        continueUrl: stripParams(this.continueUrl_),
71        isConstrainedWindow: this.isConstrainedWindow_
72      });
73
74      this.supportChannel_.registerMessage(
75        'switchToFullTab', this.switchToFullTab_.bind(this));
76      this.supportChannel_.registerMessage(
77        'completeLogin', this.completeLogin_.bind(this));
78    }
79
80    document.addEventListener('DOMContentLoaded', this.onPageLoad_.bind(this));
81    document.addEventListener('enableSAML', this.onEnableSAML_.bind(this));
82  },
83
84  isGaiaMessage_: function(msg) {
85    // Not quite right, but good enough.
86    return this.gaiaUrl_.indexOf(msg.origin) == 0 ||
87           this.GAIA_URL.indexOf(msg.origin) == 0;
88  },
89
90  isInternalMessage_: function(msg) {
91    return msg.origin == Authenticator.THIS_EXTENSION_ORIGIN;
92  },
93
94  isParentMessage_: function(msg) {
95    return msg.origin == this.parentPage_;
96  },
97
98  constructInitialFrameUrl_: function() {
99    var url = this.gaiaUrl_ + this.gaiaPath_;
100
101    url = appendParam(url, 'service', this.service_);
102    url = appendParam(url, 'continue', this.continueUrl_);
103    if (this.inputLang_)
104      url = appendParam(url, 'hl', this.inputLang_);
105    if (this.inputEmail_)
106      url = appendParam(url, 'Email', this.inputEmail_);
107    if (this.isConstrainedWindow_)
108      url = appendParam(url, 'source', this.CONSTRAINED_FLOW_SOURCE);
109    return url;
110  },
111
112  onPageLoad_: function() {
113    window.addEventListener('message', this.onMessage.bind(this), false);
114    this.loadFrame_();
115  },
116
117  loadFrame_: function() {
118    var gaiaFrame = $('gaia-frame');
119    gaiaFrame.src = this.initialFrameUrl_;
120    if (this.desktopMode_) {
121      var handler = function() {
122        this.onLoginUILoaded_();
123        gaiaFrame.removeEventListener('load', handler);
124      }.bind(this);
125      gaiaFrame.addEventListener('load', handler);
126    }
127  },
128
129  /**
130   * Invoked when the login UI is initialized or reset.
131   */
132  onLoginUILoaded_: function() {
133    var msg = {
134      'method': 'loginUILoaded'
135    };
136    window.parent.postMessage(msg, this.parentPage_);
137  },
138
139  /**
140   * Invoked when the background script sends a message to indicate that the
141   * current content does not fit in a constrained window.
142   * @param {Object=} opt_extraMsg Optional extra info to send.
143   */
144  switchToFullTab_: function(msg) {
145    var parentMsg = {
146      'method': 'switchToFullTab',
147      'url': msg.url
148    };
149    window.parent.postMessage(parentMsg, this.parentPage_);
150  },
151
152  /**
153   * Invoked when the signin flow is complete.
154   * @param {Object=} opt_extraMsg Optional extra info to send.
155   */
156  completeLogin_: function(opt_extraMsg) {
157    var msg = {
158      'method': 'completeLogin',
159      'email': (opt_extraMsg && opt_extraMsg.email) || this.email_,
160      'password': this.password_,
161      'usingSAML': this.isSAMLFlow_,
162      'chooseWhatToSync': this.chooseWhatToSync_ || false,
163      'skipForNow': opt_extraMsg && opt_extraMsg.skipForNow,
164      'sessionIndex': opt_extraMsg && opt_extraMsg.sessionIndex
165    };
166    window.parent.postMessage(msg, this.parentPage_);
167    if (this.isSAMLEnabled_)
168      this.supportChannel_.send({name: 'resetAuth'});
169  },
170
171  /**
172   * Invoked when 'enableSAML' event is received to initialize SAML support.
173   */
174  onEnableSAML_: function() {
175    this.isSAMLEnabled_ = true;
176    this.isSAMLFlow_ = false;
177
178    if (!this.supportChannel_) {
179      this.supportChannel_ = new Channel();
180      this.supportChannel_.connect('authMain');
181    }
182
183    this.supportChannel_.registerMessage(
184        'onAuthPageLoaded', this.onAuthPageLoaded_.bind(this));
185    this.supportChannel_.registerMessage(
186        'apiCall', this.onAPICall_.bind(this));
187    this.supportChannel_.send({
188      name: 'setGaiaUrl',
189      gaiaUrl: this.gaiaUrl_
190    });
191  },
192
193  /**
194   * Invoked when the background page sends 'onHostedPageLoaded' message.
195   * @param {!Object} msg Details sent with the message.
196   */
197  onAuthPageLoaded_: function(msg) {
198    var isSAMLPage = msg.url.indexOf(this.gaiaUrl_) != 0;
199
200    if (isSAMLPage && !this.isSAMLFlow_) {
201      // GAIA redirected to a SAML login page. The credentials provided to this
202      // page will determine what user gets logged in. The credentials obtained
203      // from the GAIA login from are no longer relevant and can be discarded.
204      this.isSAMLFlow_ = true;
205      this.email_ = null;
206      this.password_ = null;
207    }
208
209    window.parent.postMessage({
210      'method': 'authPageLoaded',
211      'isSAML': this.isSAMLFlow_,
212      'domain': extractDomain(msg.url)
213    }, this.parentPage_);
214  },
215
216  /**
217   * Invoked when one of the credential passing API methods is called by a SAML
218   * provider.
219   * @param {!Object} msg Details of the API call.
220   */
221  onAPICall_: function(msg) {
222    var call = msg.call;
223    if (call.method == 'add') {
224      this.apiToken_ = call.token;
225      this.email_ = call.user;
226      this.password_ = call.password;
227    } else if (call.method == 'confirm') {
228      if (call.token != this.apiToken_)
229        console.error('Authenticator.onAPICall_: token mismatch');
230    } else {
231      console.error('Authenticator.onAPICall_: unknown message');
232    }
233  },
234
235  onConfirmLogin_: function() {
236    if (!this.isSAMLFlow_) {
237      this.completeLogin_();
238      return;
239    }
240
241    var apiUsed = !!this.password_;
242
243    // Retrieve the e-mail address of the user who just authenticated from GAIA.
244    window.parent.postMessage({method: 'retrieveAuthenticatedUserEmail',
245                               attemptToken: this.attemptToken_,
246                               apiUsed: apiUsed},
247                              this.parentPage_);
248
249    if (!apiUsed) {
250      this.supportChannel_.sendWithCallback(
251          {name: 'getScrapedPasswords'},
252          function(passwords) {
253            if (passwords.length == 0) {
254              window.parent.postMessage(
255                  {method: 'noPassword', email: this.email_},
256                  this.parentPage_);
257            } else {
258              window.parent.postMessage({method: 'confirmPassword',
259                                         email: this.email_,
260                                         passwordCount: passwords.length},
261                                        this.parentPage_);
262            }
263          }.bind(this));
264    }
265  },
266
267  maybeCompleteSAMLLogin_: function() {
268    // SAML login is complete when the user's e-mail address has been retrieved
269    // from GAIA and the user has successfully confirmed the password.
270    if (this.email_ !== null && this.password_ !== null)
271      this.completeLogin_();
272  },
273
274  onVerifyConfirmedPassword_: function(password) {
275    this.supportChannel_.sendWithCallback(
276        {name: 'getScrapedPasswords'},
277        function(passwords) {
278          for (var i = 0; i < passwords.length; ++i) {
279            if (passwords[i] == password) {
280              this.password_ = passwords[i];
281              this.maybeCompleteSAMLLogin_();
282              return;
283            }
284          }
285          window.parent.postMessage(
286              {method: 'confirmPassword', email: this.email_},
287              this.parentPage_);
288        }.bind(this));
289  },
290
291  onMessage: function(e) {
292    var msg = e.data;
293    if (msg.method == 'attemptLogin' && this.isGaiaMessage_(e)) {
294      this.email_ = msg.email;
295      this.password_ = msg.password;
296      this.attemptToken_ = msg.attemptToken;
297      this.chooseWhatToSync_ = msg.chooseWhatToSync;
298      this.isSAMLFlow_ = false;
299      if (this.isSAMLEnabled_)
300        this.supportChannel_.send({name: 'startAuth'});
301    } else if (msg.method == 'clearOldAttempts' && this.isGaiaMessage_(e)) {
302      this.email_ = null;
303      this.password_ = null;
304      this.attemptToken_ = null;
305      this.isSAMLFlow_ = false;
306      this.onLoginUILoaded_();
307      if (this.isSAMLEnabled_)
308        this.supportChannel_.send({name: 'resetAuth'});
309    } else if (msg.method == 'setAuthenticatedUserEmail' &&
310               this.isParentMessage_(e)) {
311      if (this.attemptToken_ == msg.attemptToken) {
312        this.email_ = msg.email;
313        this.maybeCompleteSAMLLogin_();
314      }
315    } else if (msg.method == 'confirmLogin' && this.isInternalMessage_(e)) {
316      if (this.attemptToken_ == msg.attemptToken)
317        this.onConfirmLogin_();
318      else
319        console.error('Authenticator.onMessage: unexpected attemptToken!?');
320    } else if (msg.method == 'verifyConfirmedPassword' &&
321               this.isParentMessage_(e)) {
322      this.onVerifyConfirmedPassword_(msg.password);
323    } else if (msg.method == 'navigate' &&
324               this.isParentMessage_(e)) {
325      $('gaia-frame').src = msg.src;
326    } else if (msg.method == 'redirectToSignin' &&
327               this.isParentMessage_(e)) {
328      $('gaia-frame').src = this.constructInitialFrameUrl_();
329    } else {
330       console.error('Authenticator.onMessage: unknown message + origin!?');
331    }
332  }
333};
334
335Authenticator.getInstance().initialize();
336