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_); |
| 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]); |
| 176 return; |
| 177 } |
| 178 } |
| 179 |
| 180 if (currentUrl.lastIndexOf(this.idpOrigin_) == 0) { |
| 181 this.frame_.contentWindow.postMessage('', this.frame_.src); |
| 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'); |
| 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 |