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">
8 * @fileoverview Saml support for webview based auth.
11 cr
.define('cr.login', function() {
15 * The lowest version of the credentials passing API supported.
18 var MIN_API_VERSION_VERSION
= 1;
21 * The highest version of the credentials passing API supported.
24 var MAX_API_VERSION_VERSION
= 1;
27 * The key types supported by the credentials passing API.
28 * @type {Array} Array of strings.
31 'KEY_TYPE_PASSWORD_PLAIN',
35 var SAML_HEADER
= 'google-accounts-saml';
38 * The script to inject into webview and its sub frames.
41 var injectedJs
= String
.raw
`
42 <include src="webview_saml_injected.js">
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
;
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');
66 * A handler to provide saml support for the given webview that hosts the
68 * @extends {cr.EventTarget}
69 * @param {webview} webview
72 function SamlHandler(webview
) {
74 * The webview that serves IdP pages.
77 this.webview_
= webview
;
80 * Whether a Saml IdP page is display in the webview.
83 this.isSamlPage_
= false;
86 * Pending Saml IdP page flag that is set when a SAML_HEADER is received
87 * and is copied to |isSamlPage_| in loadcommit.
90 this.pendingIsSamlPage_
= false;
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
98 this.abortedTopLevelUrl_
= null;
101 * The domain of the Saml IdP.
104 this.authDomain
= '';
107 * Scraped password stored in an id to password field value map.
108 * @type {Object<string, string>}
111 this.passwordStore_
= {};
114 * Whether Saml API is initialized.
117 this.apiInitialized_
= false;
120 * Saml API version to use.
123 this.apiVersion_
= 0;
126 * Saml API token received.
129 this.apiToken_
= null;
132 * Saml API password bytes.
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.
142 this.blockInsecureContent
= false;
144 this.webview_
.addEventListener(
145 'contentload', this.onContentLoad_
.bind(this));
146 this.webview_
.addEventListener(
147 'loadabort', this.onLoadAbort_
.bind(this));
148 this.webview_
.addEventListener(
149 'loadcommit', this.onLoadCommit_
.bind(this));
151 this.webview_
.request
.onBeforeRequest
.addListener(
152 this.onInsecureRequest
.bind(this),
153 {urls
: ['http://*/*', 'file://*/*', 'ftp://*/*']},
155 this.webview_
.request
.onHeadersReceived
.addListener(
156 this.onHeadersReceived_
.bind(this),
157 {urls
: ['<all_urls>'], types
: ['main_frame', 'xmlhttprequest']},
158 ['blocking', 'responseHeaders']);
160 this.webview_
.addContentScripts([{
161 name
: 'samlInjected',
162 matches
: ['http://*/*', 'https://*/*'],
167 run_at
: 'document_start'
170 PostMessageChannel
.runAsDaemon(this.onConnected_
.bind(this));
173 SamlHandler
.prototype = {
174 __proto__
: cr
.EventTarget
.prototype,
177 * Whether Saml API is used during auth.
181 return !!this.apiPasswordBytes_
;
185 * Returns the Saml API password bytes.
188 get apiPasswordBytes() {
189 return this.apiPasswordBytes_
;
193 * Returns the number of scraped passwords.
196 get scrapedPasswordCount() {
197 return this.getConsolidatedScrapedPasswords_().length
;
201 * Gets the de-duped scraped passwords.
202 * @return {Array<string>}
205 getConsolidatedScrapedPasswords_: function() {
207 for (var property
in this.passwordStore_
) {
208 passwords
[this.passwordStore_
[property
]] = true;
210 return Object
.keys(passwords
);
214 * Resets all auth states
217 this.isSamlPage_
= false;
218 this.pendingIsSamlPage_
= false;
219 this.passwordStore_
= {};
221 this.apiInitialized_
= false;
222 this.apiVersion_
= 0;
223 this.apiToken_
= null;
224 this.apiPasswordBytes_
= null;
228 * Check whether the given |password| is in the scraped passwords.
229 * @return {boolean} True if the |password| is found.
231 verifyConfirmedPassword: function(password
) {
232 return this.getConsolidatedScrapedPasswords_().indexOf(password
) >= 0;
236 * Invoked on the webview's contentload event.
239 onContentLoad_: function(e
) {
240 PostMessageChannel
.init(this.webview_
.contentWindow
);
244 * Invoked on the webview's loadabort event.
247 onLoadAbort_: function(e
) {
249 this.abortedTopLevelUrl_
= e
.url
;
253 * Invoked on the webview's loadcommit event for both main and sub frames.
256 onLoadCommit_: function(e
) {
257 // Skip this loadcommit if the top level load is just aborted.
258 if (e
.isTopLevel
&& e
.url
=== this.abortedTopLevelUrl_
) {
259 this.abortedTopLevelUrl_
= null;
263 // Skip for none http/https url.
264 if (e
.url
.indexOf('https://') != 0 &&
265 e
.url
.indexOf('http://') != 0) {
269 this.isSamlPage_
= this.pendingIsSamlPage_
;
273 * Handler for webRequest.onBeforeRequest, invoked when content served over
274 * an unencrypted connection is detected. Determines whether the request
275 * should be blocked and if so, signals that an error message needs to be
277 * @param {Object} details
278 * @return {!Object} Decision whether to block the request.
280 onInsecureRequest: function(details
) {
281 if (!this.blockInsecureContent
)
283 var strippedUrl
= stripParams(details
.url
);
284 this.dispatchEvent(new CustomEvent('insecureContentBlocked',
285 {detail
: {url
: strippedUrl
}}));
286 return {cancel
: true};
290 * Invoked when headers are received for the main frame.
293 onHeadersReceived_: function(details
) {
294 var headers
= details
.responseHeaders
;
296 // Check whether GAIA headers indicating the start or end of a SAML
297 // redirect are present. If so, synthesize cookies to mark these points.
298 for (var i
= 0; headers
&& i
< headers
.length
; ++i
) {
299 var header
= headers
[i
];
300 var headerName
= header
.name
.toLowerCase();
302 if (headerName
== SAML_HEADER
) {
303 var action
= header
.value
.toLowerCase();
304 if (action
== 'start') {
305 this.pendingIsSamlPage_
= true;
307 // GAIA is redirecting to a SAML IdP. Any cookies contained in the
308 // current |headers| were set by GAIA. Any cookies set in future
309 // requests will be coming from the IdP. Append a cookie to the
310 // current |headers| that marks the point at which the redirect
312 headers
.push({name
: 'Set-Cookie',
313 value
: 'google-accounts-saml-start=now'});
314 return {responseHeaders
: headers
};
315 } else if (action
== 'end') {
316 this.pendingIsSamlPage_
= false;
318 // The SAML IdP has redirected back to GAIA. Add a cookie that marks
319 // the point at which the redirect occurred occurred. It is
320 // important that this cookie be prepended to the current |headers|
321 // because any cookies contained in the |headers| were already set
322 // by GAIA, not the IdP. Due to limitations in the webRequest API,
323 // it is not trivial to prepend a cookie:
325 // The webRequest API only allows for deleting and appending
326 // headers. To prepend a cookie (C), three steps are needed:
327 // 1) Delete any headers that set cookies (e.g., A, B).
328 // 2) Append a header which sets the cookie (C).
329 // 3) Append the original headers (A, B).
331 // Due to a further limitation of the webRequest API, it is not
332 // possible to delete a header in step 1) and append an identical
333 // header in step 3). To work around this, a trailing semicolon is
334 // added to each header before appending it. Trailing semicolons are
335 // ignored by Chrome in cookie headers, causing the modified headers
336 // to actually set the original cookies.
337 var otherHeaders
= [];
338 var cookies
= [{name
: 'Set-Cookie',
339 value
: 'google-accounts-saml-end=now'}];
340 for (var j
= 0; j
< headers
.length
; ++j
) {
341 if (headers
[j
].name
.toLowerCase().indexOf('set-cookie') == 0) {
342 var header
= headers
[j
];
344 cookies
.push(header
);
346 otherHeaders
.push(headers
[j
]);
349 return {responseHeaders
: otherHeaders
.concat(cookies
)};
358 * Invoked when the injected JS makes a connection.
360 onConnected_: function(port
) {
361 if (port
.targetWindow
!= this.webview_
.contentWindow
)
364 var channel
= Channel
.create();
367 channel
.registerMessage(
368 'apiCall', this.onAPICall_
.bind(this, channel
));
369 channel
.registerMessage(
370 'updatePassword', this.onUpdatePassword_
.bind(this, channel
));
371 channel
.registerMessage(
372 'pageLoaded', this.onPageLoaded_
.bind(this, channel
));
373 channel
.registerMessage(
374 'getSAMLFlag', this.onGetSAMLFlag_
.bind(this, channel
));
377 sendInitializationSuccess_: function(channel
) {
378 channel
.send({name
: 'apiResponse', response
: {
379 result
: 'initialized',
380 version
: this.apiVersion_
,
381 keyTypes
: API_KEY_TYPES
385 sendInitializationFailure_: function(channel
) {
388 response
: {result
: 'initialization_failed'}
393 * Handlers for channel messages.
394 * @param {Channel} channel A channel to send back response.
395 * @param {Object} msg Received message.
398 onAPICall_: function(channel
, msg
) {
400 if (call
.method
== 'initialize') {
401 if (!Number
.isInteger(call
.requestedVersion
) ||
402 call
.requestedVersion
< MIN_API_VERSION_VERSION
) {
403 this.sendInitializationFailure_(channel
);
407 this.apiVersion_
= Math
.min(call
.requestedVersion
,
408 MAX_API_VERSION_VERSION
);
409 this.apiInitialized_
= true;
410 this.sendInitializationSuccess_(channel
);
414 if (call
.method
== 'add') {
415 if (API_KEY_TYPES
.indexOf(call
.keyType
) == -1) {
416 console
.error('SamlHandler.onAPICall_: unsupported key type');
419 // Not setting |email_| and |gaiaId_| because this API call will
420 // eventually be followed by onCompleteLogin_() which does set it.
421 this.apiToken_
= call
.token
;
422 this.apiPasswordBytes_
= call
.passwordBytes
;
423 } else if (call
.method
== 'confirm') {
424 if (call
.token
!= this.apiToken_
)
425 console
.error('SamlHandler.onAPICall_: token mismatch');
427 console
.error('SamlHandler.onAPICall_: unknown message');
431 onUpdatePassword_: function(channel
, msg
) {
432 if (this.isSamlPage_
)
433 this.passwordStore_
[msg
.id
] = msg
.password
;
436 onPageLoaded_: function(channel
, msg
) {
437 this.authDomain
= extractDomain(msg
.url
);
438 this.dispatchEvent(new CustomEvent(
441 isSAMLPage
: this.isSamlPage_
,
442 domain
: this.authDomain
}}));
445 onGetSAMLFlag_: function(channel
, msg
) {
446 return this.isSamlPage_
;
451 SamlHandler
: SamlHandler