Revert of Add button to add new FSP services to Files app. (patchset #8 id:140001...
[chromium-blink-merge.git] / chrome / browser / resources / gaia_auth / main.js
blobb751687e6af643ac8260c9446bee4d1e5b26484c
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 * Allowed origins of the hosting page.
40 * @type {Array<string>}
42 Authenticator.ALLOWED_PARENT_ORIGINS = [
43 'chrome://oobe',
44 'chrome://chrome-signin'
47 /**
48 * Singleton getter of Authenticator.
49 * @return {Object} The singleton instance of Authenticator.
51 Authenticator.getInstance = function() {
52 if (!Authenticator.instance_) {
53 Authenticator.instance_ = new Authenticator();
55 return Authenticator.instance_;
58 Authenticator.prototype = {
59 email_: null,
60 gaiaId_: null,
62 // Depending on the key type chosen, this will contain the plain text password
63 // or a credential derived from it along with the information required to
64 // repeat the derivation, such as a salt. The information will be encoded so
65 // that it contains printable ASCII characters only. The exact encoding is TBD
66 // when support for key types other than plain text password is added.
67 passwordBytes_: null,
69 needPassword_: false,
70 chooseWhatToSync_: false,
71 skipForNow_: false,
72 sessionIndex_: null,
73 attemptToken_: null,
75 // Input params from extension initialization URL.
76 inputLang_: undefined,
77 intputEmail_: undefined,
79 isSAMLFlow_: false,
80 gaiaLoaded_: false,
81 supportChannel_: null,
83 useEafe_: false,
84 clientId_: '',
86 GAIA_URL: 'https://accounts.google.com/',
87 GAIA_PAGE_PATH: 'ServiceLogin?skipvpage=true&sarp=1&rm=hide',
88 SERVICE_ID: 'chromeoslogin',
89 CONTINUE_URL: Authenticator.THIS_EXTENSION_ORIGIN + '/success.html',
90 CONSTRAINED_FLOW_SOURCE: 'chrome',
92 initialize: function() {
93 var handleInitializeMessage = function(e) {
94 if (Authenticator.ALLOWED_PARENT_ORIGINS.indexOf(e.origin) == -1) {
95 console.error('Unexpected parent message, origin=' + e.origin);
96 return;
98 window.removeEventListener('message', handleInitializeMessage);
100 var params = e.data;
101 params.parentPage = e.origin;
102 this.initializeFromParent_(params);
103 this.onPageLoad_();
104 }.bind(this);
106 document.addEventListener('DOMContentLoaded', function() {
107 window.addEventListener('message', handleInitializeMessage);
111 initializeFromParent_: function(params) {
112 this.parentPage_ = params.parentPage;
113 this.gaiaUrl_ = params.gaiaUrl || this.GAIA_URL;
114 this.gaiaPath_ = params.gaiaPath || this.GAIA_PAGE_PATH;
115 this.inputLang_ = params.hl;
116 this.inputEmail_ = params.email;
117 this.service_ = params.service || this.SERVICE_ID;
118 this.continueUrl_ = params.continueUrl || this.CONTINUE_URL;
119 this.desktopMode_ = params.desktopMode == '1';
120 this.isConstrainedWindow_ = params.constrained == '1';
121 this.useEafe_ = params.useEafe || false;
122 this.clientId_ = params.clientId || '';
123 this.initialFrameUrl_ = params.frameUrl || this.constructInitialFrameUrl_();
124 this.initialFrameUrlWithoutParams_ = stripParams(this.initialFrameUrl_);
125 this.needPassword_ = params.needPassword == '1';
127 // For CrOS 'ServiceLogin' we assume that Gaia is loaded if we recieved
128 // 'clearOldAttempts' message. For other scenarios Gaia doesn't send this
129 // message so we have to rely on 'load' event.
130 // TODO(dzhioev): Do not rely on 'load' event after b/16313327 is fixed.
131 this.assumeLoadedOnLoadEvent_ =
132 this.gaiaPath_.indexOf('ServiceLogin') !== 0 ||
133 this.service_ !== 'chromeoslogin' ||
134 this.useEafe_;
137 isGaiaMessage_: function(msg) {
138 // Not quite right, but good enough.
139 return this.gaiaUrl_.indexOf(msg.origin) == 0 ||
140 this.GAIA_URL.indexOf(msg.origin) == 0;
143 isParentMessage_: function(msg) {
144 return msg.origin == this.parentPage_;
147 constructInitialFrameUrl_: function() {
148 var url = this.gaiaUrl_ + this.gaiaPath_;
150 url = appendParam(url, 'service', this.service_);
151 // Easy bootstrap use auth_code message as success signal instead of
152 // continue URL.
153 if (!this.useEafe_)
154 url = appendParam(url, 'continue', this.continueUrl_);
155 if (this.inputLang_)
156 url = appendParam(url, 'hl', this.inputLang_);
157 if (this.inputEmail_)
158 url = appendParam(url, 'Email', this.inputEmail_);
159 if (this.isConstrainedWindow_)
160 url = appendParam(url, 'source', this.CONSTRAINED_FLOW_SOURCE);
161 return url;
164 onPageLoad_: function() {
165 window.addEventListener('message', this.onMessage.bind(this), false);
166 this.initSupportChannel_();
168 if (this.assumeLoadedOnLoadEvent_) {
169 var gaiaFrame = $('gaia-frame');
170 var handler = function() {
171 gaiaFrame.removeEventListener('load', handler);
172 if (!this.gaiaLoaded_) {
173 this.gaiaLoaded_ = true;
174 this.maybeInitialized_();
176 if (this.useEafe_ && this.clientId_) {
177 // Sends initial handshake message to EAFE. Note this fails with
178 // SSO redirect because |gaiaFrame| sits on a different origin.
179 gaiaFrame.contentWindow.postMessage({
180 clientId: this.clientId_
181 }, this.gaiaUrl_);
184 }.bind(this);
185 gaiaFrame.addEventListener('load', handler);
189 initSupportChannel_: function() {
190 var supportChannel = new Channel();
191 supportChannel.connect('authMain');
193 supportChannel.registerMessage('channelConnected', function() {
194 // Load the gaia frame after the background page indicates that it is
195 // ready, so that the webRequest handlers are all setup first.
196 var gaiaFrame = $('gaia-frame');
197 gaiaFrame.src = this.initialFrameUrl_;
199 if (this.supportChannel_) {
200 console.error('Support channel is already initialized.');
201 return;
203 this.supportChannel_ = supportChannel;
205 if (this.desktopMode_) {
206 this.supportChannel_.send({
207 name: 'initDesktopFlow',
208 gaiaUrl: this.gaiaUrl_,
209 continueUrl: stripParams(this.continueUrl_),
210 isConstrainedWindow: this.isConstrainedWindow_,
211 initialFrameUrlWithoutParams: this.initialFrameUrlWithoutParams_
214 this.supportChannel_.registerMessage(
215 'switchToFullTab', this.switchToFullTab_.bind(this));
217 this.supportChannel_.registerMessage(
218 'completeLogin', this.onCompleteLogin_.bind(this));
219 this.initSAML_();
220 this.supportChannel_.send({name: 'resetAuth'});
221 this.maybeInitialized_();
222 }.bind(this));
224 window.setTimeout(function() {
225 if (!this.supportChannel_) {
226 // Re-initialize the channel if it is not connected properly, e.g.
227 // connect may be called before background script started running.
228 this.initSupportChannel_();
230 }.bind(this), 200);
234 * Called when one of the initialization stages has finished. If all the
235 * needed parts are initialized, notifies parent about successfull
236 * initialization.
238 maybeInitialized_: function() {
239 if (!this.gaiaLoaded_ || !this.supportChannel_)
240 return;
241 var msg = {
242 'method': 'loginUILoaded'
244 window.parent.postMessage(msg, this.parentPage_);
248 * Invoked when the background script sends a message to indicate that the
249 * current content does not fit in a constrained window.
250 * @param {Object=} msg Extra info to send.
252 switchToFullTab_: function(msg) {
253 var parentMsg = {
254 'method': 'switchToFullTab',
255 'url': msg.url
257 window.parent.postMessage(parentMsg, this.parentPage_);
261 * Invoked when the signin flow is complete.
262 * @param {Object=} opt_extraMsg Optional extra info to send.
264 completeLogin_: function(opt_extraMsg) {
265 var msg = {
266 'method': 'completeLogin',
267 'email': (opt_extraMsg && opt_extraMsg.email) || this.email_,
268 'password': (opt_extraMsg && opt_extraMsg.password) ||
269 this.passwordBytes_,
270 'usingSAML': this.isSAMLFlow_,
271 'chooseWhatToSync': this.chooseWhatToSync_ || false,
272 'skipForNow': (opt_extraMsg && opt_extraMsg.skipForNow) ||
273 this.skipForNow_,
274 'sessionIndex': (opt_extraMsg && opt_extraMsg.sessionIndex) ||
275 this.sessionIndex_,
276 'gaiaId': (opt_extraMsg && opt_extraMsg.gaiaId) || this.gaiaId_
278 window.parent.postMessage(msg, this.parentPage_);
279 this.supportChannel_.send({name: 'resetAuth'});
283 * Invoked when support channel is connected.
285 initSAML_: function() {
286 this.isSAMLFlow_ = false;
288 this.supportChannel_.registerMessage(
289 'onAuthPageLoaded', this.onAuthPageLoaded_.bind(this));
290 this.supportChannel_.registerMessage(
291 'onInsecureContentBlocked', this.onInsecureContentBlocked_.bind(this));
292 this.supportChannel_.registerMessage(
293 'apiCall', this.onAPICall_.bind(this));
294 this.supportChannel_.send({
295 name: 'setGaiaUrl',
296 gaiaUrl: this.gaiaUrl_
298 if (!this.desktopMode_ && this.gaiaUrl_.indexOf('https://') == 0) {
299 // Abort the login flow when content served over an unencrypted connection
300 // is detected on Chrome OS. This does not apply to tests that explicitly
301 // set a non-https GAIA URL and want to perform all authentication over
302 // http.
303 this.supportChannel_.send({
304 name: 'setBlockInsecureContent',
305 blockInsecureContent: true
311 * Invoked when the background page sends 'onHostedPageLoaded' message.
312 * @param {!Object} msg Details sent with the message.
314 onAuthPageLoaded_: function(msg) {
315 if (msg.isSAMLPage && !this.isSAMLFlow_) {
316 // GAIA redirected to a SAML login page. The credentials provided to this
317 // page will determine what user gets logged in. The credentials obtained
318 // from the GAIA login form are no longer relevant and can be discarded.
319 this.isSAMLFlow_ = true;
320 this.email_ = null;
321 this.gaiaId_ = null;
322 this.passwordBytes_ = null;
325 window.parent.postMessage({
326 'method': 'authPageLoaded',
327 'isSAML': this.isSAMLFlow_,
328 'domain': extractDomain(msg.url)
329 }, this.parentPage_);
333 * Invoked when the background page sends an 'onInsecureContentBlocked'
334 * message.
335 * @param {!Object} msg Details sent with the message.
337 onInsecureContentBlocked_: function(msg) {
338 window.parent.postMessage({
339 'method': 'insecureContentBlocked',
340 'url': stripParams(msg.url)
341 }, this.parentPage_);
345 * Invoked when one of the credential passing API methods is called by a SAML
346 * provider.
347 * @param {!Object} msg Details of the API call.
349 onAPICall_: function(msg) {
350 var call = msg.call;
351 if (call.method == 'initialize') {
352 if (!Number.isInteger(call.requestedVersion) ||
353 call.requestedVersion < Authenticator.MIN_API_VERSION_VERSION) {
354 this.sendInitializationFailure_();
355 return;
358 this.apiVersion_ = Math.min(call.requestedVersion,
359 Authenticator.MAX_API_VERSION_VERSION);
360 this.initialized_ = true;
361 this.sendInitializationSuccess_();
362 return;
365 if (call.method == 'add') {
366 if (Authenticator.API_KEY_TYPES.indexOf(call.keyType) == -1) {
367 console.error('Authenticator.onAPICall_: unsupported key type');
368 return;
370 // Not setting |email_| and |gaiaId_| because this API call will
371 // eventually be followed by onCompleteLogin_() which does set it.
372 this.apiToken_ = call.token;
373 this.passwordBytes_ = call.passwordBytes;
374 } else if (call.method == 'confirm') {
375 if (call.token != this.apiToken_)
376 console.error('Authenticator.onAPICall_: token mismatch');
377 } else {
378 console.error('Authenticator.onAPICall_: unknown message');
382 onGotAuthCode_: function(authCode) {
383 window.parent.postMessage({
384 'method': 'completeAuthenticationAuthCodeOnly',
385 'authCode': authCode
386 }, this.parentPage_);
389 sendInitializationSuccess_: function() {
390 this.supportChannel_.send({name: 'apiResponse', response: {
391 result: 'initialized',
392 version: this.apiVersion_,
393 keyTypes: Authenticator.API_KEY_TYPES
394 }});
397 sendInitializationFailure_: function() {
398 this.supportChannel_.send({
399 name: 'apiResponse',
400 response: {result: 'initialization_failed'}
405 * Callback invoked for 'completeLogin' message.
406 * @param {Object=} msg Message sent from background page.
408 onCompleteLogin_: function(msg) {
409 if (!msg.email || !msg.gaiaId || !msg.sessionIndex) {
410 // On desktop, if the skipForNow message field is set, send it to handler.
411 // This does not require the email, gaiaid or session to be valid.
412 if (this.desktopMode_ && msg.skipForNow) {
413 this.completeLogin_(msg);
414 } else {
415 console.error('Missing fields to complete login.');
416 window.parent.postMessage({method: 'missingGaiaInfo'},
417 this.parentPage_);
418 return;
422 // Skip SAML extra steps for desktop flow and non-SAML flow.
423 if (!this.isSAMLFlow_ || this.desktopMode_) {
424 this.completeLogin_(msg);
425 return;
428 this.email_ = msg.email;
429 this.gaiaId_ = msg.gaiaId;
430 // Password from |msg| is not used because ChromeOS SAML flow
431 // gets password by asking user to confirm.
432 this.skipForNow_ = msg.skipForNow;
433 this.sessionIndex_ = msg.sessionIndex;
435 if (this.passwordBytes_) {
436 // If the credentials passing API was used, login is complete.
437 window.parent.postMessage({method: 'samlApiUsed'}, this.parentPage_);
438 this.completeLogin_(msg);
439 } else if (!this.needPassword_) {
440 // If the credentials passing API was not used, the password was obtained
441 // by scraping. It must be verified before use. However, the host may not
442 // be interested in the password at all. In that case, verification is
443 // unnecessary and login is complete.
444 this.completeLogin_(msg);
445 } else {
446 this.supportChannel_.sendWithCallback(
447 {name: 'getScrapedPasswords'},
448 function(passwords) {
449 if (passwords.length == 0) {
450 window.parent.postMessage(
451 {method: 'noPassword', email: this.email_},
452 this.parentPage_);
453 } else {
454 window.parent.postMessage({method: 'confirmPassword',
455 email: this.email_,
456 passwordCount: passwords.length},
457 this.parentPage_);
459 }.bind(this));
463 onVerifyConfirmedPassword_: function(password) {
464 this.supportChannel_.sendWithCallback(
465 {name: 'getScrapedPasswords'},
466 function(passwords) {
467 for (var i = 0; i < passwords.length; ++i) {
468 if (passwords[i] == password) {
469 this.passwordBytes_ = passwords[i];
470 // SAML login is complete when the user has successfully
471 // confirmed the password.
472 if (this.passwordBytes_ !== null)
473 this.completeLogin_();
474 return;
477 window.parent.postMessage(
478 {method: 'confirmPassword', email: this.email_},
479 this.parentPage_);
480 }.bind(this));
483 onMessage: function(e) {
484 var msg = e.data;
486 if (this.useEafe_) {
487 if (msg == '!_{h:\'gaia-frame\'}' && this.isGaiaMessage_(e)) {
488 // Sends client ID again on the hello message to work around the SSO
489 // signin issue.
490 // TODO(xiyuan): Revisit this when EAFE is integrated or for webview.
491 $('gaia-frame').contentWindow.postMessage({
492 clientId: this.clientId_
493 }, this.gaiaUrl_);
494 } else if (typeof msg == 'object' &&
495 msg.type == 'authorizationCode' && this.isGaiaMessage_(e)) {
496 this.onGotAuthCode_(msg.authorizationCode);
497 } else {
498 console.error('Authenticator.onMessage: unknown message' +
499 ', msg=' + JSON.stringify(msg));
502 return;
505 if (msg.method == 'attemptLogin' && this.isGaiaMessage_(e)) {
506 // At this point GAIA does not yet know the gaiaId, so its not set here.
507 this.email_ = msg.email;
508 this.passwordBytes_ = msg.password;
509 this.attemptToken_ = msg.attemptToken;
510 this.chooseWhatToSync_ = msg.chooseWhatToSync;
511 this.isSAMLFlow_ = false;
512 if (this.supportChannel_)
513 this.supportChannel_.send({name: 'startAuth'});
514 else
515 console.error('Support channel is not initialized.');
516 } else if (msg.method == 'clearOldAttempts' && this.isGaiaMessage_(e)) {
517 if (!this.gaiaLoaded_) {
518 this.gaiaLoaded_ = true;
519 this.maybeInitialized_();
521 this.email_ = null;
522 this.gaiaId_ = null;
523 this.sessionIndex_ = false;
524 this.passwordBytes_ = null;
525 this.attemptToken_ = null;
526 this.isSAMLFlow_ = false;
527 this.skipForNow_ = false;
528 this.chooseWhatToSync_ = false;
529 if (this.supportChannel_) {
530 this.supportChannel_.send({name: 'resetAuth'});
531 // This message is for clearing saml properties in gaia_auth_host and
532 // oobe_screen_oauth_enrollment.
533 window.parent.postMessage({
534 'method': 'resetAuthFlow',
535 }, this.parentPage_);
537 } else if (msg.method == 'verifyConfirmedPassword' &&
538 this.isParentMessage_(e)) {
539 this.onVerifyConfirmedPassword_(msg.password);
540 } else if (msg.method == 'redirectToSignin' &&
541 this.isParentMessage_(e)) {
542 $('gaia-frame').src = this.constructInitialFrameUrl_();
543 } else {
544 console.error('Authenticator.onMessage: unknown message + origin!?');
549 Authenticator.getInstance().initialize();