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