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 * The lowest version of the credentials passing API supported. 20 * @type {number} 21 */ 22Authenticator.MIN_API_VERSION_VERSION = 1; 23 24/** 25 * The highest version of the credentials passing API supported. 26 * @type {number} 27 */ 28Authenticator.MAX_API_VERSION_VERSION = 1; 29 30/** 31 * The key types supported by the credentials passing API. 32 * @type {Array} Array of strings. 33 */ 34Authenticator.API_KEY_TYPES = [ 35 'KEY_TYPE_PASSWORD_PLAIN', 36]; 37 38/** 39 * Singleton getter of Authenticator. 40 * @return {Object} The singleton instance of Authenticator. 41 */ 42Authenticator.getInstance = function() { 43 if (!Authenticator.instance_) { 44 Authenticator.instance_ = new Authenticator(); 45 } 46 return Authenticator.instance_; 47}; 48 49Authenticator.prototype = { 50 email_: null, 51 52 // Depending on the key type chosen, this will contain the plain text password 53 // or a credential derived from it along with the information required to 54 // repeat the derivation, such as a salt. The information will be encoded so 55 // that it contains printable ASCII characters only. The exact encoding is TBD 56 // when support for key types other than plain text password is added. 57 passwordBytes_: null, 58 59 attemptToken_: null, 60 61 // Input params from extension initialization URL. 62 inputLang_: undefined, 63 intputEmail_: undefined, 64 65 isSAMLFlow_: false, 66 gaiaLoaded_: false, 67 supportChannel_: null, 68 69 GAIA_URL: 'https://accounts.google.com/', 70 GAIA_PAGE_PATH: 'ServiceLogin?skipvpage=true&sarp=1&rm=hide', 71 PARENT_PAGE: 'chrome://oobe/', 72 SERVICE_ID: 'chromeoslogin', 73 CONTINUE_URL: Authenticator.THIS_EXTENSION_ORIGIN + '/success.html', 74 CONSTRAINED_FLOW_SOURCE: 'chrome', 75 76 initialize: function() { 77 var params = getUrlSearchParams(location.search); 78 this.parentPage_ = params.parentPage || this.PARENT_PAGE; 79 this.gaiaUrl_ = params.gaiaUrl || this.GAIA_URL; 80 this.gaiaPath_ = params.gaiaPath || this.GAIA_PAGE_PATH; 81 this.inputLang_ = params.hl; 82 this.inputEmail_ = params.email; 83 this.service_ = params.service || this.SERVICE_ID; 84 this.continueUrl_ = params.continueUrl || this.CONTINUE_URL; 85 this.desktopMode_ = params.desktopMode == '1'; 86 this.isConstrainedWindow_ = params.constrained == '1'; 87 this.initialFrameUrl_ = params.frameUrl || this.constructInitialFrameUrl_(); 88 this.initialFrameUrlWithoutParams_ = stripParams(this.initialFrameUrl_); 89 90 // For CrOS 'ServiceLogin' we assume that Gaia is loaded if we recieved 91 // 'clearOldAttempts' message. For other scenarios Gaia doesn't send this 92 // message so we have to rely on 'load' event. 93 // TODO(dzhioev): Do not rely on 'load' event after b/16313327 is fixed. 94 this.assumeLoadedOnLoadEvent_ = 95 this.gaiaPath_.indexOf('ServiceLogin') !== 0 || 96 this.service_ !== 'chromeoslogin'; 97 98 document.addEventListener('DOMContentLoaded', this.onPageLoad_.bind(this)); 99 }, 100 101 isGaiaMessage_: function(msg) { 102 // Not quite right, but good enough. 103 return this.gaiaUrl_.indexOf(msg.origin) == 0 || 104 this.GAIA_URL.indexOf(msg.origin) == 0; 105 }, 106 107 isInternalMessage_: function(msg) { 108 return msg.origin == Authenticator.THIS_EXTENSION_ORIGIN; 109 }, 110 111 isParentMessage_: function(msg) { 112 return msg.origin == this.parentPage_; 113 }, 114 115 constructInitialFrameUrl_: function() { 116 var url = this.gaiaUrl_ + this.gaiaPath_; 117 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; 127 }, 128 129 onPageLoad_: function() { 130 window.addEventListener('message', this.onMessage.bind(this), false); 131 this.initSupportChannel_(); 132 133 var gaiaFrame = $('gaia-frame'); 134 gaiaFrame.src = this.initialFrameUrl_; 135 136 if (this.assumeLoadedOnLoadEvent_) { 137 var handler = function() { 138 gaiaFrame.removeEventListener('load', handler); 139 if (!this.gaiaLoaded_) { 140 this.gaiaLoaded_ = true; 141 this.maybeInitialized_(); 142 } 143 }.bind(this); 144 gaiaFrame.addEventListener('load', handler); 145 } 146 }, 147 148 initSupportChannel_: function() { 149 var supportChannel = new Channel(); 150 supportChannel.connect('authMain'); 151 152 supportChannel.registerMessage('channelConnected', function() { 153 if (this.supportChannel_) { 154 console.error('Support channel is already initialized.'); 155 return; 156 } 157 this.supportChannel_ = supportChannel; 158 159 if (this.desktopMode_) { 160 this.supportChannel_.send({ 161 name: 'initDesktopFlow', 162 gaiaUrl: this.gaiaUrl_, 163 continueUrl: stripParams(this.continueUrl_), 164 isConstrainedWindow: this.isConstrainedWindow_ 165 }); 166 this.supportChannel_.registerMessage( 167 'switchToFullTab', this.switchToFullTab_.bind(this)); 168 this.supportChannel_.registerMessage( 169 'completeLogin', this.completeLogin_.bind(this)); 170 } 171 this.initSAML_(); 172 this.maybeInitialized_(); 173 }.bind(this)); 174 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_(); 180 } 181 }.bind(this), 200); 182 }, 183 184 /** 185 * Called when one of the initialization stages has finished. If all the 186 * needed parts are initialized, notifies parent about successfull 187 * initialization. 188 */ 189 maybeInitialized_: function() { 190 if (!this.gaiaLoaded_ || !this.supportChannel_) 191 return; 192 var msg = { 193 'method': 'loginUILoaded' 194 }; 195 window.parent.postMessage(msg, this.parentPage_); 196 }, 197 198 /** 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=} opt_extraMsg Optional extra info to send. 202 */ 203 switchToFullTab_: function(msg) { 204 var parentMsg = { 205 'method': 'switchToFullTab', 206 'url': msg.url 207 }; 208 window.parent.postMessage(parentMsg, this.parentPage_); 209 }, 210 211 /** 212 * Invoked when the signin flow is complete. 213 * @param {Object=} opt_extraMsg Optional extra info to send. 214 */ 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 'sessionIndex': opt_extraMsg && opt_extraMsg.sessionIndex 225 }; 226 window.parent.postMessage(msg, this.parentPage_); 227 this.supportChannel_.send({name: 'resetAuth'}); 228 }, 229 230 /** 231 * Invoked when support channel is connected. 232 */ 233 initSAML_: function() { 234 this.isSAMLFlow_ = false; 235 236 this.supportChannel_.registerMessage( 237 'onAuthPageLoaded', this.onAuthPageLoaded_.bind(this)); 238 this.supportChannel_.registerMessage( 239 'onInsecureContentBlocked', this.onInsecureContentBlocked_.bind(this)); 240 this.supportChannel_.registerMessage( 241 'apiCall', this.onAPICall_.bind(this)); 242 this.supportChannel_.send({ 243 name: 'setGaiaUrl', 244 gaiaUrl: this.gaiaUrl_ 245 }); 246 if (!this.desktopMode_ && this.gaiaUrl_.indexOf('https://') == 0) { 247 // Abort the login flow when content served over an unencrypted connection 248 // is detected on Chrome OS. This does not apply to tests that explicitly 249 // set a non-https GAIA URL and want to perform all authentication over 250 // http. 251 this.supportChannel_.send({ 252 name: 'setBlockInsecureContent', 253 blockInsecureContent: true 254 }); 255 } 256 }, 257 258 /** 259 * Invoked when the background page sends 'onHostedPageLoaded' message. 260 * @param {!Object} msg Details sent with the message. 261 */ 262 onAuthPageLoaded_: function(msg) { 263 var isSAMLPage = msg.url.indexOf(this.gaiaUrl_) != 0; 264 265 if (isSAMLPage && !this.isSAMLFlow_) { 266 // GAIA redirected to a SAML login page. The credentials provided to this 267 // page will determine what user gets logged in. The credentials obtained 268 // from the GAIA login form are no longer relevant and can be discarded. 269 this.isSAMLFlow_ = true; 270 this.email_ = null; 271 this.passwordBytes_ = null; 272 } 273 274 window.parent.postMessage({ 275 'method': 'authPageLoaded', 276 'isSAML': this.isSAMLFlow_, 277 'domain': extractDomain(msg.url) 278 }, this.parentPage_); 279 }, 280 281 /** 282 * Invoked when the background page sends an 'onInsecureContentBlocked' 283 * message. 284 * @param {!Object} msg Details sent with the message. 285 */ 286 onInsecureContentBlocked_: function(msg) { 287 window.parent.postMessage({ 288 'method': 'insecureContentBlocked', 289 'url': stripParams(msg.url) 290 }, this.parentPage_); 291 }, 292 293 /** 294 * Invoked when one of the credential passing API methods is called by a SAML 295 * provider. 296 * @param {!Object} msg Details of the API call. 297 */ 298 onAPICall_: function(msg) { 299 var call = msg.call; 300 if (call.method == 'initialize') { 301 if (!Number.isInteger(call.requestedVersion) || 302 call.requestedVersion < Authenticator.MIN_API_VERSION_VERSION) { 303 this.sendInitializationFailure_(); 304 return; 305 } 306 307 this.apiVersion_ = Math.min(call.requestedVersion, 308 Authenticator.MAX_API_VERSION_VERSION); 309 this.initialized_ = true; 310 this.sendInitializationSuccess_(); 311 return; 312 } 313 314 if (call.method == 'add') { 315 if (Authenticator.API_KEY_TYPES.indexOf(call.keyType) == -1) { 316 console.error('Authenticator.onAPICall_: unsupported key type'); 317 return; 318 } 319 this.apiToken_ = call.token; 320 this.email_ = call.user; 321 this.passwordBytes_ = call.passwordBytes; 322 } else if (call.method == 'confirm') { 323 if (call.token != this.apiToken_) 324 console.error('Authenticator.onAPICall_: token mismatch'); 325 } else { 326 console.error('Authenticator.onAPICall_: unknown message'); 327 } 328 }, 329 330 sendInitializationSuccess_: function() { 331 this.supportChannel_.send({name: 'apiResponse', response: { 332 result: 'initialized', 333 version: this.apiVersion_, 334 keyTypes: Authenticator.API_KEY_TYPES 335 }}); 336 }, 337 338 sendInitializationFailure_: function() { 339 this.supportChannel_.send({ 340 name: 'apiResponse', 341 response: {result: 'initialization_failed'} 342 }); 343 }, 344 345 onConfirmLogin_: function() { 346 if (!this.isSAMLFlow_) { 347 this.completeLogin_(); 348 return; 349 } 350 351 var apiUsed = !!this.passwordBytes_; 352 353 // Retrieve the e-mail address of the user who just authenticated from GAIA. 354 window.parent.postMessage({method: 'retrieveAuthenticatedUserEmail', 355 attemptToken: this.attemptToken_, 356 apiUsed: apiUsed}, 357 this.parentPage_); 358 359 if (!apiUsed) { 360 this.supportChannel_.sendWithCallback( 361 {name: 'getScrapedPasswords'}, 362 function(passwords) { 363 if (passwords.length == 0) { 364 window.parent.postMessage( 365 {method: 'noPassword', email: this.email_}, 366 this.parentPage_); 367 } else { 368 window.parent.postMessage({method: 'confirmPassword', 369 email: this.email_, 370 passwordCount: passwords.length}, 371 this.parentPage_); 372 } 373 }.bind(this)); 374 } 375 }, 376 377 maybeCompleteSAMLLogin_: function() { 378 // SAML login is complete when the user's e-mail address has been retrieved 379 // from GAIA and the user has successfully confirmed the password. 380 if (this.email_ !== null && this.passwordBytes_ !== null) 381 this.completeLogin_(); 382 }, 383 384 onVerifyConfirmedPassword_: function(password) { 385 this.supportChannel_.sendWithCallback( 386 {name: 'getScrapedPasswords'}, 387 function(passwords) { 388 for (var i = 0; i < passwords.length; ++i) { 389 if (passwords[i] == password) { 390 this.passwordBytes_ = passwords[i]; 391 this.maybeCompleteSAMLLogin_(); 392 return; 393 } 394 } 395 window.parent.postMessage( 396 {method: 'confirmPassword', email: this.email_}, 397 this.parentPage_); 398 }.bind(this)); 399 }, 400 401 onMessage: function(e) { 402 var msg = e.data; 403 if (msg.method == 'attemptLogin' && this.isGaiaMessage_(e)) { 404 this.email_ = msg.email; 405 this.passwordBytes_ = msg.password; 406 this.attemptToken_ = msg.attemptToken; 407 this.chooseWhatToSync_ = msg.chooseWhatToSync; 408 this.isSAMLFlow_ = false; 409 if (this.supportChannel_) 410 this.supportChannel_.send({name: 'startAuth'}); 411 else 412 console.error('Support channel is not initialized.'); 413 } else if (msg.method == 'clearOldAttempts' && this.isGaiaMessage_(e)) { 414 if (!this.gaiaLoaded_) { 415 this.gaiaLoaded_ = true; 416 this.maybeInitialized_(); 417 } 418 this.email_ = null; 419 this.passwordBytes_ = null; 420 this.attemptToken_ = null; 421 this.isSAMLFlow_ = false; 422 if (this.supportChannel_) 423 this.supportChannel_.send({name: 'resetAuth'}); 424 } else if (msg.method == 'setAuthenticatedUserEmail' && 425 this.isParentMessage_(e)) { 426 if (this.attemptToken_ == msg.attemptToken) { 427 this.email_ = msg.email; 428 this.maybeCompleteSAMLLogin_(); 429 } 430 } else if (msg.method == 'confirmLogin' && this.isInternalMessage_(e)) { 431 // In the desktop mode, Chrome needs to wait for extra info such as 432 // session index from the background JS. 433 if (this.desktopMode_) 434 return; 435 436 if (this.attemptToken_ == msg.attemptToken) 437 this.onConfirmLogin_(); 438 else 439 console.error('Authenticator.onMessage: unexpected attemptToken!?'); 440 } else if (msg.method == 'verifyConfirmedPassword' && 441 this.isParentMessage_(e)) { 442 this.onVerifyConfirmedPassword_(msg.password); 443 } else if (msg.method == 'redirectToSignin' && 444 this.isParentMessage_(e)) { 445 $('gaia-frame').src = this.constructInitialFrameUrl_(); 446 } else { 447 console.error('Authenticator.onMessage: unknown message + origin!?'); 448 } 449 } 450}; 451 452Authenticator.getInstance().initialize(); 453