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 */ 8 function Authenticator() { 9 } 10 11 /** 12 * Gaia auth extension url origin. 13 * @type {string} 14 */ 15 Authenticator.THIS_EXTENSION_ORIGIN = 16 'chrome-extension://mfffpogegjflfpflabcdkioaeobkgjik'; 17 18 /** 19 * The lowest version of the credentials passing API supported. 20 * @type {number} 21 */ 22 Authenticator.MIN_API_VERSION_VERSION = 1; 23 24 /** 25 * The highest version of the credentials passing API supported. 26 * @type {number} 27 */ 28 Authenticator.MAX_API_VERSION_VERSION = 1; 29 30 /** 31 * The key types supported by the credentials passing API. 32 * @type {Array} Array of strings. 33 */ 34 Authenticator.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 */ 42 Authenticator.getInstance = function() { 43 if (!Authenticator.instance_) { 44 Authenticator.instance_ = new Authenticator(); 45 } 46 return Authenticator.instance_; 47 }; 48 49 Authenticator.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 isSAMLEnabled_: 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 document.addEventListener('DOMContentLoaded', this.onPageLoad_.bind(this)); 91 if (!this.desktopMode_) { 92 // SAML is always enabled in desktop mode, thus no need to listen for 93 // enableSAML event. 94 document.addEventListener('enableSAML', this.onEnableSAML_.bind(this)); 95 } 96 }, 97 98 isGaiaMessage_: function(msg) { 99 // Not quite right, but good enough. 100 return this.gaiaUrl_.indexOf(msg.origin) == 0 || 101 this.GAIA_URL.indexOf(msg.origin) == 0; 102 }, 103 104 isInternalMessage_: function(msg) { 105 return msg.origin == Authenticator.THIS_EXTENSION_ORIGIN; 106 }, 107 108 isParentMessage_: function(msg) { 109 return msg.origin == this.parentPage_; 110 }, 111 112 constructInitialFrameUrl_: function() { 113 var url = this.gaiaUrl_ + this.gaiaPath_; 114 115 url = appendParam(url, 'service', this.service_); 116 url = appendParam(url, 'continue', this.continueUrl_); 117 if (this.inputLang_) 118 url = appendParam(url, 'hl', this.inputLang_); 119 if (this.inputEmail_) 120 url = appendParam(url, 'Email', this.inputEmail_); 121 if (this.isConstrainedWindow_) 122 url = appendParam(url, 'source', this.CONSTRAINED_FLOW_SOURCE); 123 return url; 124 }, 125 126 onPageLoad_: function() { 127 window.addEventListener('message', this.onMessage.bind(this), false); 128 129 var gaiaFrame = $('gaia-frame'); 130 gaiaFrame.src = this.initialFrameUrl_; 131 132 if (this.desktopMode_) { 133 var handler = function() { 134 this.onLoginUILoaded_(); 135 gaiaFrame.removeEventListener('load', handler); 136 137 this.initDesktopChannel_(); 138 }.bind(this); 139 gaiaFrame.addEventListener('load', handler); 140 } 141 }, 142 143 initDesktopChannel_: function() { 144 this.supportChannel_ = new Channel(); 145 this.supportChannel_.connect('authMain'); 146 147 var channelConnected = false; 148 this.supportChannel_.registerMessage('channelConnected', function() { 149 channelConnected = true; 150 151 this.supportChannel_.send({ 152 name: 'initDesktopFlow', 153 gaiaUrl: this.gaiaUrl_, 154 continueUrl: stripParams(this.continueUrl_), 155 isConstrainedWindow: this.isConstrainedWindow_ 156 }); 157 this.supportChannel_.registerMessage( 158 'switchToFullTab', this.switchToFullTab_.bind(this)); 159 this.supportChannel_.registerMessage( 160 'completeLogin', this.completeLogin_.bind(this)); 161 162 this.onEnableSAML_(); 163 }.bind(this)); 164 165 window.setTimeout(function() { 166 if (!channelConnected) { 167 // Re-initialize the channel if it is not connected properly, e.g. 168 // connect may be called before background script started running. 169 this.initDesktopChannel_(); 170 } 171 }.bind(this), 200); 172 }, 173 174 /** 175 * Invoked when the login UI is initialized or reset. 176 */ 177 onLoginUILoaded_: function() { 178 var msg = { 179 'method': 'loginUILoaded' 180 }; 181 window.parent.postMessage(msg, this.parentPage_); 182 }, 183 184 /** 185 * Invoked when the background script sends a message to indicate that the 186 * current content does not fit in a constrained window. 187 * @param {Object=} opt_extraMsg Optional extra info to send. 188 */ 189 switchToFullTab_: function(msg) { 190 var parentMsg = { 191 'method': 'switchToFullTab', 192 'url': msg.url 193 }; 194 window.parent.postMessage(parentMsg, this.parentPage_); 195 }, 196 197 /** 198 * Invoked when the signin flow is complete. 199 * @param {Object=} opt_extraMsg Optional extra info to send. 200 */ 201 completeLogin_: function(opt_extraMsg) { 202 var msg = { 203 'method': 'completeLogin', 204 'email': (opt_extraMsg && opt_extraMsg.email) || this.email_, 205 'password': (opt_extraMsg && opt_extraMsg.password) || 206 this.passwordBytes_, 207 'usingSAML': this.isSAMLFlow_, 208 'chooseWhatToSync': this.chooseWhatToSync_ || false, 209 'skipForNow': opt_extraMsg && opt_extraMsg.skipForNow, 210 'sessionIndex': opt_extraMsg && opt_extraMsg.sessionIndex 211 }; 212 window.parent.postMessage(msg, this.parentPage_); 213 if (this.isSAMLEnabled_) 214 this.supportChannel_.send({name: 'resetAuth'}); 215 }, 216 217 /** 218 * Invoked when 'enableSAML' event is received to initialize SAML support on 219 * Chrome OS, or when initDesktopChannel_ is called on desktop. 220 */ 221 onEnableSAML_: function() { 222 this.isSAMLEnabled_ = true; 223 this.isSAMLFlow_ = false; 224 225 if (!this.supportChannel_) { 226 this.supportChannel_ = new Channel(); 227 this.supportChannel_.connect('authMain'); 228 } 229 230 this.supportChannel_.registerMessage( 231 'onAuthPageLoaded', this.onAuthPageLoaded_.bind(this)); 232 this.supportChannel_.registerMessage( 233 'onInsecureContentBlocked', this.onInsecureContentBlocked_.bind(this)); 234 this.supportChannel_.registerMessage( 235 'apiCall', this.onAPICall_.bind(this)); 236 this.supportChannel_.send({ 237 name: 'setGaiaUrl', 238 gaiaUrl: this.gaiaUrl_ 239 }); 240 if (!this.desktopMode_ && this.gaiaUrl_.indexOf('https://') == 0) { 241 // Abort the login flow when content served over an unencrypted connection 242 // is detected on Chrome OS. This does not apply to tests that explicitly 243 // set a non-https GAIA URL and want to perform all authentication over 244 // http. 245 this.supportChannel_.send({ 246 name: 'setBlockInsecureContent', 247 blockInsecureContent: true 248 }); 249 } 250 }, 251 252 /** 253 * Invoked when the background page sends 'onHostedPageLoaded' message. 254 * @param {!Object} msg Details sent with the message. 255 */ 256 onAuthPageLoaded_: function(msg) { 257 var isSAMLPage = msg.url.indexOf(this.gaiaUrl_) != 0; 258 259 if (isSAMLPage && !this.isSAMLFlow_) { 260 // GAIA redirected to a SAML login page. The credentials provided to this 261 // page will determine what user gets logged in. The credentials obtained 262 // from the GAIA login form are no longer relevant and can be discarded. 263 this.isSAMLFlow_ = true; 264 this.email_ = null; 265 this.passwordBytes_ = null; 266 } 267 268 window.parent.postMessage({ 269 'method': 'authPageLoaded', 270 'isSAML': this.isSAMLFlow_, 271 'domain': extractDomain(msg.url) 272 }, this.parentPage_); 273 }, 274 275 /** 276 * Invoked when the background page sends an 'onInsecureContentBlocked' 277 * message. 278 * @param {!Object} msg Details sent with the message. 279 */ 280 onInsecureContentBlocked_: function(msg) { 281 window.parent.postMessage({ 282 'method': 'insecureContentBlocked', 283 'url': stripParams(msg.url) 284 }, this.parentPage_); 285 }, 286 287 /** 288 * Invoked when one of the credential passing API methods is called by a SAML 289 * provider. 290 * @param {!Object} msg Details of the API call. 291 */ 292 onAPICall_: function(msg) { 293 var call = msg.call; 294 if (call.method == 'initialize') { 295 if (!Number.isInteger(call.requestedVersion) || 296 call.requestedVersion < Authenticator.MIN_API_VERSION_VERSION) { 297 this.sendInitializationFailure_(); 298 return; 299 } 300 301 this.apiVersion_ = Math.min(call.requestedVersion, 302 Authenticator.MAX_API_VERSION_VERSION); 303 this.initialized_ = true; 304 this.sendInitializationSuccess_(); 305 return; 306 } 307 308 if (call.method == 'add') { 309 if (Authenticator.API_KEY_TYPES.indexOf(call.keyType) == -1) { 310 console.error('Authenticator.onAPICall_: unsupported key type'); 311 return; 312 } 313 this.apiToken_ = call.token; 314 this.email_ = call.user; 315 this.passwordBytes_ = call.passwordBytes; 316 } else if (call.method == 'confirm') { 317 if (call.token != this.apiToken_) 318 console.error('Authenticator.onAPICall_: token mismatch'); 319 } else { 320 console.error('Authenticator.onAPICall_: unknown message'); 321 } 322 }, 323 324 sendInitializationSuccess_: function() { 325 this.supportChannel_.send({name: 'apiResponse', response: { 326 result: 'initialized', 327 version: this.apiVersion_, 328 keyTypes: Authenticator.API_KEY_TYPES 329 }}); 330 }, 331 332 sendInitializationFailure_: function() { 333 this.supportChannel_.send({ 334 name: 'apiResponse', 335 response: {result: 'initialization_failed'} 336 }); 337 }, 338 339 onConfirmLogin_: function() { 340 if (!this.isSAMLFlow_) { 341 this.completeLogin_(); 342 return; 343 } 344 345 var apiUsed = !!this.passwordBytes_; 346 347 // Retrieve the e-mail address of the user who just authenticated from GAIA. 348 window.parent.postMessage({method: 'retrieveAuthenticatedUserEmail', 349 attemptToken: this.attemptToken_, 350 apiUsed: apiUsed}, 351 this.parentPage_); 352 353 if (!apiUsed) { 354 this.supportChannel_.sendWithCallback( 355 {name: 'getScrapedPasswords'}, 356 function(passwords) { 357 if (passwords.length == 0) { 358 window.parent.postMessage( 359 {method: 'noPassword', email: this.email_}, 360 this.parentPage_); 361 } else { 362 window.parent.postMessage({method: 'confirmPassword', 363 email: this.email_, 364 passwordCount: passwords.length}, 365 this.parentPage_); 366 } 367 }.bind(this)); 368 } 369 }, 370 371 maybeCompleteSAMLLogin_: function() { 372 // SAML login is complete when the user's e-mail address has been retrieved 373 // from GAIA and the user has successfully confirmed the password. 374 if (this.email_ !== null && this.passwordBytes_ !== null) 375 this.completeLogin_(); 376 }, 377 378 onVerifyConfirmedPassword_: function(password) { 379 this.supportChannel_.sendWithCallback( 380 {name: 'getScrapedPasswords'}, 381 function(passwords) { 382 for (var i = 0; i < passwords.length; ++i) { 383 if (passwords[i] == password) { 384 this.passwordBytes_ = passwords[i]; 385 this.maybeCompleteSAMLLogin_(); 386 return; 387 } 388 } 389 window.parent.postMessage( 390 {method: 'confirmPassword', email: this.email_}, 391 this.parentPage_); 392 }.bind(this)); 393 }, 394 395 onMessage: function(e) { 396 var msg = e.data; 397 if (msg.method == 'attemptLogin' && this.isGaiaMessage_(e)) { 398 this.email_ = msg.email; 399 this.passwordBytes_ = msg.password; 400 this.attemptToken_ = msg.attemptToken; 401 this.chooseWhatToSync_ = msg.chooseWhatToSync; 402 this.isSAMLFlow_ = false; 403 if (this.isSAMLEnabled_) 404 this.supportChannel_.send({name: 'startAuth'}); 405 } else if (msg.method == 'clearOldAttempts' && this.isGaiaMessage_(e)) { 406 this.email_ = null; 407 this.passwordBytes_ = null; 408 this.attemptToken_ = null; 409 this.isSAMLFlow_ = false; 410 this.onLoginUILoaded_(); 411 if (this.isSAMLEnabled_) 412 this.supportChannel_.send({name: 'resetAuth'}); 413 } else if (msg.method == 'setAuthenticatedUserEmail' && 414 this.isParentMessage_(e)) { 415 if (this.attemptToken_ == msg.attemptToken) { 416 this.email_ = msg.email; 417 this.maybeCompleteSAMLLogin_(); 418 } 419 } else if (msg.method == 'confirmLogin' && this.isInternalMessage_(e)) { 420 if (this.attemptToken_ == msg.attemptToken) 421 this.onConfirmLogin_(); 422 else 423 console.error('Authenticator.onMessage: unexpected attemptToken!?'); 424 } else if (msg.method == 'verifyConfirmedPassword' && 425 this.isParentMessage_(e)) { 426 this.onVerifyConfirmedPassword_(msg.password); 427 } else if (msg.method == 'redirectToSignin' && 428 this.isParentMessage_(e)) { 429 $('gaia-frame').src = this.constructInitialFrameUrl_(); 430 } else { 431 console.error('Authenticator.onMessage: unknown message + origin!?'); 432 } 433 } 434 }; 435 436 Authenticator.getInstance().initialize(); 437