Revert of Add button to add new FSP services to Files app. (patchset #8 id:140001...
[chromium-blink-merge.git] / chrome / browser / resources / gaia_auth_host / saml_handler.js
blob4065f79552ed35f5fa5865d9b91d67d028945b70
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.
5 <include src="post_message_channel.js">
7 /**
8 * @fileoverview Saml support for webview based auth.
9 */
11 cr.define('cr.login', function() {
12 'use strict';
14 /**
15 * The lowest version of the credentials passing API supported.
16 * @type {number}
18 var MIN_API_VERSION_VERSION = 1;
20 /**
21 * The highest version of the credentials passing API supported.
22 * @type {number}
24 var MAX_API_VERSION_VERSION = 1;
26 /**
27 * The key types supported by the credentials passing API.
28 * @type {Array} Array of strings.
30 var API_KEY_TYPES = [
31 'KEY_TYPE_PASSWORD_PLAIN',
34 /** @const */
35 var SAML_HEADER = 'google-accounts-saml';
37 /**
38 * The script to inject into webview and its sub frames.
39 * @type {string}
41 var injectedJs = String.raw`
42 <include src="webview_saml_injected.js">
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.
50 function stripParams(url) {
51 return url.substring(0, url.indexOf('?')) || url;
54 /**
55 * Extract domain name from an URL.
56 * @param {string} url An URL string.
57 * @return {string} The host name of the URL.
59 function extractDomain(url) {
60 var a = document.createElement('a');
61 a.href = url;
62 return a.hostname;
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
72 function SamlHandler(webview) {
73 /**
74 * The webview that serves IdP pages.
75 * @type {webview}
77 this.webview_ = webview;
79 /**
80 * Whether a Saml IdP page is display in the webview.
81 * @type {boolean}
83 this.isSamlPage_ = false;
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}
90 this.pendingIsSamlPage_ = false;
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}
98 this.abortedTopLevelUrl_ = null;
101 * The domain of the Saml IdP.
102 * @type {string}
104 this.authDomain = '';
107 * Scraped password stored in an id to password field value map.
108 * @type {Object.<string, string>}
109 * @private
111 this.passwordStore_ = {};
114 * Whether Saml API is initialized.
115 * @type {boolean}
117 this.apiInitialized_ = false;
120 * Saml API version to use.
121 * @type {number}
123 this.apiVersion_ = 0;
126 * Saml API token received.
127 * @type {string}
129 this.apiToken_ = null;
132 * Saml API password bytes.
133 * @type {string}
135 this.apiPasswordBytes_ = null;
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}
142 this.blockInsecureContent = false;
144 this.webview_.addEventListener(
145 'loadabort', this.onLoadAbort_.bind(this));
146 this.webview_.addEventListener(
147 'loadcommit', this.onLoadCommit_.bind(this));
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']);
158 PostMessageChannel.runAsDaemon(this.onConnected_.bind(this));
161 SamlHandler.prototype = {
162 __proto__: cr.EventTarget.prototype,
165 * Whether Saml API is used during auth.
166 * @return {boolean}
168 get samlApiUsed() {
169 return !!this.apiPasswordBytes_;
173 * Returns the Saml API password bytes.
174 * @return {string}
176 get apiPasswordBytes() {
177 return this.apiPasswordBytes_;
181 * Returns the number of scraped passwords.
182 * @return {number}
184 get scrapedPasswordCount() {
185 return this.getConsolidatedScrapedPasswords_().length;
189 * Gets the de-duped scraped passwords.
190 * @return {Array.<string>}
191 * @private
193 getConsolidatedScrapedPasswords_: function() {
194 var passwords = {};
195 for (var property in this.passwordStore_) {
196 passwords[this.passwordStore_[property]] = true;
198 return Object.keys(passwords);
202 * Resets all auth states
204 reset: function() {
205 this.isSamlPage_ = false;
206 this.pendingIsSamlPage_ = false;
207 this.passwordStore_ = {};
209 this.apiInitialized_ = false;
210 this.apiVersion_ = 0;
211 this.apiToken_ = null;
212 this.apiPasswordBytes_ = null;
216 * Check whether the given |password| is in the scraped passwords.
217 * @return {boolean} True if the |password| is found.
219 verifyConfirmedPassword: function(password) {
220 return this.getConsolidatedScrapedPasswords_().indexOf(password) >= 0;
224 * Injects JS code to all frames.
225 * @private
227 injectJs_: function() {
228 if (!injectedJs)
229 return;
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));
242 * Invoked on the webview's loadabort event.
243 * @private
245 onLoadAbort_: function(e) {
246 if (e.isTopLevel)
247 this.abortedTopLevelUrl_ = e.url;
251 * Invoked on the webview's loadcommit event for both main and sub frames.
252 * @private
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;
261 this.isSamlPage_ = this.pendingIsSamlPage_;
262 this.injectJs_();
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.
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};
283 * Invoked when headers are received for the main frame.
284 * @private
286 onHeadersReceived_: function(details) {
287 var headers = details.responseHeaders;
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();
295 if (headerName == SAML_HEADER) {
296 var action = header.value.toLowerCase();
297 if (action == 'start') {
298 this.pendingIsSamlPage_ = true;
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;
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:
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).
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]);
342 return {responseHeaders: otherHeaders.concat(cookies)};
347 return {};
351 * Invoked when the injected JS makes a connection.
353 onConnected_: function(port) {
354 if (port.targetWindow != this.webview_.contentWindow)
355 return;
357 var channel = Channel.create();
358 channel.init(port);
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));
370 sendInitializationSuccess_: function(channel) {
371 channel.send({name: 'apiResponse', response: {
372 result: 'initialized',
373 version: this.apiVersion_,
374 keyTypes: API_KEY_TYPES
375 }});
378 sendInitializationFailure_: function(channel) {
379 channel.send({
380 name: 'apiResponse',
381 response: {result: 'initialization_failed'}
386 * Handlers for channel messages.
387 * @param {Channel} channel A channel to send back response.
388 * @param {Object} msg Received message.
389 * @private
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;
400 this.apiVersion_ = Math.min(call.requestedVersion,
401 MAX_API_VERSION_VERSION);
402 this.apiInitialized_ = true;
403 this.sendInitializationSuccess_(channel);
404 return;
407 if (call.method == 'add') {
408 if (API_KEY_TYPES.indexOf(call.keyType) == -1) {
409 console.error('SamlHandler.onAPICall_: unsupported key type');
410 return;
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');
424 onUpdatePassword_: function(channel, msg) {
425 if (this.isSamlPage_)
426 this.passwordStore_[msg.id] = msg.password;
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}}));
438 onGetSAMLFlag_: function(channel, msg) {
439 return this.isSamlPage_;
444 * Sets the saml injected JS code.
445 * @param {string} samlInjectedJs JS code to inejct for Saml.
447 SamlHandler.setSamlInjectedJs = function(samlInjectedJs) {
448 injectedJs = samlInjectedJs;
451 return {
452 SamlHandler: SamlHandler