ozone: evdev: Sync caps lock LED state to evdev
[chromium-blink-merge.git] / chrome / browser / resources / gaia_auth_host / authenticator.js
blobdb85d62bb75a08eea9ed94f7238b52e0d3b7a81d
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.
5 /**
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
8  * authentication events should pass a listener object of type
9  * cr.login.GaiaAuthHost.Listener as defined in this file. After initialization,
10  * call {@code load} to start the authentication flow.
11  */
12 cr.define('cr.login', function() {
13   'use strict';
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.
18   var IDP_ORIGIN = 'https://accounts.google.com/';
19   var IDP_PATH = 'ServiceLogin?skipvpage=true&sarp=1&rm=hide';
20   var CONTINUE_URL =
21       'chrome-extension://mfffpogegjflfpflabcdkioaeobkgjik/success.html';
22   var SIGN_IN_HEADER = 'google-accounts-signin';
23   var EMBEDDED_FORM_HEADER = 'google-accounts-embedded';
24   var SAML_HEADER = 'google-accounts-saml';
25   var LOCATION_HEADER = 'location';
26   var SET_COOKIE_HEADER = 'set-cookie';
27   var OAUTH_CODE_COOKIE = 'oauth_code';
28   var SERVICE_ID = 'chromeoslogin';
30   /**
31    * The source URL parameter for the constrained signin flow.
32    */
33   var CONSTRAINED_FLOW_SOURCE = 'chrome';
35   /**
36    * Enum for the authorization mode, must match AuthMode defined in
37    * chrome/browser/ui/webui/inline_login_ui.cc.
38    * @enum {number}
39    */
40   var AuthMode = {
41     DEFAULT: 0,
42     OFFLINE: 1,
43     DESKTOP: 2
44   };
46   /**
47    * Enum for the authorization type.
48    * @enum {number}
49    */
50   var AuthFlow = {
51     DEFAULT: 0,
52     SAML: 1
53   };
55   /**
56    * Supported Authenticator params.
57    * @type {!Array<string>}
58    * @const
59    */
60   var SUPPORTED_PARAMS = [
61     'gaiaUrl',       // Gaia url to use;
62     'gaiaPath',      // Gaia path to use without a leading slash;
63     'hl',            // Language code for the user interface;
64     'email',         // Pre-fill the email field in Gaia UI;
65     'service',       // Name of Gaia service;
66     'continueUrl',   // Continue url to use;
67     'frameUrl',      // Initial frame URL to use. If empty defaults to gaiaUrl.
68     'constrained',   // Whether the extension is loaded in a constrained window;
69     'clientId'       // Chrome client id;
70   ];
72   /**
73    * Initializes the authenticator component.
74    * @param {webview|string} webview The webview element or its ID to host IdP
75    *     web pages.
76    * @constructor
77    */
78   function Authenticator(webview) {
79     this.webview_ = typeof webview == 'string' ? $(webview) : webview;
80     assert(this.webview_);
82     this.email_ = null;
83     this.password_ = null;
84     this.gaiaId_ = null,
85     this.sessionIndex_ = null;
86     this.chooseWhatToSync_ = false;
87     this.skipForNow_ = false;
88     this.authFlow_ = AuthFlow.DEFAULT;
89     this.loaded_ = false;
90     this.idpOrigin_ = null;
91     this.continueUrl_ = null;
92     this.continueUrlWithoutParams_ = null;
93     this.initialFrameUrl_ = null;
94     this.reloadUrl_ = null;
95     this.trusted_ = true;
96     this.oauth_code_ = null;
98     this.webview_.addEventListener('droplink', this.onDropLink_.bind(this));
99     this.webview_.addEventListener(
100         'newwindow', this.onNewWindow_.bind(this));
101     this.webview_.addEventListener(
102         'contentload', this.onContentLoad_.bind(this));
103     this.webview_.addEventListener(
104         'loadstop', this.onLoadStop_.bind(this));
105     this.webview_.addEventListener(
106         'loadcommit', this.onLoadCommit_.bind(this));
107     this.webview_.request.onCompleted.addListener(
108         this.onRequestCompleted_.bind(this),
109         {urls: ['<all_urls>'], types: ['main_frame']},
110         ['responseHeaders']);
111     this.webview_.request.onHeadersReceived.addListener(
112         this.onHeadersReceived_.bind(this),
113         {urls: ['<all_urls>'], types: ['main_frame', 'xmlhttprequest']},
114         ['responseHeaders']);
115     window.addEventListener(
116         'message', this.onMessageFromWebview_.bind(this), false);
117     window.addEventListener(
118         'focus', this.onFocus_.bind(this), false);
119     window.addEventListener(
120         'popstate', this.onPopState_.bind(this), false);
121   }
123   // TODO(guohui,xiyuan): no need to inherit EventTarget once we deprecate the
124   // old event-based signin flow.
125   Authenticator.prototype = Object.create(cr.EventTarget.prototype);
127   /**
128    * Loads the authenticator component with the given parameters.
129    * @param {AuthMode} authMode Authorization mode.
130    * @param {Object} data Parameters for the authorization flow.
131    */
132   Authenticator.prototype.load = function(authMode, data) {
133     this.idpOrigin_ = data.gaiaUrl || IDP_ORIGIN;
134     this.continueUrl_ = data.continueUrl || CONTINUE_URL;
135     this.continueUrlWithoutParams_ =
136         this.continueUrl_.substring(0, this.continueUrl_.indexOf('?')) ||
137         this.continueUrl_;
138     this.isConstrainedWindow_ = data.constrained == '1';
139     this.isMinuteMaidChromeOS = data.isMinuteMaidChromeOS;
141     this.initialFrameUrl_ = this.constructInitialFrameUrl_(data);
142     this.reloadUrl_ = data.frameUrl || this.initialFrameUrl_;
143     this.authFlow_ = AuthFlow.DEFAULT;
145     this.webview_.src = this.reloadUrl_;
147     this.loaded_ = false;
148   };
150   /**
151    * Reloads the authenticator component.
152    */
153   Authenticator.prototype.reload = function() {
154     this.webview_.src = this.reloadUrl_;
155     this.authFlow_ = AuthFlow.DEFAULT;
156     this.loaded_ = false;
157   };
159   /**
160    * Send message 'focusready' to Gaia so it sets focus on default input.
161    */
162   Authenticator.prototype.sendFocusReady = function() {
163     var currentUrl = this.webview_.src;
164     if (currentUrl.lastIndexOf(this.idpOrigin_) == 0) {
165       var msg = {
166         'method': 'focusready'
167       };
168       // TODO(rsorokin): Get rid of this check once issue crbug.com/456118 is
169       // fixed.
170       if (this.webview_.contentWindow) {
171         this.webview_.contentWindow.postMessage(msg, currentUrl);
172       }
173     }
174   };
176   Authenticator.prototype.constructInitialFrameUrl_ = function(data) {
177     var url = this.idpOrigin_ + (data.gaiaPath || IDP_PATH);
179     if (this.isMinuteMaidChromeOS) {
180       if (data.chromeType)
181         url = appendParam(url, 'chrometype', data.chromeType);
182       if (data.clientId)
183         url = appendParam(url, 'client_id', data.clientId);
184       if (data.enterpriseDomain)
185         url = appendParam(url, 'managedomain', data.enterpriseDomain);
186     } else {
187       url = appendParam(url, 'continue', this.continueUrl_);
188       url = appendParam(url, 'service', data.service || SERVICE_ID);
189     }
190     if (data.hl)
191       url = appendParam(url, 'hl', data.hl);
192     if (data.email)
193       url = appendParam(url, 'Email', data.email);
194     if (this.isConstrainedWindow_)
195       url = appendParam(url, 'source', CONSTRAINED_FLOW_SOURCE);
196     return url;
197   };
199   /**
200    * Invoked when a main frame request in the webview has completed.
201    * @private
202    */
203   Authenticator.prototype.onRequestCompleted_ = function(details) {
204     var currentUrl = details.url;
206     if (currentUrl.lastIndexOf(this.continueUrlWithoutParams_, 0) == 0) {
207       if (currentUrl.indexOf('ntp=1') >= 0)
208         this.skipForNow_ = true;
210       this.onAuthCompleted_();
211       return;
212     }
214     if (currentUrl.indexOf('https') != 0)
215       this.trusted_ = false;
217     if (this.isConstrainedWindow_) {
218       var isEmbeddedPage = false;
219       if (this.idpOrigin_ && currentUrl.lastIndexOf(this.idpOrigin_) == 0) {
220         var headers = details.responseHeaders;
221         for (var i = 0; headers && i < headers.length; ++i) {
222           if (headers[i].name.toLowerCase() == EMBEDDED_FORM_HEADER) {
223             isEmbeddedPage = true;
224             break;
225           }
226         }
227       }
228       if (!isEmbeddedPage) {
229         this.dispatchEvent(new CustomEvent('resize', {detail: currentUrl}));
230         return;
231       }
232     }
234     this.updateHistoryState_(currentUrl);
236   };
238   /**
239     * Manually updates the history. Invoked upon completion of a webview
240     * navigation.
241     * @param {string} url Request URL.
242     * @private
243     */
244   Authenticator.prototype.updateHistoryState_ = function(url) {
245     if (history.state && history.state.url != url)
246       history.pushState({url: url}, '');
247     else
248       history.replaceState({url: url});
249   };
251   /**
252    * Invoked when the sign-in page takes focus.
253    * @param {object} e The focus event being triggered.
254    * @private
255    */
256   Authenticator.prototype.onFocus_ = function(e) {
257     this.webview_.focus();
258   };
260   /**
261    * Invoked when the history state is changed.
262    * @param {object} e The popstate event being triggered.
263    * @private
264    */
265   Authenticator.prototype.onPopState_ = function(e) {
266     var state = e.state;
267     if (state && state.url)
268       this.webview_.src = state.url;
269   };
271   /**
272    * Invoked when headers are received in the main frame of the webview. It
273    * 1) reads the authenticated user info from a signin header,
274    * 2) signals the start of a saml flow upon receiving a saml header.
275    * @return {!Object} Modified request headers.
276    * @private
277    */
278   Authenticator.prototype.onHeadersReceived_ = function(details) {
279     var currentUrl = details.url;
280     if (currentUrl.lastIndexOf(this.idpOrigin_, 0) != 0)
281       return;
283     var headers = details.responseHeaders;
284     for (var i = 0; headers && i < headers.length; ++i) {
285       var header = headers[i];
286       var headerName = header.name.toLowerCase();
287       if (headerName == SIGN_IN_HEADER) {
288         var headerValues = header.value.toLowerCase().split(',');
289         var signinDetails = {};
290         headerValues.forEach(function(e) {
291           var pair = e.split('=');
292           signinDetails[pair[0].trim()] = pair[1].trim();
293         });
294         // Removes "" around.
295         this.email_ = signinDetails['email'].slice(1, -1);
296         this.gaiaId_ = signinDetails['obfuscatedid'].slice(1, -1);
297         this.sessionIndex_ = signinDetails['sessionindex'];
298       } else if (headerName == SAML_HEADER) {
299         this.authFlow_ = AuthFlow.SAML;
300       } else if (headerName == LOCATION_HEADER) {
301         // If the "choose what to sync" checkbox was clicked, then the continue
302         // URL will contain a source=3 field.
303         var location = decodeURIComponent(header.value);
304         this.chooseWhatToSync_ = !!location.match(/(\?|&)source=3($|&)/);
305       } else if (this.isMinuteMaidChromeOS && headerName == SET_COOKIE_HEADER) {
306         var headerValue = header.value;
307         if (headerValue.indexOf(OAUTH_CODE_COOKIE + '=', 0) == 0) {
308           this.oauth_code_ =
309               headerValue.substring(OAUTH_CODE_COOKIE.length + 1).split(';')[0];
310         }
311       }
312     }
313   };
315   /**
316    * Invoked when an HTML5 message is received from the webview element.
317    * @param {object} e Payload of the received HTML5 message.
318    * @private
319    */
320   Authenticator.prototype.onMessageFromWebview_ = function(e) {
321     // The event origin does not have a trailing slash.
322     if (e.origin != this.idpOrigin_.substring(0, this.idpOrigin_.length - 1)) {
323       return;
324     }
326     var msg = e.data;
327     if (msg.method == 'attemptLogin') {
328       this.email_ = msg.email;
329       this.password_ = msg.password;
330       this.chooseWhatToSync_ = msg.chooseWhatToSync;
331     }
332   };
334   /**
335    * Invoked to process authentication completion.
336    * @private
337    */
338   Authenticator.prototype.onAuthCompleted_ = function() {
339     if (!this.email_ && !this.skipForNow_) {
340       this.webview_.src = this.initialFrameUrl_;
341       return;
342     }
344     this.dispatchEvent(
345         new CustomEvent('authCompleted',
346           // TODO(rsorokin): get rid of the stub values.
347                         {detail: {email: this.email_ || '',
348                                   gaiaId: this.gaiaId_ || '',
349                                   password: this.password_ || '',
350                                   authCode: this.oauth_code_,
351                                   usingSAML: this.authFlow_ == AuthFlow.SAML,
352                                   chooseWhatToSync: this.chooseWhatToSync_,
353                                   skipForNow: this.skipForNow_,
354                                   sessionIndex: this.sessionIndex_ || '',
355                                   trusted: this.trusted_}}));
356   };
358   /**
359    * Invoked when a link is dropped on the webview.
360    * @private
361    */
362   Authenticator.prototype.onDropLink_ = function(e) {
363     this.dispatchEvent(new CustomEvent('dropLink', {detail: e.url}));
364   };
366   /**
367    * Invoked when the webview attempts to open a new window.
368    * @private
369    */
370   Authenticator.prototype.onNewWindow_ = function(e) {
371     this.dispatchEvent(new CustomEvent('newWindow', {detail: e}));
372   };
374   /**
375    * Invoked when a new document is loaded.
376    * @private
377    */
378   Authenticator.prototype.onContentLoad_ = function(e) {
379     // Posts a message to IdP pages to initiate communication.
380     var currentUrl = this.webview_.src;
381     if (currentUrl.lastIndexOf(this.idpOrigin_) == 0) {
382       var msg = {
383         'method': 'handshake'
384       };
385       this.webview_.contentWindow.postMessage(msg, currentUrl);
386     }
387   };
389   /**
390    * Invoked when the webview finishes loading a page.
391    * @private
392    */
393   Authenticator.prototype.onLoadStop_ = function(e) {
394     if (!this.loaded_) {
395       this.loaded_ = true;
396       this.webview_.focus();
397       this.dispatchEvent(new Event('ready'));
398     }
399   };
401   /**
402    * Invoked when the webview navigates withing the current document.
403    * @private
404    */
405   Authenticator.prototype.onLoadCommit_ = function(e) {
406     // TODO(rsorokin): Investigate whether this breaks SAML.
407     if (this.oauth_code_) {
408       this.skipForNow_ = true;
409       this.onAuthCompleted_();
410     }
411   };
413   Authenticator.AuthFlow = AuthFlow;
414   Authenticator.AuthMode = AuthMode;
415   Authenticator.SUPPORTED_PARAMS = SUPPORTED_PARAMS;
417   return {
418     // TODO(guohui, xiyuan): Rename GaiaAuthHost to Authenticator once the old
419     // iframe-based flow is deprecated.
420     GaiaAuthHost: Authenticator
421   };