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 * Singleton getter of Authenticator. 20 * @return {Object} The singleton instance of Authenticator. 21 */ 22 Authenticator.getInstance = function() { 23 if (!Authenticator.instance_) { 24 Authenticator.instance_ = new Authenticator(); 25 } 26 return Authenticator.instance_; 27 }; 28 29 Authenticator.prototype = { 30 email_: null, 31 password_: null, 32 attemptToken_: null, 33 34 // Input params from extension initialization URL. 35 inputLang_: undefined, 36 intputEmail_: undefined, 37 38 samlPageLoaded_: false, 39 samlSupportChannel_: null, 40 41 GAIA_URL: 'https://accounts.google.com/', 42 GAIA_PAGE_PATH: 'ServiceLogin?skipvpage=true&sarp=1&rm=hide', 43 PARENT_PAGE: 'chrome://oobe/', 44 SERVICE_ID: 'chromeoslogin', 45 CONTINUE_URL: Authenticator.THIS_EXTENSION_ORIGIN + '/success.html', 46 47 initialize: function() { 48 var params = getUrlSearchParams(location.search); 49 this.parentPage_ = params.parentPage || this.PARENT_PAGE; 50 this.gaiaUrl_ = params.gaiaUrl || this.GAIA_URL; 51 this.gaiaPath_ = params.gaiaPath || this.GAIA_PAGE_PATH; 52 this.inputLang_ = params.hl; 53 this.inputEmail_ = params.email; 54 this.service_ = params.service || this.SERVICE_ID; 55 this.continueUrl_ = params.continueUrl || this.CONTINUE_URL; 56 this.continueUrlWithoutParams_ = stripParams(this.continueUrl_); 57 this.inlineMode_ = params.inlineMode == '1'; 58 this.constrained_ = params.constrained == '1'; 59 this.partitionId_ = params.partitionId || ''; 60 this.initialFrameUrl_ = params.frameUrl || this.constructInitialFrameUrl_(); 61 this.initialFrameUrlWithoutParams_ = stripParams(this.initialFrameUrl_); 62 this.loaded_ = false; 63 64 document.addEventListener('DOMContentLoaded', this.onPageLoad.bind(this)); 65 document.addEventListener('enableSAML', this.onEnableSAML_.bind(this)); 66 }, 67 68 isGaiaMessage_: function(msg) { 69 // Not quite right, but good enough. 70 return this.gaiaUrl_.indexOf(msg.origin) == 0 || 71 this.GAIA_URL.indexOf(msg.origin) == 0; 72 }, 73 74 isInternalMessage_: function(msg) { 75 return msg.origin == Authenticator.THIS_EXTENSION_ORIGIN; 76 }, 77 78 isParentMessage_: function(msg) { 79 return msg.origin == this.parentPage_; 80 }, 81 82 constructInitialFrameUrl_: function() { 83 var url = this.gaiaUrl_ + this.gaiaPath_; 84 85 url = appendParam(url, 'service', this.service_); 86 url = appendParam(url, 'continue', this.continueUrl_); 87 if (this.inputLang_) 88 url = appendParam(url, 'hl', this.inputLang_); 89 if (this.inputEmail_) 90 url = appendParam(url, 'Email', this.inputEmail_); 91 92 return url; 93 }, 94 95 /** Callback when all loads in the gaia webview is complete. */ 96 onWebviewLoadstop_: function(gaiaFrame) { 97 // Report the current state to the parent which will then update the 98 // browser history so that later it could respond properly to back/forward. 99 var msg = { 100 'method': 'reportState', 101 'src': gaiaFrame.src 102 }; 103 window.parent.postMessage(msg, this.parentPage_); 104 105 if (gaiaFrame.src.lastIndexOf( 106 this.continueUrlWithoutParams_, 0) == 0) { 107 // Detect when login is finished by the load stop event of the continue 108 // URL. Cannot reuse the login complete flow in success.html, because 109 // webview does not support extension pages yet. 110 gaiaFrame.hidden = true; 111 msg = {'method': 'completeLogin'}; 112 window.parent.postMessage(msg, this.parentPage_); 113 return; 114 } 115 116 if (gaiaFrame.src.lastIndexOf(this.gaiaUrl_, 0) == 0) { 117 gaiaFrame.executeScript({file: 'inline_injected.js'}, function() { 118 // Send an initial message to gaia so that it has an JavaScript 119 // reference to the embedder. 120 gaiaFrame.contentWindow.postMessage('', gaiaFrame.src); 121 }); 122 } 123 124 this.loaded_ || this.onLoginUILoaded(); 125 }, 126 127 /** 128 * Callback when the gaia webview attempts to open a new window. 129 */ 130 onWebviewNewWindow_: function(gaiaFrame, e) { 131 window.open(e.targetUrl, '_blank'); 132 e.window.discard(); 133 }, 134 135 onWebviewRequestCompleted_: function(details) { 136 if (details.url.lastIndexOf(this.continueUrlWithoutParams_, 0) == 0) { 137 return; 138 } 139 140 var headers = details.responseHeaders; 141 for (var i = 0; headers && i < headers.length; ++i) { 142 if (headers[i].name.toLowerCase() == 'google-accounts-embedded') { 143 return; 144 } 145 } 146 var msg = { 147 'method': 'switchToFullTab', 148 'url': details.url 149 }; 150 window.parent.postMessage(msg, this.parentPage_); 151 }, 152 153 loadFrame_: function() { 154 var gaiaFrame = $('gaia-frame'); 155 gaiaFrame.partition = this.partitionId_; 156 gaiaFrame.src = this.initialFrameUrl_; 157 if (this.inlineMode_) { 158 gaiaFrame.addEventListener( 159 'loadstop', this.onWebviewLoadstop_.bind(this, gaiaFrame)); 160 gaiaFrame.addEventListener( 161 'newwindow', this.onWebviewNewWindow_.bind(this, gaiaFrame)); 162 } 163 if (this.constrained_) { 164 gaiaFrame.request.onCompleted.addListener( 165 this.onWebviewRequestCompleted_.bind(this), 166 {urls: ['<all_urls>'], types: ['main_frame']}, 167 ['responseHeaders']); 168 } 169 }, 170 171 completeLogin: function(username, password) { 172 var msg = { 173 'method': 'completeLogin', 174 'email': username, 175 'password': password 176 }; 177 window.parent.postMessage(msg, this.parentPage_); 178 if (this.samlSupportChannel_) 179 this.samlSupportChannel_.send({name: 'resetAuth'}); 180 }, 181 182 onPageLoad: function(e) { 183 window.addEventListener('message', this.onMessage.bind(this), false); 184 this.loadFrame_(); 185 }, 186 187 /** 188 * Invoked when 'enableSAML' event is received to initialize SAML support. 189 */ 190 onEnableSAML_: function() { 191 this.samlPageLoaded_ = false; 192 193 this.samlSupportChannel_ = new Channel(); 194 this.samlSupportChannel_.connect('authMain'); 195 this.samlSupportChannel_.registerMessage( 196 'onAuthPageLoaded', this.onAuthPageLoaded_.bind(this)); 197 this.samlSupportChannel_.send({ 198 name: 'setGaiaUrl', 199 gaiaUrl: this.gaiaUrl_ 200 }); 201 }, 202 203 /** 204 * Invoked when the background page sends 'onHostedPageLoaded' message. 205 * @param {!Object} msg Details sent with the message. 206 */ 207 onAuthPageLoaded_: function(msg) { 208 this.samlPageLoaded_ = msg.url.indexOf(this.gaiaUrl_) != 0; 209 window.parent.postMessage({ 210 'method': 'authPageLoaded', 211 'isSAML': this.samlPageLoaded_ 212 }, this.parentPage_); 213 }, 214 215 onLoginUILoaded: function() { 216 var msg = { 217 'method': 'loginUILoaded' 218 }; 219 window.parent.postMessage(msg, this.parentPage_); 220 if (this.inlineMode_) { 221 $('gaia-frame').focus(); 222 } 223 this.loaded_ = true; 224 }, 225 226 onConfirmLogin_: function() { 227 if (!this.samlPageLoaded_) { 228 this.completeLogin(this.email_, this.password_); 229 return; 230 } 231 232 this.samlSupportChannel_.sendWithCallback( 233 {name: 'getScrapedPasswords'}, 234 function(passwords) { 235 if (passwords.length == 0) { 236 window.parent.postMessage( 237 {method: 'noPassword', email: this.email_}, 238 this.parentPage_); 239 } else { 240 window.parent.postMessage( 241 {method: 'confirmPassword', email: this.email_}, 242 this.parentPage_); 243 } 244 }.bind(this)); 245 }, 246 247 onVerifyConfirmedPassword_: function(password) { 248 this.samlSupportChannel_.sendWithCallback( 249 {name: 'getScrapedPasswords'}, 250 function(passwords) { 251 for (var i = 0; i < passwords.length; ++i) { 252 if (passwords[i] == password) { 253 this.completeLogin(this.email_, passwords[i]); 254 return; 255 } 256 } 257 window.parent.postMessage( 258 {method: 'confirmPassword', email: this.email_}, 259 this.parentPage_); 260 }.bind(this)); 261 }, 262 263 onMessage: function(e) { 264 var msg = e.data; 265 if (msg.method == 'attemptLogin' && this.isGaiaMessage_(e)) { 266 this.email_ = msg.email; 267 this.password_ = msg.password; 268 this.attemptToken_ = msg.attemptToken; 269 this.samlPageLoaded_ = false; 270 if (this.samlSupportChannel_) 271 this.samlSupportChannel_.send({name: 'startAuth'}); 272 } else if (msg.method == 'clearOldAttempts' && this.isGaiaMessage_(e)) { 273 this.email_ = null; 274 this.password_ = null; 275 this.attemptToken_ = null; 276 this.samlPageLoaded_ = false; 277 this.onLoginUILoaded(); 278 if (this.samlSupportChannel_) 279 this.samlSupportChannel_.send({name: 'resetAuth'}); 280 } else if (msg.method == 'confirmLogin' && this.isInternalMessage_(e)) { 281 if (this.attemptToken_ == msg.attemptToken) 282 this.onConfirmLogin_(); 283 else 284 console.error('Authenticator.onMessage: unexpected attemptToken!?'); 285 } else if (msg.method == 'verifyConfirmedPassword' && 286 this.isParentMessage_(e)) { 287 this.onVerifyConfirmedPassword_(msg.password); 288 } else if (msg.method == 'navigate' && 289 this.isParentMessage_(e)) { 290 $('gaia-frame').src = msg.src; 291 } else { 292 console.error('Authenticator.onMessage: unknown message + origin!?'); 293 } 294 } 295 }; 296 297 Authenticator.getInstance().initialize(); 298