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