• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 An UI component to host gaia auth extension in an iframe.
7 * After the component binds with an iframe, call its {@code load} to start the
8 * authentication flow. There are two events would be raised after this point:
9 * a 'ready' event when the authentication UI is ready to use and a 'completed'
10 * event when the authentication is completed successfully. If caller is
11 * interested in the user credentials, he may supply a success callback with
12 * {@code load} call. The callback will be invoked when the authentication is
13 * completed successfully and with the available credential data.
14 */
15
16cr.define('cr.login', function() {
17  'use strict';
18
19  /**
20   * Base URL of gaia auth extension.
21   * @const
22   */
23  var AUTH_URL_BASE = 'chrome-extension://mfffpogegjflfpflabcdkioaeobkgjik';
24
25  /**
26   * Auth URL to use for online flow.
27   * @const
28   */
29  var AUTH_URL = AUTH_URL_BASE + '/main.html';
30
31  /**
32   * Auth URL to use for offline flow.
33   * @const
34   */
35  var OFFLINE_AUTH_URL = AUTH_URL_BASE + '/offline.html';
36
37  /**
38   * Origin of the gaia sign in page.
39   * @const
40   */
41  var GAIA_ORIGIN = 'https://accounts.google.com';
42
43  /**
44   * Supported params of auth extension. For a complete list, check out the
45   * auth extension's main.js.
46   * @type {!Array.<string>}
47   * @const
48   */
49  var SUPPORTED_PARAMS = [
50    'gaiaUrl',       // Gaia url to use;
51    'gaiaPath',      // Gaia path to use without a leading slash;
52    'hl',            // Language code for the user interface;
53    'email',         // Pre-fill the email field in Gaia UI;
54    'service',       // Name of Gaia service;
55    'continueUrl',   // Continue url to use;
56    'frameUrl',      // Initial frame URL to use. If empty defaults to gaiaUrl.
57    'constrained'    // Whether the extension is loaded in a constrained window;
58  ];
59
60  /**
61   * Supported localized strings. For a complete list, check out the auth
62   * extension's offline.js
63   * @type {!Array.<string>}
64   * @const
65   */
66  var LOCALIZED_STRING_PARAMS = [
67      'stringSignIn',
68      'stringEmail',
69      'stringPassword',
70      'stringEmptyEmail',
71      'stringEmptyPassword',
72      'stringError'
73  ];
74
75  /**
76   * Enum for the authorization mode, must match AuthMode defined in
77   * chrome/browser/ui/webui/inline_login_ui.cc.
78   * @enum {number}
79   */
80  var AuthMode = {
81    DEFAULT: 0,
82    OFFLINE: 1,
83    DESKTOP: 2
84  };
85
86  /**
87   * Enum for the auth flow.
88   * @enum {number}
89   */
90  var AuthFlow = {
91    GAIA: 0,
92    SAML: 1
93  };
94
95  /**
96   * Creates a new gaia auth extension host.
97   * @param {HTMLIFrameElement|string} container The iframe element or its id
98   *     to host the auth extension.
99   * @constructor
100   * @extends {cr.EventTarget}
101   */
102  function GaiaAuthHost(container) {
103    this.frame_ = typeof container == 'string' ? $(container) : container;
104    assert(this.frame_);
105    window.addEventListener('message',
106                            this.onMessage_.bind(this), false);
107  }
108
109  GaiaAuthHost.prototype = {
110    __proto__: cr.EventTarget.prototype,
111
112    /**
113     * An url to use with {@code reload}.
114     * @type {?string}
115     * @private
116     */
117    reloadUrl_: null,
118
119    /**
120     * The domain name of the current auth page.
121     * @type {string}
122     */
123    authDomain: '',
124
125    /**
126     * Invoked when authentication is completed successfully with credential
127     * data. A credential data object looks like this:
128     * <pre>
129     * {@code
130     * {
131     *   email: 'xx@gmail.com',
132     *   password: 'xxxx',  // May not present
133     *   authCode: 'x/xx',  // May not present
134     *   authMode: 'x',     // Authorization mode, default/offline/desktop.
135     * }
136     * }
137     * </pre>
138     * @type {function(Object)}
139     * @private
140     */
141    successCallback_: null,
142
143    /**
144     * Invoked when GAIA indicates login success and SAML was used. At this
145     * point, GAIA cookies are present but the identity of the authenticated
146     * user is not known. The embedder of GaiaAuthHost should extract the GAIA
147     * cookies from the cookie jar, query GAIA for the authenticated user's
148     * e-mail address and invoke GaiaAuthHost.setAuthenticatedUserEmail with the
149     * result. The argument is an opaque token that should be passed back to
150     * GaiaAuthHost.setAuthenticatedUserEmail.
151     * @type {function(number)}
152     */
153    retrieveAuthenticatedUserEmailCallback_: null,
154
155    /**
156     * Invoked when the auth flow needs a user to confirm his/her passwords.
157     * This could happen when there are more than one passwords scraped during
158     * SAML flow. The embedder of GaiaAuthHost should show an UI to collect a
159     * password from user then call GaiaAuthHost.verifyConfirmedPassword to
160     * verify. If the password is good, the auth flow continues with success
161     * path. Otherwise, confirmPasswordCallback_ is invoked again.
162     * @type {function()}
163     */
164    confirmPasswordCallback_: null,
165
166    /**
167     * Similar to confirmPasswordCallback_ but is used when there is no
168     * password scraped after a success authentication. The authenticated user
169     * account is passed to the callback. The embedder should take over the
170     * flow and decide what to do next.
171     * @type {function(string)}
172     */
173    noPasswordCallback_: null,
174
175    /**
176     * Invoked when the authentication flow had to be aborted because content
177     * served over an unencrypted connection was detected.
178    insecureContentBlockedCallback_: null,
179
180    /**
181     * The iframe container.
182     * @type {HTMLIFrameElement}
183     */
184    get frame() {
185      return this.frame_;
186    },
187
188    /**
189     * Sets retrieveAuthenticatedUserEmailCallback_.
190     * @type {function()}
191     */
192    set retrieveAuthenticatedUserEmailCallback(callback) {
193      this.retrieveAuthenticatedUserEmailCallback_ = callback;
194    },
195
196    /**
197     * Sets confirmPasswordCallback_.
198     * @type {function()}
199     */
200    set confirmPasswordCallback(callback) {
201      this.confirmPasswordCallback_ = callback;
202    },
203
204    /**
205     * Sets noPasswordCallback_.
206     * @type {function()}
207     */
208    set noPasswordCallback(callback) {
209      this.noPasswordCallback_ = callback;
210    },
211
212    /**
213     * Sets insecureContentBlockedCallback_.
214     * @type {function(string)}
215     */
216    set insecureContentBlockedCallback(callback) {
217      this.insecureContentBlockedCallback_ = callback;
218    },
219
220    /**
221     * Loads the auth extension.
222     * @param {AuthMode} authMode Authorization mode.
223     * @param {Object} data Parameters for the auth extension. See the auth
224     *     extension's main.js for all supported params and their defaults.
225     * @param {function(Object)} successCallback A function to be called when
226     *     the authentication is completed successfully. The callback is
227     *     invoked with a credential object.
228     */
229    load: function(authMode, data, successCallback) {
230      var params = [];
231
232      var populateParams = function(nameList, values) {
233        if (!values)
234          return;
235
236        for (var i in nameList) {
237          var name = nameList[i];
238          if (values[name])
239            params.push(name + '=' + encodeURIComponent(values[name]));
240        }
241      };
242
243      populateParams(SUPPORTED_PARAMS, data);
244      populateParams(LOCALIZED_STRING_PARAMS, data.localizedStrings);
245      params.push('parentPage=' + encodeURIComponent(window.location.origin));
246
247      var url;
248      switch (authMode) {
249        case AuthMode.OFFLINE:
250          url = OFFLINE_AUTH_URL;
251          break;
252        case AuthMode.DESKTOP:
253          url = AUTH_URL;
254          params.push('desktopMode=1');
255          break;
256        default:
257          url = AUTH_URL;
258      }
259      url += '?' + params.join('&');
260
261      this.frame_.src = url;
262      this.reloadUrl_ = url;
263      this.successCallback_ = successCallback;
264      this.authFlow = AuthFlow.GAIA;
265    },
266
267    /**
268     * Reloads the auth extension.
269     */
270    reload: function() {
271      this.frame_.src = this.reloadUrl_;
272      this.authFlow = AuthFlow.GAIA;
273    },
274
275    /**
276     * Verifies the supplied password by sending it to the auth extension,
277     * which will then check if it matches the scraped passwords.
278     * @param {string} password The confirmed password that needs verification.
279     */
280    verifyConfirmedPassword: function(password) {
281      var msg = {
282        method: 'verifyConfirmedPassword',
283        password: password
284      };
285      this.frame_.contentWindow.postMessage(msg, AUTH_URL_BASE);
286    },
287
288    /**
289     * Sends the authenticated user's e-mail address to the auth extension.
290     * @param {number} attemptToken The opaque token provided to the
291     *     retrieveAuthenticatedUserEmailCallback_.
292     * @param {string} email The authenticated user's e-mail address.
293     */
294    setAuthenticatedUserEmail: function(attemptToken, email) {
295      var msg = {
296        method: 'setAuthenticatedUserEmail',
297        attemptToken: attemptToken,
298        email: email
299      };
300      this.frame_.contentWindow.postMessage(msg, AUTH_URL_BASE);
301    },
302
303    /**
304     * Invoked to process authentication success.
305     * @param {Object} credentials Credential object to pass to success
306     *     callback.
307     * @private
308     */
309    onAuthSuccess_: function(credentials) {
310      if (this.successCallback_)
311        this.successCallback_(credentials);
312      cr.dispatchSimpleEvent(this, 'completed');
313    },
314
315    /**
316     * Checks if message comes from the loaded authentication extension.
317     * @param {Object} e Payload of the received HTML5 message.
318     * @type {boolean}
319     */
320    isAuthExtMessage_: function(e) {
321      return this.frame_.src &&
322          this.frame_.src.indexOf(e.origin) == 0 &&
323          e.source == this.frame_.contentWindow;
324    },
325
326    /**
327     * Event handler that is invoked when HTML5 message is received.
328     * @param {object} e Payload of the received HTML5 message.
329     */
330    onMessage_: function(e) {
331      var msg = e.data;
332
333      if (!this.isAuthExtMessage_(e))
334        return;
335
336      if (msg.method == 'loginUILoaded') {
337        cr.dispatchSimpleEvent(this, 'ready');
338        return;
339      }
340
341      if (/^complete(Login|Authentication)$|^offlineLogin$/.test(msg.method)) {
342        if (!msg.email && !this.email_ && !msg.skipForNow) {
343          var msg = {method: 'redirectToSignin'};
344          this.frame_.contentWindow.postMessage(msg, AUTH_URL_BASE);
345          return;
346        }
347        this.onAuthSuccess_({email: msg.email,
348                             password: msg.password,
349                             useOffline: msg.method == 'offlineLogin',
350                             usingSAML: msg.usingSAML || false,
351                             chooseWhatToSync: msg.chooseWhatToSync,
352                             skipForNow: msg.skipForNow || false,
353                             sessionIndex: msg.sessionIndex || ''});
354        return;
355      }
356
357      if (msg.method == 'retrieveAuthenticatedUserEmail') {
358        if (this.retrieveAuthenticatedUserEmailCallback_) {
359          this.retrieveAuthenticatedUserEmailCallback_(msg.attemptToken,
360                                                       msg.apiUsed);
361        } else {
362          console.error(
363              'GaiaAuthHost: Invalid retrieveAuthenticatedUserEmailCallback_.');
364        }
365        return;
366      }
367
368      if (msg.method == 'confirmPassword') {
369        if (this.confirmPasswordCallback_)
370          this.confirmPasswordCallback_(msg.passwordCount);
371        else
372          console.error('GaiaAuthHost: Invalid confirmPasswordCallback_.');
373        return;
374      }
375
376      if (msg.method == 'noPassword') {
377        if (this.noPasswordCallback_)
378          this.noPasswordCallback_(msg.email);
379        else
380          console.error('GaiaAuthHost: Invalid noPasswordCallback_.');
381        return;
382      }
383
384      if (msg.method == 'authPageLoaded') {
385        this.authDomain = msg.domain;
386        this.authFlow = msg.isSAML ? AuthFlow.SAML : AuthFlow.GAIA;
387        return;
388      }
389
390      if (msg.method == 'insecureContentBlocked') {
391        if (this.insecureContentBlockedCallback_) {
392          this.insecureContentBlockedCallback_(msg.url);
393        } else {
394          console.error(
395              'GaiaAuthHost: Invalid insecureContentBlockedCallback_.');
396        }
397        return;
398      }
399
400      if (msg.method == 'switchToFullTab') {
401        chrome.send('switchToFullTab', [msg.url]);
402        return;
403      }
404
405      console.error('Unknown message method=' + msg.method);
406    }
407  };
408
409  /**
410   * The current auth flow of the hosted gaia_auth extension.
411   * @type {AuthFlow}
412   */
413  cr.defineProperty(GaiaAuthHost, 'authFlow');
414
415  GaiaAuthHost.SUPPORTED_PARAMS = SUPPORTED_PARAMS;
416  GaiaAuthHost.LOCALIZED_STRING_PARAMS = LOCALIZED_STRING_PARAMS;
417  GaiaAuthHost.AuthMode = AuthMode;
418  GaiaAuthHost.AuthFlow = AuthFlow;
419
420  return {
421    GaiaAuthHost: GaiaAuthHost
422  };
423});
424