• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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  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
297Authenticator.getInstance().initialize();
298