| OLD | NEW |
| 1 // Copyright 2014 The Chromium Authors. All rights reserved. | 1 // Copyright 2014 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 /** | 5 /** |
| 6 * @fileoverview An UI component to authenciate to Chrome. The component hosts | 6 * @fileoverview An UI component to authenciate to Chrome. The component hosts |
| 7 * IdP web pages in a webview. A client who is interested in monitoring | 7 * IdP web pages in a webview. A client who is interested in monitoring |
| 8 * authentication events should pass a listener object of type | 8 * authentication events should pass a listener object of type |
| 9 * cr.login.GaiaAuthHost.Listener as defined in this file. After initialization, | 9 * cr.login.GaiaAuthHost.Listener as defined in this file. After initialization, |
| 10 * call {@code load} to start the authentication flow. | 10 * call {@code load} to start the authentication flow. |
| 11 */ | 11 */ |
| 12 cr.define('cr.login', function() { | 12 cr.define('cr.login', function() { |
| 13 'use strict'; | 13 'use strict'; |
| 14 | 14 |
| 15 // TODO(rogerta): should use gaia URL from GaiaUrls::gaia_url() instead |
| 16 // of hardcoding the prod URL here. As is, this does not work with staging |
| 17 // environments. |
| 15 var IDP_ORIGIN = 'https://accounts.google.com/'; | 18 var IDP_ORIGIN = 'https://accounts.google.com/'; |
| 16 var IDP_PATH = 'ServiceLogin?skipvpage=true&sarp=1&rm=hide'; | 19 var IDP_PATH = 'ServiceLogin?skipvpage=true&sarp=1&rm=hide'; |
| 17 var CONTINUE_URL = | 20 var CONTINUE_URL = |
| 18 'chrome-extension://mfffpogegjflfpflabcdkioaeobkgjik/success.html'; | 21 'chrome-extension://mfffpogegjflfpflabcdkioaeobkgjik/success.html'; |
| 19 var SIGN_IN_HEADER = 'google-accounts-signin'; | 22 var SIGN_IN_HEADER = 'google-accounts-signin'; |
| 20 var EMBEDDED_FORM_HEADER = 'google-accounts-embedded'; | 23 var EMBEDDED_FORM_HEADER = 'google-accounts-embedded'; |
| 21 var SAML_HEADER = 'google-accounts-saml'; | 24 var SAML_HEADER = 'google-accounts-saml'; |
| 22 | 25 |
| 23 /** | 26 /** |
| 24 * The source URL parameter for the constrained signin flow. | 27 * The source URL parameter for the constrained signin flow. |
| (...skipping 17 matching lines...) Expand all Loading... |
| 42 */ | 45 */ |
| 43 var AuthFlow = { | 46 var AuthFlow = { |
| 44 DEFAULT: 0, | 47 DEFAULT: 0, |
| 45 SAML: 1 | 48 SAML: 1 |
| 46 }; | 49 }; |
| 47 | 50 |
| 48 /** | 51 /** |
| 49 * Initializes the authenticator component. | 52 * Initializes the authenticator component. |
| 50 * @param {webview|string} webview The webview element or its ID to host IdP | 53 * @param {webview|string} webview The webview element or its ID to host IdP |
| 51 * web pages. | 54 * web pages. |
| 52 * @param {Authenticator.Listener=} opt_listener An optional listener for | |
| 53 * authentication events. | |
| 54 * @constructor | 55 * @constructor |
| 55 * @extends {cr.EventTarget} | |
| 56 */ | 56 */ |
| 57 function Authenticator(webview, opt_listener) { | 57 function Authenticator(webview) { |
| 58 this.webview_ = typeof webview == 'string' ? $(webview) : webview; | 58 this.webview_ = typeof webview == 'string' ? $(webview) : webview; |
| 59 assert(this.webview_); | 59 assert(this.webview_); |
| 60 | 60 |
| 61 this.listener_ = opt_listener || null; | |
| 62 | |
| 63 this.email_ = null; | 61 this.email_ = null; |
| 64 this.password_ = null; | 62 this.password_ = null; |
| 65 this.gaiaId_ = null, | 63 this.gaiaId_ = null, |
| 66 this.sessionIndex_ = null; | 64 this.sessionIndex_ = null; |
| 67 this.chooseWhatToSync_ = false; | 65 this.chooseWhatToSync_ = false; |
| 68 this.skipForNow_ = false; | 66 this.skipForNow_ = false; |
| 69 this.authFlow_ = AuthFlow.DEFAULT; | 67 this.authFlow_ = AuthFlow.DEFAULT; |
| 70 this.loaded_ = false; | 68 this.loaded_ = false; |
| 71 this.idpOrigin_ = null; | 69 this.idpOrigin_ = null; |
| 72 this.continueUrl_ = null; | 70 this.continueUrl_ = null; |
| 73 this.continueUrlWithoutParams_ = null; | 71 this.continueUrlWithoutParams_ = null; |
| 74 this.initialFrameUrl_ = null; | 72 this.initialFrameUrl_ = null; |
| 75 this.reloadUrl_ = null; | 73 this.reloadUrl_ = null; |
| 74 this.trusted_ = true; |
| 76 } | 75 } |
| 77 | 76 |
| 78 // TODO(guohui,xiyuan): no need to inherit EventTarget once we deprecate the | 77 // TODO(guohui,xiyuan): no need to inherit EventTarget once we deprecate the |
| 79 // old event-based signin flow. | 78 // old event-based signin flow. |
| 80 Authenticator.prototype = Object.create(cr.EventTarget.prototype); | 79 Authenticator.prototype = Object.create(cr.EventTarget.prototype); |
| 81 | 80 |
| 82 /** | 81 /** |
| 83 * An interface for receiving notifications upon authentication events. | |
| 84 * @interface | |
| 85 */ | |
| 86 Authenticator.Listener = function() {}; | |
| 87 | |
| 88 /** | |
| 89 * Invoked when authentication UI is ready. | |
| 90 */ | |
| 91 Authenticator.Listener.prototype.onReady = function(e) {}; | |
| 92 | |
| 93 /** | |
| 94 * Invoked when authentication is completed successfully with credential data. | |
| 95 * A credential data object looks like this: | |
| 96 * <pre> | |
| 97 * {@code | |
| 98 * { | |
| 99 * email: 'xx@gmail.com', | |
| 100 * password: 'xxxx', // May be null or empty. | |
| 101 * usingSAML: false, | |
| 102 * chooseWhatToSync: false, | |
| 103 * skipForNow: false, | |
| 104 * sessionIndex: '0' | |
| 105 * } | |
| 106 * } | |
| 107 * </pre> | |
| 108 * @param {Object} credentials A credential data object. | |
| 109 */ | |
| 110 Authenticator.Listener.prototype.onSuccess = function(credentials) {}; | |
| 111 | |
| 112 /** | |
| 113 * Invoked when the requested URL does not fit the container. | |
| 114 * @param {string} url Request URL. | |
| 115 */ | |
| 116 Authenticator.Listener.prototype.onResize = function(url) {}; | |
| 117 | |
| 118 /** | |
| 119 * Invoked when a new window event is fired. | |
| 120 * @param {Event} e Event object. | |
| 121 */ | |
| 122 Authenticator.Listener.prototype.onNewWindow = function(e) {}; | |
| 123 | |
| 124 /** | |
| 125 * Loads the authenticator component with the given parameters. | 82 * Loads the authenticator component with the given parameters. |
| 126 * @param {AuthMode} authMode Authorization mode. | 83 * @param {AuthMode} authMode Authorization mode. |
| 127 * @param {Object} data Parameters for the authorization flow. | 84 * @param {Object} data Parameters for the authorization flow. |
| 128 */ | 85 */ |
| 129 Authenticator.prototype.load = function(authMode, data) { | 86 Authenticator.prototype.load = function(authMode, data) { |
| 130 this.idpOrigin_ = data.gaiaUrl || IDP_ORIGIN; | 87 this.idpOrigin_ = data.gaiaUrl || IDP_ORIGIN; |
| 131 this.continueUrl_ = data.continueUrl || CONTINUE_URL; | 88 this.continueUrl_ = data.continueUrl || CONTINUE_URL; |
| 132 this.continueUrlWithoutParams_ = | 89 this.continueUrlWithoutParams_ = |
| 133 this.continueUrl_.substring(0, this.continueUrl_.indexOf('?')) || | 90 this.continueUrl_.substring(0, this.continueUrl_.indexOf('?')) || |
| 134 this.continueUrl_; | 91 this.continueUrl_; |
| 135 this.isConstrainedWindow_ = data.constrained == '1'; | 92 this.isConstrainedWindow_ = data.constrained == '1'; |
| 136 | 93 |
| 137 this.initialFrameUrl_ = this.constructInitialFrameUrl_(data); | 94 this.initialFrameUrl_ = this.constructInitialFrameUrl_(data); |
| 138 this.reloadUrl_ = data.frameUrl || this.initialFrameUrl_; | 95 this.reloadUrl_ = data.frameUrl || this.initialFrameUrl_; |
| 139 this.authFlow_ = AuthFlow.DEFAULT; | 96 this.authFlow_ = AuthFlow.DEFAULT; |
| 140 | 97 |
| 141 this.webview_.src = this.reloadUrl_; | 98 this.webview_.src = this.reloadUrl_; |
| 142 this.webview_.addEventListener( | 99 this.webview_.addEventListener( |
| 143 'newwindow', this.onNewWindow_.bind(this)); | 100 'newwindow', this.onNewWindow_.bind(this)); |
| 101 this.webview_.addEventListener( |
| 102 'loadstop', this.onLoadStop_.bind(this)); |
| 144 this.webview_.request.onCompleted.addListener( | 103 this.webview_.request.onCompleted.addListener( |
| 145 this.onRequestCompleted_.bind(this), | 104 this.onRequestCompleted_.bind(this), |
| 146 {urls: ['*://*/*', this.continueUrlWithoutParams_ + '*'], | 105 {urls: ['*://*/*', this.continueUrlWithoutParams_ + '*'], |
| 147 types: ['main_frame']}, | 106 types: ['main_frame']}, |
| 148 ['responseHeaders']); | 107 ['responseHeaders']); |
| 149 this.webview_.request.onHeadersReceived.addListener( | 108 this.webview_.request.onHeadersReceived.addListener( |
| 150 this.onHeadersReceived_.bind(this), | 109 this.onHeadersReceived_.bind(this), |
| 151 {urls: [this.idpOrigin_ + '*'], types: ['main_frame']}, | 110 {urls: [this.idpOrigin_ + '*'], types: ['main_frame']}, |
| 152 ['responseHeaders']); | 111 ['responseHeaders']); |
| 153 window.addEventListener( | 112 window.addEventListener( |
| 154 'message', this.onMessage_.bind(this), false); | 113 'message', this.onMessageFromWebview_.bind(this), false); |
| 155 }; | 114 }; |
| 156 | 115 |
| 157 /** | 116 /** |
| 158 * Reloads the authenticator component. | 117 * Reloads the authenticator component. |
| 159 */ | 118 */ |
| 160 Authenticator.prototype.reload = function() { | 119 Authenticator.prototype.reload = function() { |
| 161 this.webview_.src = this.reloadUrl_; | 120 this.webview_.src = this.reloadUrl_; |
| 162 this.authFlow_ = AuthFlow.DEFAULT; | 121 this.authFlow_ = AuthFlow.DEFAULT; |
| 163 }; | 122 }; |
| 164 | 123 |
| (...skipping 10 matching lines...) Expand all Loading... |
| 175 url = appendParam(url, 'source', CONSTRAINED_FLOW_SOURCE); | 134 url = appendParam(url, 'source', CONSTRAINED_FLOW_SOURCE); |
| 176 return url; | 135 return url; |
| 177 }; | 136 }; |
| 178 | 137 |
| 179 /** | 138 /** |
| 180 * Invoked when a main frame request in the webview has completed. | 139 * Invoked when a main frame request in the webview has completed. |
| 181 * @private | 140 * @private |
| 182 */ | 141 */ |
| 183 Authenticator.prototype.onRequestCompleted_ = function(details) { | 142 Authenticator.prototype.onRequestCompleted_ = function(details) { |
| 184 var currentUrl = details.url; | 143 var currentUrl = details.url; |
| 144 |
| 185 if (currentUrl.lastIndexOf(this.continueUrlWithoutParams_, 0) == 0) { | 145 if (currentUrl.lastIndexOf(this.continueUrlWithoutParams_, 0) == 0) { |
| 186 if (currentUrl.indexOf('ntp=1') >= 0) { | 146 if (currentUrl.indexOf('ntp=1') >= 0) { |
| 187 this.skipForNow_ = true; | 147 this.skipForNow_ = true; |
| 188 } | 148 } |
| 189 this.onAuthCompleted_(); | 149 this.onAuthCompleted_(); |
| 190 return; | 150 return; |
| 191 } | 151 } |
| 192 | 152 |
| 153 if (currentUrl.indexOf('https') != 0) { |
| 154 this.trusted_ = false; |
| 155 } |
| 156 |
| 193 if (this.isConstrainedWindow_) { | 157 if (this.isConstrainedWindow_) { |
| 194 var isEmbeddedPage = false; | 158 var isEmbeddedPage = false; |
| 195 if (this.idpOrigin_ && currentUrl.lastIndexOf(this.idpOrigin_) == 0) { | 159 if (this.idpOrigin_ && currentUrl.lastIndexOf(this.idpOrigin_) == 0) { |
| 196 var headers = details.responseHeaders; | 160 var headers = details.responseHeaders; |
| 197 for (var i = 0; headers && i < headers.length; ++i) { | 161 for (var i = 0; headers && i < headers.length; ++i) { |
| 198 if (headers[i].name.toLowerCase() == EMBEDDED_FORM_HEADER) { | 162 if (headers[i].name.toLowerCase() == EMBEDDED_FORM_HEADER) { |
| 199 isEmbeddedPage = true; | 163 isEmbeddedPage = true; |
| 200 break; | 164 break; |
| 201 } | 165 } |
| 202 } | 166 } |
| 203 } | 167 } |
| 204 if (!isEmbeddedPage && this.listener_) { | 168 if (!isEmbeddedPage) { |
| 205 this.listener_.onResize(currentUrl); | 169 this.dispatchEvent(new CustomEvent('resize', {detail: currentUrl})); |
| 206 return; | 170 return; |
| 207 } | 171 } |
| 208 } | 172 } |
| 209 | 173 |
| 210 if (currentUrl.lastIndexOf(this.idpOrigin_) == 0) { | 174 if (currentUrl.lastIndexOf(this.idpOrigin_) == 0) { |
| 211 this.webview_.contentWindow.postMessage({}, currentUrl); | 175 this.webview_.contentWindow.postMessage({}, currentUrl); |
| 212 } | 176 } |
| 213 | |
| 214 if (!this.loaded_) { | |
| 215 this.loaded_ = true; | |
| 216 if (this.listener_) { | |
| 217 this.listener_.onReady(); | |
| 218 } | |
| 219 } | |
| 220 }; | 177 }; |
| 221 | 178 |
| 222 /** | 179 /** |
| 223 * Invoked when headers are received in the main frame of the webview. It | 180 * Invoked when headers are received in the main frame of the webview. It |
| 224 * 1) reads the authenticated user info from a signin header, | 181 * 1) reads the authenticated user info from a signin header, |
| 225 * 2) signals the start of a saml flow upon receiving a saml header. | 182 * 2) signals the start of a saml flow upon receiving a saml header. |
| 226 * @return {!Object} Modified request headers. | 183 * @return {!Object} Modified request headers. |
| 227 * @private | 184 * @private |
| 228 */ | 185 */ |
| 229 Authenticator.prototype.onHeadersReceived_ = function(details) { | 186 Authenticator.prototype.onHeadersReceived_ = function(details) { |
| (...skipping 17 matching lines...) Expand all Loading... |
| 247 } | 204 } |
| 248 this.gaiaId_ = signinDetails['obfuscatedid'].slice(1, -1); | 205 this.gaiaId_ = signinDetails['obfuscatedid'].slice(1, -1); |
| 249 this.sessionIndex_ = signinDetails['sessionindex']; | 206 this.sessionIndex_ = signinDetails['sessionindex']; |
| 250 } else if (headerName == SAML_HEADER) { | 207 } else if (headerName == SAML_HEADER) { |
| 251 this.authFlow_ = AuthFlow.SAML; | 208 this.authFlow_ = AuthFlow.SAML; |
| 252 } | 209 } |
| 253 } | 210 } |
| 254 }; | 211 }; |
| 255 | 212 |
| 256 /** | 213 /** |
| 257 * Invoked when an HTML5 message is received. | 214 * Invoked when an HTML5 message is received from the webview element. |
| 258 * @param {object} e Payload of the received HTML5 message. | 215 * @param {object} e Payload of the received HTML5 message. |
| 259 * @private | 216 * @private |
| 260 */ | 217 */ |
| 261 Authenticator.prototype.onMessage_ = function(e) { | 218 Authenticator.prototype.onMessageFromWebview_ = function(e) { |
| 262 if (e.origin != this.idpOrigin_) { | 219 // The event origin does not have a trailing slash. |
| 220 if (e.origin != this.idpOrigin_.substring(0, this.idpOrigin_ - 1)) { |
| 263 return; | 221 return; |
| 264 } | 222 } |
| 265 | 223 |
| 266 var msg = e.data; | 224 var msg = e.data; |
| 267 | |
| 268 if (msg.method == 'attemptLogin') { | 225 if (msg.method == 'attemptLogin') { |
| 269 this.email_ = msg.email; | 226 this.email_ = msg.email; |
| 270 this.password_ = msg.password; | 227 this.password_ = msg.password; |
| 271 this.chooseWhatToSync_ = msg.chooseWhatToSync; | 228 this.chooseWhatToSync_ = msg.chooseWhatToSync; |
| 272 } | 229 } |
| 273 }; | 230 }; |
| 274 | 231 |
| 275 /** | 232 /** |
| 276 * Invoked to process authentication completion. | 233 * Invoked to process authentication completion. |
| 277 * @private | 234 * @private |
| 278 */ | 235 */ |
| 279 Authenticator.prototype.onAuthCompleted_ = function() { | 236 Authenticator.prototype.onAuthCompleted_ = function() { |
| 280 if (!this.listener_) { | |
| 281 return; | |
| 282 } | |
| 283 | |
| 284 if (!this.email_ && !this.skipForNow_) { | 237 if (!this.email_ && !this.skipForNow_) { |
| 285 this.webview_.src = this.initialFrameUrl_; | 238 this.webview_.src = this.initialFrameUrl_; |
| 286 return; | 239 return; |
| 287 } | 240 } |
| 288 | 241 |
| 289 this.listener_.onSuccess({email: this.email_, | 242 this.dispatchEvent( |
| 290 gaiaId: this.gaiaId_, | 243 new CustomEvent('authCompleted', |
| 291 password: this.password_, | 244 {detail: {email: this.email_, |
| 292 usingSAML: this.authFlow_ == AuthFlow.SAML, | 245 gaiaId: this.gaiaId_, |
| 293 chooseWhatToSync: this.chooseWhatToSync_, | 246 password: this.password_, |
| 294 skipForNow: this.skipForNow_, | 247 usingSAML: this.authFlow_ == AuthFlow.SAML, |
| 295 sessionIndex: this.sessionIndex_ || ''}); | 248 chooseWhatToSync: this.chooseWhatToSync_, |
| 249 skipForNow: this.skipForNow_, |
| 250 sessionIndex: this.sessionIndex_ || '', |
| 251 trusted: this.trusted_}})); |
| 296 }; | 252 }; |
| 297 | 253 |
| 298 /** | 254 /** |
| 299 * Invoked when the webview attempts to open a new window. | 255 * Invoked when the webview attempts to open a new window. |
| 300 * @private | 256 * @private |
| 301 */ | 257 */ |
| 302 Authenticator.prototype.onNewWindow_ = function(e) { | 258 Authenticator.prototype.onNewWindow_ = function(e) { |
| 303 if (!this.listener_) { | 259 this.dispatchEvent(new CustomEvent('newWindow', {detail: e})); |
| 304 return; | 260 }; |
| 261 |
| 262 /** |
| 263 * Invoked when the webview finishes loading a page. |
| 264 * @private |
| 265 */ |
| 266 Authenticator.prototype.onLoadStop_ = function(e) { |
| 267 if (!this.loaded_) { |
| 268 this.loaded_ = true; |
| 269 this.dispatchEvent(new Event('ready')); |
| 305 } | 270 } |
| 306 | |
| 307 this.listener_.onNewWindow(e); | |
| 308 }; | 271 }; |
| 309 | 272 |
| 310 Authenticator.AuthFlow = AuthFlow; | 273 Authenticator.AuthFlow = AuthFlow; |
| 311 Authenticator.AuthMode = AuthMode; | 274 Authenticator.AuthMode = AuthMode; |
| 312 | 275 |
| 313 return { | 276 return { |
| 314 // TODO(guohui, xiyuan): Rename GaiaAuthHost to Authenticator once the old | 277 // TODO(guohui, xiyuan): Rename GaiaAuthHost to Authenticator once the old |
| 315 // iframe-based flow is deprecated. | 278 // iframe-based flow is deprecated. |
| 316 GaiaAuthHost: Authenticator | 279 GaiaAuthHost: Authenticator |
| 317 }; | 280 }; |
| 318 }); | 281 }); |
| OLD | NEW |