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.
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.
12 cr.define('cr.login', function() {
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
18 var IDP_ORIGIN = 'https://accounts.google.com/';
19 var IDP_PATH = 'ServiceLogin?skipvpage=true&sarp=1&rm=hide';
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';
31 * The source URL parameter for the constrained signin flow.
33 var CONSTRAINED_FLOW_SOURCE = 'chrome';
36 * Enum for the authorization mode, must match AuthMode defined in
37 * chrome/browser/ui/webui/inline_login_ui.cc.
47 * Enum for the authorization type.
56 * Supported Authenticator params.
57 * @type {!Array<string>}
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;
73 * Initializes the authenticator component.
74 * @param {webview|string} webview The webview element or its ID to host IdP
78 function Authenticator(webview) {
79 this.webview_ = typeof webview == 'string' ? $(webview) : webview;
80 assert(this.webview_);
83 this.password_ = null;
85 this.sessionIndex_ = null;
86 this.chooseWhatToSync_ = false;
87 this.skipForNow_ = false;
88 this.authFlow_ = AuthFlow.DEFAULT;
90 this.idpOrigin_ = null;
91 this.continueUrl_ = null;
92 this.continueUrlWithoutParams_ = null;
93 this.initialFrameUrl_ = null;
94 this.reloadUrl_ = null;
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);
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);
128 * Loads the authenticator component with the given parameters.
129 * @param {AuthMode} authMode Authorization mode.
130 * @param {Object} data Parameters for the authorization flow.
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('?')) ||
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;
151 * Reloads the authenticator component.
153 Authenticator.prototype.reload = function() {
154 this.webview_.src = this.reloadUrl_;
155 this.authFlow_ = AuthFlow.DEFAULT;
156 this.loaded_ = false;
160 * Send message 'focusready' to Gaia so it sets focus on default input.
162 Authenticator.prototype.sendFocusReady = function() {
163 var currentUrl = this.webview_.src;
164 if (currentUrl.lastIndexOf(this.idpOrigin_) == 0) {
166 'method': 'focusready'
168 // TODO(rsorokin): Get rid of this check once issue crbug.com/456118 is
170 if (this.webview_.contentWindow) {
171 this.webview_.contentWindow.postMessage(msg, currentUrl);
176 Authenticator.prototype.constructInitialFrameUrl_ = function(data) {
177 var url = this.idpOrigin_ + (data.gaiaPath || IDP_PATH);
179 if (this.isMinuteMaidChromeOS) {
181 url = appendParam(url, 'chrometype', data.chromeType);
183 url = appendParam(url, 'client_id', data.clientId);
184 if (data.enterpriseDomain)
185 url = appendParam(url, 'managedomain', data.enterpriseDomain);
187 url = appendParam(url, 'continue', this.continueUrl_);
188 url = appendParam(url, 'service', data.service || SERVICE_ID);
191 url = appendParam(url, 'hl', data.hl);
193 url = appendParam(url, 'Email', data.email);
194 if (this.isConstrainedWindow_)
195 url = appendParam(url, 'source', CONSTRAINED_FLOW_SOURCE);
200 * Invoked when a main frame request in the webview has completed.
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_();
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;
228 if (!isEmbeddedPage) {
229 this.dispatchEvent(new CustomEvent('resize', {detail: currentUrl}));
234 this.updateHistoryState_(currentUrl);
239 * Manually updates the history. Invoked upon completion of a webview
241 * @param {string} url Request URL.
244 Authenticator.prototype.updateHistoryState_ = function(url) {
245 if (history.state && history.state.url != url)
246 history.pushState({url: url}, '');
248 history.replaceState({url: url});
252 * Invoked when the sign-in page takes focus.
253 * @param {object} e The focus event being triggered.
256 Authenticator.prototype.onFocus_ = function(e) {
257 this.webview_.focus();
261 * Invoked when the history state is changed.
262 * @param {object} e The popstate event being triggered.
265 Authenticator.prototype.onPopState_ = function(e) {
267 if (state && state.url)
268 this.webview_.src = state.url;
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.
278 Authenticator.prototype.onHeadersReceived_ = function(details) {
279 var currentUrl = details.url;
280 if (currentUrl.lastIndexOf(this.idpOrigin_, 0) != 0)
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();
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) {
309 headerValue.substring(OAUTH_CODE_COOKIE.length + 1).split(';')[0];
316 * Invoked when an HTML5 message is received from the webview element.
317 * @param {object} e Payload of the received HTML5 message.
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)) {
327 if (msg.method == 'attemptLogin') {
328 this.email_ = msg.email;
329 this.password_ = msg.password;
330 this.chooseWhatToSync_ = msg.chooseWhatToSync;
335 * Invoked to process authentication completion.
338 Authenticator.prototype.onAuthCompleted_ = function() {
339 if (!this.email_ && !this.skipForNow_) {
340 this.webview_.src = this.initialFrameUrl_;
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_}}));
359 * Invoked when a link is dropped on the webview.
362 Authenticator.prototype.onDropLink_ = function(e) {
363 this.dispatchEvent(new CustomEvent('dropLink', {detail: e.url}));
367 * Invoked when the webview attempts to open a new window.
370 Authenticator.prototype.onNewWindow_ = function(e) {
371 this.dispatchEvent(new CustomEvent('newWindow', {detail: e}));
375 * Invoked when a new document is loaded.
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) {
383 'method': 'handshake'
385 this.webview_.contentWindow.postMessage(msg, currentUrl);
390 * Invoked when the webview finishes loading a page.
393 Authenticator.prototype.onLoadStop_ = function(e) {
396 this.webview_.focus();
397 this.dispatchEvent(new Event('ready'));
402 * Invoked when the webview navigates withing the current document.
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_();
413 Authenticator.AuthFlow = AuthFlow;
414 Authenticator.AuthMode = AuthMode;
415 Authenticator.SUPPORTED_PARAMS = SUPPORTED_PARAMS;
418 // TODO(guohui, xiyuan): Rename GaiaAuthHost to Authenticator once the old
419 // iframe-based flow is deprecated.
420 GaiaAuthHost: Authenticator