Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(49)

Side by Side Diff: chrome/browser/resources/gaia_auth_host/saml_handler.js

Issue 1004753004: cros: Port SAML support to webview sign-in. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: handle loadabort to fix test Created 5 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright 2015 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 <include src="post_message_channel.js">
6
7 /**
8 * @fileoverview Saml support for webview based auth.
9 */
10
11 cr.define('cr.login', function() {
12 'use strict';
13
14 /**
15 * The lowest version of the credentials passing API supported.
16 * @type {number}
17 */
18 var MIN_API_VERSION_VERSION = 1;
19
20 /**
21 * The highest version of the credentials passing API supported.
22 * @type {number}
23 */
24 var MAX_API_VERSION_VERSION = 1;
25
26 /**
27 * The key types supported by the credentials passing API.
28 * @type {Array} Array of strings.
29 */
30 var API_KEY_TYPES = [
31 'KEY_TYPE_PASSWORD_PLAIN',
32 ];
33
34 /** @const */
35 var SAML_HEADER = 'google-accounts-saml';
36
37 /**
38 * The script to inject into webview and its sub frames.
39 * @type {string}
40 */
41 var injectedJs = String.raw`
42 <include src="webview_saml_injected.js">
43 `;
44
45 /**
46 * Creates a new URL by striping all query parameters.
47 * @param {string} url The original URL.
48 * @return {string} The new URL with all query parameters stripped.
49 */
50 function stripParams(url) {
51 return url.substring(0, url.indexOf('?')) || url;
52 }
53
54 /**
55 * Extract domain name from an URL.
56 * @param {string} url An URL string.
57 * @return {string} The host name of the URL.
58 */
59 function extractDomain(url) {
60 var a = document.createElement('a');
61 a.href = url;
62 return a.hostname;
63 }
64
65 /**
66 * A handler to provide saml support for the given webview that hosts the
67 * auth IdP pages.
68 * @extends {cr.EventTarget}
69 * @param {webview} webview
70 * @constructor
71 */
72 function SamlHandler(webview) {
73 /**
74 * The webview that serves IdP pages.
75 * @type {webview}
76 */
77 this.webview_ = webview;
78
79 /**
80 * Whether a Saml IdP page is display in the webview.
81 * @type {boolean}
82 */
83 this.isSamlPage_ = false;
84
85 /**
86 * Pending Saml IdP page flag that is set when a SAML_HEADER is received
87 * and is copied to |isSamlPage_| in loadcommit.
88 * @type {boolean}
89 */
90 this.pendingIsSamlPage_ = false;
91
92 /**
93 * The last aborted top level url. It is recorded in loadabort event and
94 * used to skip injection into Chrome's error page in the following
95 * loadcommit event.
96 * @type {string}
97 */
98 this.abortedTopLevelUrl_ = null;
99
100 /**
101 * The domain of the Saml IdP.
102 * @type {string}
103 */
104 this.authDomain = '';
105
106 /**
107 * Scraped password stored in an id to password field value map.
108 * @type {Object.<string, string>}
109 * @private
110 */
111 this.passwordStore_ = {};
112
113 /**
114 * Whether Saml API is initialized.
115 * @type {boolean}
116 */
117 this.apiInitialized_ = false;
118
119 /**
120 * Saml API version to use.
121 * @type {number}
122 */
123 this.apiVersion_ = 0;
124
125 /**
126 * Saml API token received.
127 * @type {string}
128 */
129 this.apiToken_ = null;
130
131 /**
132 * Saml API password bytes.
133 * @type {string}
134 */
135 this.apiPasswordBytes_ = null;
136
137 /*
138 * Whether to abort the authentication flow and show an error messagen when
139 * content served over an unencrypted connection is detected.
140 * @type {boolean}
141 */
142 this.blockInsecureContent = false;
143
144 this.webview_.addEventListener(
145 'loadabort', this.onLoadAbort_.bind(this));
146 this.webview_.addEventListener(
147 'loadcommit', this.onLoadCommit_.bind(this));
148
149 this.webview_.request.onBeforeRequest.addListener(
150 this.onInsecureRequest.bind(this),
151 {urls: ['http://*/*', 'file://*/*', 'ftp://*/*']},
152 ['blocking']);
153 this.webview_.request.onHeadersReceived.addListener(
154 this.onHeadersReceived_.bind(this),
155 {urls: ['<all_urls>'], types: ['main_frame', 'xmlhttprequest']},
156 ['blocking', 'responseHeaders']);
157
158 PostMessageChannel.runAsDaemon(this.onConnected_.bind(this));
159 }
160
161 SamlHandler.prototype = {
162 __proto__: cr.EventTarget.prototype,
163
164 /**
165 * Whether Saml API is used during auth.
166 * @return {boolean}
167 */
168 get samlApiUsed() {
169 return !!this.apiPasswordBytes_;
170 },
171
172 /**
173 * Returns the Saml API password bytes.
174 * @return {string}
175 */
176 get apiPasswordBytes() {
177 return this.apiPasswordBytes_;
178 },
179
180 /**
181 * Returns the number of scraped passwords.
182 * @return {number}
183 */
184 get scrapedPasswordCount() {
185 return this.getConsolidatedScrapedPasswords_().length;
186 },
187
188 /**
189 * Gets the de-duped scraped passwords.
190 * @return {Array.<string>}
191 * @private
192 */
193 getConsolidatedScrapedPasswords_: function() {
194 var passwords = {};
195 for (var property in this.passwordStore_) {
196 passwords[this.passwordStore_[property]] = true;
197 }
198 return Object.keys(passwords);
199 },
200
201 /**
202 * Resets all auth states
203 */
204 reset: function() {
205 this.isSamlPage_ = false;
206 this.pendingIsSamlPage_ = false;
207 this.passwordStore_ = {};
208
209 this.apiInitialized_ = false;
210 this.apiVersion_ = 0;
211 this.apiToken_ = null;
212 this.apiPasswordBytes_ = null;
213 },
214
215 /**
216 * Check whether the given |password| is in the scraped passwords.
217 * @return {boolean} True if the |password| is found.
218 */
219 verifyConfirmedPassword: function(password) {
220 return this.getConsolidatedScrapedPasswords_().indexOf(password) >= 0;
221 },
222
223 /**
224 * Injects JS code to all frames.
225 * @private
226 */
227 injectJs_: function() {
228 if (!injectedJs)
229 return;
230
231 // TODO(xiyuan): Replace this with webview.addContentScript.
232 this.webview_.executeScript({
233 code: injectedJs,
234 allFrames: true,
235 runAt: 'document_start'
236 }, (function() {
237 PostMessageChannel.init(this.webview_.contentWindow);
238 }).bind(this));
239 },
240
241 /**
242 * Invoked on the webview's loadabort event.
243 * @private
244 */
245 onLoadAbort_: function(e) {
246 if (e.isTopLevel)
247 this.abortedTopLevelUrl_ = e.url;
248 },
249
250 /**
251 * Invoked on the webview's loadcommit event for both main and sub frames.
252 * @private
253 */
254 onLoadCommit_: function(e) {
255 // Skip this loadcommit if the top level load is just aborted.
256 if (e.isTopLevel && e.url === this.abortedTopLevelUrl_) {
257 this.abortedTopLevelUrl_ = null;
258 return;
259 }
260
261 this.isSamlPage_ = this.pendingIsSamlPage_;
262 this.injectJs_();
263 },
264
265 /**
266 * Handler for webRequest.onBeforeRequest, invoked when content served over
267 * an unencrypted connection is detected. Determines whether the request
268 * should be blocked and if so, signals that an error message needs to be
269 * shown.
270 * @param {Object} details
271 * @return {!Object} Decision whether to block the request.
272 */
273 onInsecureRequest: function(details) {
274 if (!this.blockInsecureContent)
275 return {};
276 var strippedUrl = stripParams(details.url);
277 this.dispatchEvent(new CustomEvent('insecureContentBlocked',
278 {detail: {url: strippedUrl}}));
279 return {cancel: true};
280 },
281
282 /**
283 * Invoked when headers are received for the main frame.
284 * @private
285 */
286 onHeadersReceived_: function(details) {
287 var headers = details.responseHeaders;
288
289 // Check whether GAIA headers indicating the start or end of a SAML
290 // redirect are present. If so, synthesize cookies to mark these points.
291 for (var i = 0; headers && i < headers.length; ++i) {
292 var header = headers[i];
293 var headerName = header.name.toLowerCase();
294
295 if (headerName == SAML_HEADER) {
296 var action = header.value.toLowerCase();
297 if (action == 'start') {
298 this.pendingIsSamlPage_ = true;
299
300 // GAIA is redirecting to a SAML IdP. Any cookies contained in the
301 // current |headers| were set by GAIA. Any cookies set in future
302 // requests will be coming from the IdP. Append a cookie to the
303 // current |headers| that marks the point at which the redirect
304 // occurred.
305 headers.push({name: 'Set-Cookie',
306 value: 'google-accounts-saml-start=now'});
307 return {responseHeaders: headers};
308 } else if (action == 'end') {
309 this.pendingIsSamlPage_ = false;
310
311 // The SAML IdP has redirected back to GAIA. Add a cookie that marks
312 // the point at which the redirect occurred occurred. It is
313 // important that this cookie be prepended to the current |headers|
314 // because any cookies contained in the |headers| were already set
315 // by GAIA, not the IdP. Due to limitations in the webRequest API,
316 // it is not trivial to prepend a cookie:
317 //
318 // The webRequest API only allows for deleting and appending
319 // headers. To prepend a cookie (C), three steps are needed:
320 // 1) Delete any headers that set cookies (e.g., A, B).
321 // 2) Append a header which sets the cookie (C).
322 // 3) Append the original headers (A, B).
323 //
324 // Due to a further limitation of the webRequest API, it is not
325 // possible to delete a header in step 1) and append an identical
326 // header in step 3). To work around this, a trailing semicolon is
327 // added to each header before appending it. Trailing semicolons are
328 // ignored by Chrome in cookie headers, causing the modified headers
329 // to actually set the original cookies.
330 var otherHeaders = [];
331 var cookies = [{name: 'Set-Cookie',
332 value: 'google-accounts-saml-end=now'}];
333 for (var j = 0; j < headers.length; ++j) {
334 if (headers[j].name.toLowerCase().indexOf('set-cookie') == 0) {
335 var header = headers[j];
336 header.value += ';';
337 cookies.push(header);
338 } else {
339 otherHeaders.push(headers[j]);
340 }
341 }
342 return {responseHeaders: otherHeaders.concat(cookies)};
343 }
344 }
345 }
346
347 return {};
348 },
349
350 /**
351 * Invoked when the injected JS makes a connection.
352 */
353 onConnected_: function(port) {
354 if (port.targetWindow != this.webview_.contentWindow)
355 return;
356
357 var channel = Channel.create();
358 channel.init(port);
359
360 channel.registerMessage(
361 'apiCall', this.onAPICall_.bind(this, channel));
362 channel.registerMessage(
363 'updatePassword', this.onUpdatePassword_.bind(this, channel));
364 channel.registerMessage(
365 'pageLoaded', this.onPageLoaded_.bind(this, channel));
366 channel.registerMessage(
367 'getSAMLFlag', this.onGetSAMLFlag_.bind(this, channel));
368 },
369
370 sendInitializationSuccess_: function(channel) {
371 channel.send({name: 'apiResponse', response: {
372 result: 'initialized',
373 version: this.apiVersion_,
374 keyTypes: API_KEY_TYPES
375 }});
376 },
377
378 sendInitializationFailure_: function(channel) {
379 channel.send({
380 name: 'apiResponse',
381 response: {result: 'initialization_failed'}
382 });
383 },
384
385 /**
386 * Handlers for channel messages.
387 * @param {Channel} channel A channel to send back response.
388 * @param {Object} msg Received message.
389 * @private
390 */
391 onAPICall_: function(channel, msg) {
392 var call = msg.call;
393 if (call.method == 'initialize') {
394 if (!Number.isInteger(call.requestedVersion) ||
395 call.requestedVersion < MIN_API_VERSION_VERSION) {
396 this.sendInitializationFailure_(channel);
397 return;
398 }
399
400 this.apiVersion_ = Math.min(call.requestedVersion,
401 MAX_API_VERSION_VERSION);
402 this.apiInitialized_ = true;
403 this.sendInitializationSuccess_(channel);
404 return;
405 }
406
407 if (call.method == 'add') {
408 if (API_KEY_TYPES.indexOf(call.keyType) == -1) {
409 console.error('SamlHandler.onAPICall_: unsupported key type');
410 return;
411 }
412 // Not setting |email_| and |gaiaId_| because this API call will
413 // eventually be followed by onCompleteLogin_() which does set it.
414 this.apiToken_ = call.token;
415 this.apiPasswordBytes_ = call.passwordBytes;
416 } else if (call.method == 'confirm') {
417 if (call.token != this.apiToken_)
418 console.error('SamlHandler.onAPICall_: token mismatch');
419 } else {
420 console.error('SamlHandler.onAPICall_: unknown message');
421 }
422 },
423
424 onUpdatePassword_: function(channel, msg) {
425 if (this.isSamlPage_)
426 this.passwordStore_[msg.id] = msg.password;
427 },
428
429 onPageLoaded_: function(channel, msg) {
430 this.authDomain = extractDomain(msg.url);
431 this.dispatchEvent(new CustomEvent(
432 'authPageLoaded',
433 {detail: {url: url,
434 isSAMLPage: this.isSamlPage_,
435 domain: this.authDomain}}));
436 },
437
438 onGetSAMLFlag_: function(channel, msg) {
439 return this.isSamlPage_;
440 },
441 };
442
443 /**
444 * Sets the saml injected JS code.
445 * @param {string} samlInjectedJs JS code to inejct for Saml.
446 */
447 SamlHandler.setSamlInjectedJs = function(samlInjectedJs) {
448 injectedJs = samlInjectedJs;
449 };
450
451 return {
452 SamlHandler: SamlHandler
453 };
454 });
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698