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 |