Roll src/third_party/WebKit bf18a82:a9cee16 (svn 185297:185304)
[chromium-blink-merge.git] / chrome / browser / resources / gaia_auth / main.js
blob7523ceb65c2942bf4280c2c97d1996385cda8d20
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.
5 /**
6 * Authenticator class wraps the communications between Gaia and its host.
7 */
8 function Authenticator() {
11 /**
12 * Gaia auth extension url origin.
13 * @type {string}
15 Authenticator.THIS_EXTENSION_ORIGIN =
16 'chrome-extension://mfffpogegjflfpflabcdkioaeobkgjik';
18 /**
19 * The lowest version of the credentials passing API supported.
20 * @type {number}
22 Authenticator.MIN_API_VERSION_VERSION = 1;
24 /**
25 * The highest version of the credentials passing API supported.
26 * @type {number}
28 Authenticator.MAX_API_VERSION_VERSION = 1;
30 /**
31 * The key types supported by the credentials passing API.
32 * @type {Array} Array of strings.
34 Authenticator.API_KEY_TYPES = [
35 'KEY_TYPE_PASSWORD_PLAIN',
38 /**
39 * Singleton getter of Authenticator.
40 * @return {Object} The singleton instance of Authenticator.
42 Authenticator.getInstance = function() {
43 if (!Authenticator.instance_) {
44 Authenticator.instance_ = new Authenticator();
46 return Authenticator.instance_;
49 Authenticator.prototype = {
50 email_: null,
51 gaiaId_: null,
53 // Depending on the key type chosen, this will contain the plain text password
54 // or a credential derived from it along with the information required to
55 // repeat the derivation, such as a salt. The information will be encoded so
56 // that it contains printable ASCII characters only. The exact encoding is TBD
57 // when support for key types other than plain text password is added.
58 passwordBytes_: null,
60 chooseWhatToSync_: false,
61 skipForNow_: false,
62 sessionIndex_: null,
63 attemptToken_: null,
65 // Input params from extension initialization URL.
66 inputLang_: undefined,
67 intputEmail_: undefined,
69 isSAMLFlow_: false,
70 gaiaLoaded_: false,
71 supportChannel_: null,
73 GAIA_URL: 'https://accounts.google.com/',
74 GAIA_PAGE_PATH: 'ServiceLogin?skipvpage=true&sarp=1&rm=hide',
75 PARENT_PAGE: 'chrome://oobe/',
76 SERVICE_ID: 'chromeoslogin',
77 CONTINUE_URL: Authenticator.THIS_EXTENSION_ORIGIN + '/success.html',
78 CONSTRAINED_FLOW_SOURCE: 'chrome',
80 initialize: function() {
81 var params = getUrlSearchParams(location.search);
82 this.parentPage_ = params.parentPage || this.PARENT_PAGE;
83 this.gaiaUrl_ = params.gaiaUrl || this.GAIA_URL;
84 this.gaiaPath_ = params.gaiaPath || this.GAIA_PAGE_PATH;
85 this.inputLang_ = params.hl;
86 this.inputEmail_ = params.email;
87 this.service_ = params.service || this.SERVICE_ID;
88 this.continueUrl_ = params.continueUrl || this.CONTINUE_URL;
89 this.desktopMode_ = params.desktopMode == '1';
90 this.isConstrainedWindow_ = params.constrained == '1';
91 this.initialFrameUrl_ = params.frameUrl || this.constructInitialFrameUrl_();
92 this.initialFrameUrlWithoutParams_ = stripParams(this.initialFrameUrl_);
94 // For CrOS 'ServiceLogin' we assume that Gaia is loaded if we recieved
95 // 'clearOldAttempts' message. For other scenarios Gaia doesn't send this
96 // message so we have to rely on 'load' event.
97 // TODO(dzhioev): Do not rely on 'load' event after b/16313327 is fixed.
98 this.assumeLoadedOnLoadEvent_ =
99 this.gaiaPath_.indexOf('ServiceLogin') !== 0 ||
100 this.service_ !== 'chromeoslogin';
102 document.addEventListener('DOMContentLoaded', this.onPageLoad_.bind(this));
105 isGaiaMessage_: function(msg) {
106 // Not quite right, but good enough.
107 return this.gaiaUrl_.indexOf(msg.origin) == 0 ||
108 this.GAIA_URL.indexOf(msg.origin) == 0;
111 isParentMessage_: function(msg) {
112 return msg.origin == this.parentPage_;
115 constructInitialFrameUrl_: function() {
116 var url = this.gaiaUrl_ + this.gaiaPath_;
118 url = appendParam(url, 'service', this.service_);
119 url = appendParam(url, 'continue', this.continueUrl_);
120 if (this.inputLang_)
121 url = appendParam(url, 'hl', this.inputLang_);
122 if (this.inputEmail_)
123 url = appendParam(url, 'Email', this.inputEmail_);
124 if (this.isConstrainedWindow_)
125 url = appendParam(url, 'source', this.CONSTRAINED_FLOW_SOURCE);
126 return url;
129 onPageLoad_: function() {
130 window.addEventListener('message', this.onMessage.bind(this), false);
131 this.initSupportChannel_();
133 var gaiaFrame = $('gaia-frame');
134 gaiaFrame.src = this.initialFrameUrl_;
136 if (this.assumeLoadedOnLoadEvent_) {
137 var handler = function() {
138 gaiaFrame.removeEventListener('load', handler);
139 if (!this.gaiaLoaded_) {
140 this.gaiaLoaded_ = true;
141 this.maybeInitialized_();
143 }.bind(this);
144 gaiaFrame.addEventListener('load', handler);
148 initSupportChannel_: function() {
149 var supportChannel = new Channel();
150 supportChannel.connect('authMain');
152 supportChannel.registerMessage('channelConnected', function() {
153 if (this.supportChannel_) {
154 console.error('Support channel is already initialized.');
155 return;
157 this.supportChannel_ = supportChannel;
159 if (this.desktopMode_) {
160 this.supportChannel_.send({
161 name: 'initDesktopFlow',
162 gaiaUrl: this.gaiaUrl_,
163 continueUrl: stripParams(this.continueUrl_),
164 isConstrainedWindow: this.isConstrainedWindow_
166 this.supportChannel_.registerMessage(
167 'switchToFullTab', this.switchToFullTab_.bind(this));
169 this.supportChannel_.registerMessage(
170 'completeLogin', this.onCompleteLogin_.bind(this));
171 this.initSAML_();
172 this.maybeInitialized_();
173 }.bind(this));
175 window.setTimeout(function() {
176 if (!this.supportChannel_) {
177 // Re-initialize the channel if it is not connected properly, e.g.
178 // connect may be called before background script started running.
179 this.initSupportChannel_();
181 }.bind(this), 200);
185 * Called when one of the initialization stages has finished. If all the
186 * needed parts are initialized, notifies parent about successfull
187 * initialization.
189 maybeInitialized_: function() {
190 if (!this.gaiaLoaded_ || !this.supportChannel_)
191 return;
192 var msg = {
193 'method': 'loginUILoaded'
195 window.parent.postMessage(msg, this.parentPage_);
199 * Invoked when the background script sends a message to indicate that the
200 * current content does not fit in a constrained window.
201 * @param {Object=} msg Extra info to send.
203 switchToFullTab_: function(msg) {
204 var parentMsg = {
205 'method': 'switchToFullTab',
206 'url': msg.url
208 window.parent.postMessage(parentMsg, this.parentPage_);
212 * Invoked when the signin flow is complete.
213 * @param {Object=} opt_extraMsg Optional extra info to send.
215 completeLogin_: function(opt_extraMsg) {
216 var msg = {
217 'method': 'completeLogin',
218 'email': (opt_extraMsg && opt_extraMsg.email) || this.email_,
219 'password': (opt_extraMsg && opt_extraMsg.password) ||
220 this.passwordBytes_,
221 'usingSAML': this.isSAMLFlow_,
222 'chooseWhatToSync': this.chooseWhatToSync_ || false,
223 'skipForNow': (opt_extraMsg && opt_extraMsg.skipForNow) ||
224 this.skipForNow_,
225 'sessionIndex': (opt_extraMsg && opt_extraMsg.sessionIndex) ||
226 this.sessionIndex_,
227 'gaiaId': (opt_extraMsg && opt_extraMsg.gaiaId) || this.gaiaId_
229 window.parent.postMessage(msg, this.parentPage_);
230 this.supportChannel_.send({name: 'resetAuth'});
234 * Invoked when support channel is connected.
236 initSAML_: function() {
237 this.isSAMLFlow_ = false;
239 this.supportChannel_.registerMessage(
240 'onAuthPageLoaded', this.onAuthPageLoaded_.bind(this));
241 this.supportChannel_.registerMessage(
242 'onInsecureContentBlocked', this.onInsecureContentBlocked_.bind(this));
243 this.supportChannel_.registerMessage(
244 'apiCall', this.onAPICall_.bind(this));
245 this.supportChannel_.send({
246 name: 'setGaiaUrl',
247 gaiaUrl: this.gaiaUrl_
249 if (!this.desktopMode_ && this.gaiaUrl_.indexOf('https://') == 0) {
250 // Abort the login flow when content served over an unencrypted connection
251 // is detected on Chrome OS. This does not apply to tests that explicitly
252 // set a non-https GAIA URL and want to perform all authentication over
253 // http.
254 this.supportChannel_.send({
255 name: 'setBlockInsecureContent',
256 blockInsecureContent: true
262 * Invoked when the background page sends 'onHostedPageLoaded' message.
263 * @param {!Object} msg Details sent with the message.
265 onAuthPageLoaded_: function(msg) {
266 if (msg.isSAMLPage && !this.isSAMLFlow_) {
267 // GAIA redirected to a SAML login page. The credentials provided to this
268 // page will determine what user gets logged in. The credentials obtained
269 // from the GAIA login form are no longer relevant and can be discarded.
270 this.isSAMLFlow_ = true;
271 this.email_ = null;
272 this.gaiaId_ = null;
273 this.passwordBytes_ = null;
276 window.parent.postMessage({
277 'method': 'authPageLoaded',
278 'isSAML': this.isSAMLFlow_,
279 'domain': extractDomain(msg.url)
280 }, this.parentPage_);
284 * Invoked when the background page sends an 'onInsecureContentBlocked'
285 * message.
286 * @param {!Object} msg Details sent with the message.
288 onInsecureContentBlocked_: function(msg) {
289 window.parent.postMessage({
290 'method': 'insecureContentBlocked',
291 'url': stripParams(msg.url)
292 }, this.parentPage_);
296 * Invoked when one of the credential passing API methods is called by a SAML
297 * provider.
298 * @param {!Object} msg Details of the API call.
300 onAPICall_: function(msg) {
301 var call = msg.call;
302 if (call.method == 'initialize') {
303 if (!Number.isInteger(call.requestedVersion) ||
304 call.requestedVersion < Authenticator.MIN_API_VERSION_VERSION) {
305 this.sendInitializationFailure_();
306 return;
309 this.apiVersion_ = Math.min(call.requestedVersion,
310 Authenticator.MAX_API_VERSION_VERSION);
311 this.initialized_ = true;
312 this.sendInitializationSuccess_();
313 return;
316 if (call.method == 'add') {
317 if (Authenticator.API_KEY_TYPES.indexOf(call.keyType) == -1) {
318 console.error('Authenticator.onAPICall_: unsupported key type');
319 return;
321 // Not setting |email_| and |gaiaId_| because this API call will
322 // eventually be followed by onCompleteLogin_() which does set it.
323 this.apiToken_ = call.token;
324 this.passwordBytes_ = call.passwordBytes;
325 } else if (call.method == 'confirm') {
326 if (call.token != this.apiToken_)
327 console.error('Authenticator.onAPICall_: token mismatch');
328 } else {
329 console.error('Authenticator.onAPICall_: unknown message');
333 sendInitializationSuccess_: function() {
334 this.supportChannel_.send({name: 'apiResponse', response: {
335 result: 'initialized',
336 version: this.apiVersion_,
337 keyTypes: Authenticator.API_KEY_TYPES
338 }});
341 sendInitializationFailure_: function() {
342 this.supportChannel_.send({
343 name: 'apiResponse',
344 response: {result: 'initialization_failed'}
349 * Callback invoked for 'completeLogin' message.
350 * @param {Object=} msg Message sent from background page.
352 onCompleteLogin_: function(msg) {
353 if (!msg.email || !msg.gaiaId || !msg.sessionIndex) {
354 console.error('Missing fields to complete login.');
355 window.parent.postMessage({method: 'missingGaiaInfo'}, this.parentPage_);
356 return;
359 // Skip SAML extra steps for desktop flow and non-SAML flow.
360 if (!this.isSAMLFlow_ || this.desktopMode_) {
361 this.completeLogin_(msg);
362 return;
365 this.email_ = msg.email;
366 this.gaiaId_ = msg.gaiaId;
367 // Password from |msg| is not used because ChromeOS SAML flow
368 // gets password by asking user to confirm.
369 this.skipForNow_ = msg.skipForNow;
370 this.sessionIndex_ = msg.sessionIndex;
372 if (this.passwordBytes_) {
373 window.parent.postMessage({method: 'samlApiUsed'}, this.parentPage_);
374 this.completeLogin_(msg);
375 } else {
376 this.supportChannel_.sendWithCallback(
377 {name: 'getScrapedPasswords'},
378 function(passwords) {
379 if (passwords.length == 0) {
380 window.parent.postMessage(
381 {method: 'noPassword', email: this.email_},
382 this.parentPage_);
383 } else {
384 window.parent.postMessage({method: 'confirmPassword',
385 email: this.email_,
386 passwordCount: passwords.length},
387 this.parentPage_);
389 }.bind(this));
393 onVerifyConfirmedPassword_: function(password) {
394 this.supportChannel_.sendWithCallback(
395 {name: 'getScrapedPasswords'},
396 function(passwords) {
397 for (var i = 0; i < passwords.length; ++i) {
398 if (passwords[i] == password) {
399 this.passwordBytes_ = passwords[i];
400 // SAML login is complete when the user has successfully
401 // confirmed the password.
402 if (this.passwordBytes_ !== null)
403 this.completeLogin_();
404 return;
407 window.parent.postMessage(
408 {method: 'confirmPassword', email: this.email_},
409 this.parentPage_);
410 }.bind(this));
413 onMessage: function(e) {
414 var msg = e.data;
415 if (msg.method == 'attemptLogin' && this.isGaiaMessage_(e)) {
416 // At this point GAIA does not yet know the gaiaId, so its not set here.
417 this.email_ = msg.email;
418 this.passwordBytes_ = msg.password;
419 this.attemptToken_ = msg.attemptToken;
420 this.chooseWhatToSync_ = msg.chooseWhatToSync;
421 this.isSAMLFlow_ = false;
422 if (this.supportChannel_)
423 this.supportChannel_.send({name: 'startAuth'});
424 else
425 console.error('Support channel is not initialized.');
426 } else if (msg.method == 'clearOldAttempts' && this.isGaiaMessage_(e)) {
427 if (!this.gaiaLoaded_) {
428 this.gaiaLoaded_ = true;
429 this.maybeInitialized_();
431 this.email_ = null;
432 this.gaiaId_ = null;
433 this.sessionIndex_ = false;
434 this.passwordBytes_ = null;
435 this.attemptToken_ = null;
436 this.isSAMLFlow_ = false;
437 this.skipForNow_ = false;
438 this.chooseWhatToSync_ = false;
439 if (this.supportChannel_)
440 this.supportChannel_.send({name: 'resetAuth'});
441 } else if (msg.method == 'verifyConfirmedPassword' &&
442 this.isParentMessage_(e)) {
443 this.onVerifyConfirmedPassword_(msg.password);
444 } else if (msg.method == 'redirectToSignin' &&
445 this.isParentMessage_(e)) {
446 $('gaia-frame').src = this.constructInitialFrameUrl_();
447 } else {
448 console.error('Authenticator.onMessage: unknown message + origin!?');
453 Authenticator.getInstance().initialize();