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