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 |