1 // Copyright (c) 2012 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 * Authenticator class wraps the communications between Gaia and its host.
8 function Authenticator() {
12 * Gaia auth extension url origin.
15 Authenticator.THIS_EXTENSION_ORIGIN =
16 'chrome-extension://mfffpogegjflfpflabcdkioaeobkgjik';
19 * Singleton getter of Authenticator.
20 * @return {Object} The singleton instance of Authenticator.
22 Authenticator.getInstance = function() {
23 if (!Authenticator.instance_) {
24 Authenticator.instance_ = new Authenticator();
26 return Authenticator.instance_;
29 Authenticator.prototype = {
34 // Input params from extension initialization URL.
35 inputLang_: undefined,
36 intputEmail_: undefined,
39 samlSupportChannel_: null,
41 GAIA_URL: 'https://accounts.google.com/',
42 GAIA_PAGE_PATH: 'ServiceLogin?skipvpage=true&sarp=1&rm=hide',
43 PARENT_PAGE: 'chrome://oobe/',
44 SERVICE_ID: 'chromeoslogin',
45 CONTINUE_URL: Authenticator.THIS_EXTENSION_ORIGIN + '/success.html',
46 CONSTRAINED_FLOW_SOURCE: 'chrome',
48 initialize: function() {
49 var params = getUrlSearchParams(location.search);
50 this.parentPage_ = params.parentPage || this.PARENT_PAGE;
51 this.gaiaUrl_ = params.gaiaUrl || this.GAIA_URL;
52 this.gaiaPath_ = params.gaiaPath || this.GAIA_PAGE_PATH;
53 this.inputLang_ = params.hl;
54 this.inputEmail_ = params.email;
55 this.service_ = params.service || this.SERVICE_ID;
56 this.continueUrl_ = params.continueUrl || this.CONTINUE_URL;
57 this.continueUrlWithoutParams_ = stripParams(this.continueUrl_);
58 this.inlineMode_ = params.inlineMode == '1';
59 this.constrained_ = params.constrained == '1';
60 this.partitionId_ = params.partitionId || '';
61 this.initialFrameUrl_ = params.frameUrl || this.constructInitialFrameUrl_();
62 this.initialFrameUrlWithoutParams_ = stripParams(this.initialFrameUrl_);
65 document.addEventListener('DOMContentLoaded', this.onPageLoad.bind(this));
66 document.addEventListener('enableSAML', this.onEnableSAML_.bind(this));
69 isGaiaMessage_: function(msg) {
70 // Not quite right, but good enough.
71 return this.gaiaUrl_.indexOf(msg.origin) == 0 ||
72 this.GAIA_URL.indexOf(msg.origin) == 0;
75 isInternalMessage_: function(msg) {
76 return msg.origin == Authenticator.THIS_EXTENSION_ORIGIN;
79 isParentMessage_: function(msg) {
80 return msg.origin == this.parentPage_;
83 constructInitialFrameUrl_: function() {
84 var url = this.gaiaUrl_ + this.gaiaPath_;
86 url = appendParam(url, 'service', this.service_);
87 url = appendParam(url, 'continue', this.continueUrl_);
89 url = appendParam(url, 'hl', this.inputLang_);
91 url = appendParam(url, 'Email', this.inputEmail_);
92 if (this.constrained_)
93 url = appendParam(url, 'source', this.CONSTRAINED_FLOW_SOURCE);
97 /** Callback when all loads in the gaia webview is complete. */
98 onWebviewLoadstop_: function(gaiaFrame) {
99 if (gaiaFrame.src.lastIndexOf(this.continueUrlWithoutParams_, 0) == 0) {
100 // Detect when login is finished by the load stop event of the continue
101 // URL. Cannot reuse the login complete flow in success.html, because
102 // webview does not support extension pages yet.
103 var skipForNow = false;
104 if (this.inlineMode_ && gaiaFrame.src.indexOf('ntp=1') >= 0) {
108 'method': 'completeLogin',
109 'skipForNow': skipForNow
111 window.parent.postMessage(msg, this.parentPage_);
112 // Do no report state to the parent for the continue URL, since it is a
117 // Report the current state to the parent which will then update the
118 // browser history so that later it could respond properly to back/forward.
120 'method': 'reportState',
123 window.parent.postMessage(msg, this.parentPage_);
125 if (gaiaFrame.src.lastIndexOf(this.gaiaUrl_, 0) == 0) {
126 gaiaFrame.executeScript({file: 'inline_injected.js'}, function() {
127 // Send an initial message to gaia so that it has an JavaScript
128 // reference to the embedder.
129 gaiaFrame.contentWindow.postMessage('', gaiaFrame.src);
131 if (this.constrained_) {
132 var preventContextMenu = 'document.addEventListener("contextmenu", ' +
133 'function(e) {e.preventDefault();})';
134 gaiaFrame.executeScript({code: preventContextMenu});
138 this.loaded_ || this.onLoginUILoaded();
142 * Callback when the gaia webview attempts to open a new window.
144 onWebviewNewWindow_: function(gaiaFrame, e) {
145 window.open(e.targetUrl, '_blank');
149 onWebviewRequestCompleted_: function(details) {
150 if (details.url.lastIndexOf(this.continueUrlWithoutParams_, 0) == 0) {
154 var headers = details.responseHeaders;
155 for (var i = 0; headers && i < headers.length; ++i) {
156 if (headers[i].name.toLowerCase() == 'google-accounts-embedded') {
161 'method': 'switchToFullTab',
164 window.parent.postMessage(msg, this.parentPage_);
167 loadFrame_: function() {
168 var gaiaFrame = $('gaia-frame');
169 gaiaFrame.partition = this.partitionId_;
170 gaiaFrame.src = this.initialFrameUrl_;
171 if (this.inlineMode_) {
172 gaiaFrame.addEventListener(
173 'loadstop', this.onWebviewLoadstop_.bind(this, gaiaFrame));
174 gaiaFrame.addEventListener(
175 'newwindow', this.onWebviewNewWindow_.bind(this, gaiaFrame));
177 if (this.constrained_) {
178 gaiaFrame.request.onCompleted.addListener(
179 this.onWebviewRequestCompleted_.bind(this),
180 {urls: ['<all_urls>'], types: ['main_frame']},
181 ['responseHeaders']);
185 completeLogin: function() {
187 'method': 'completeLogin',
188 'email': this.email_,
189 'password': this.password_,
190 'usingSAML': this.isSAMLFlow_
192 window.parent.postMessage(msg, this.parentPage_);
193 if (this.samlSupportChannel_)
194 this.samlSupportChannel_.send({name: 'resetAuth'});
197 onPageLoad: function(e) {
198 window.addEventListener('message', this.onMessage.bind(this), false);
203 * Invoked when 'enableSAML' event is received to initialize SAML support.
205 onEnableSAML_: function() {
206 this.isSAMLFlow_ = false;
208 this.samlSupportChannel_ = new Channel();
209 this.samlSupportChannel_.connect('authMain');
210 this.samlSupportChannel_.registerMessage(
211 'onAuthPageLoaded', this.onAuthPageLoaded_.bind(this));
212 this.samlSupportChannel_.registerMessage(
213 'apiCall', this.onAPICall_.bind(this));
214 this.samlSupportChannel_.send({
216 gaiaUrl: this.gaiaUrl_
221 * Invoked when the background page sends 'onHostedPageLoaded' message.
222 * @param {!Object} msg Details sent with the message.
224 onAuthPageLoaded_: function(msg) {
225 var isSAMLPage = msg.url.indexOf(this.gaiaUrl_) != 0;
227 if (isSAMLPage && !this.isSAMLFlow_) {
228 // GAIA redirected to a SAML login page. The credentials provided to this
229 // page will determine what user gets logged in. The credentials obtained
230 // from the GAIA login from are no longer relevant and can be discarded.
231 this.isSAMLFlow_ = true;
233 this.password_ = null;
236 window.parent.postMessage({
237 'method': 'authPageLoaded',
238 'isSAML': this.isSAMLFlow_,
239 'domain': extractDomain(msg.url)
240 }, this.parentPage_);
244 * Invoked when one of the credential passing API methods is called by a SAML
246 * @param {!Object} msg Details of the API call.
248 onAPICall_: function(msg) {
250 if (call.method == 'add') {
251 this.apiToken_ = call.token;
252 this.email_ = call.user;
253 this.password_ = call.password;
254 } else if (call.method == 'confirm') {
255 if (call.token != this.apiToken_)
256 console.error('Authenticator.onAPICall_: token mismatch');
258 console.error('Authenticator.onAPICall_: unknown message');
262 onLoginUILoaded: function() {
264 'method': 'loginUILoaded'
266 window.parent.postMessage(msg, this.parentPage_);
267 if (this.inlineMode_) {
268 // TODO(guohui): temporary workaround until webview team fixes the focus
270 var gaiaFrame = $('gaia-frame');
272 gaiaFrame.onblur = function() {
279 onConfirmLogin_: function() {
280 if (!this.isSAMLFlow_) {
281 this.completeLogin();
285 // Retrieve the e-mail address of the user who just authenticated from GAIA.
286 window.parent.postMessage({method: 'retrieveAuthenticatedUserEmail',
287 attemptToken: this.attemptToken_},
290 if (!this.password_) {
291 this.samlSupportChannel_.sendWithCallback(
292 {name: 'getScrapedPasswords'},
293 function(passwords) {
294 if (passwords.length == 0) {
295 window.parent.postMessage(
296 {method: 'noPassword', email: this.email_},
299 window.parent.postMessage(
300 {method: 'confirmPassword', email: this.email_},
307 maybeCompleteSAMLLogin_: function() {
308 // SAML login is complete when the user's e-mail address has been retrieved
309 // from GAIA and the user has successfully confirmed the password.
310 if (this.email_ !== null && this.password_ !== null)
311 this.completeLogin();
314 onVerifyConfirmedPassword_: function(password) {
315 this.samlSupportChannel_.sendWithCallback(
316 {name: 'getScrapedPasswords'},
317 function(passwords) {
318 for (var i = 0; i < passwords.length; ++i) {
319 if (passwords[i] == password) {
320 this.password_ = passwords[i];
321 this.maybeCompleteSAMLLogin_();
325 window.parent.postMessage(
326 {method: 'confirmPassword', email: this.email_},
331 onMessage: function(e) {
333 if (msg.method == 'attemptLogin' && this.isGaiaMessage_(e)) {
334 this.email_ = msg.email;
335 this.password_ = msg.password;
336 this.attemptToken_ = msg.attemptToken;
337 this.isSAMLFlow_ = false;
338 if (this.samlSupportChannel_)
339 this.samlSupportChannel_.send({name: 'startAuth'});
340 } else if (msg.method == 'clearOldAttempts' && this.isGaiaMessage_(e)) {
342 this.password_ = null;
343 this.attemptToken_ = null;
344 this.isSAMLFlow_ = false;
345 this.onLoginUILoaded();
346 if (this.samlSupportChannel_)
347 this.samlSupportChannel_.send({name: 'resetAuth'});
348 } else if (msg.method == 'setAuthenticatedUserEmail' &&
349 this.isParentMessage_(e)) {
350 if (this.attemptToken_ == msg.attemptToken) {
351 this.email_ = msg.email;
352 this.maybeCompleteSAMLLogin_();
354 } else if (msg.method == 'confirmLogin' && this.isInternalMessage_(e)) {
355 if (this.attemptToken_ == msg.attemptToken)
356 this.onConfirmLogin_();
358 console.error('Authenticator.onMessage: unexpected attemptToken!?');
359 } else if (msg.method == 'verifyConfirmedPassword' &&
360 this.isParentMessage_(e)) {
361 this.onVerifyConfirmedPassword_(msg.password);
362 } else if (msg.method == 'navigate' &&
363 this.isParentMessage_(e)) {
364 $('gaia-frame').src = msg.src;
365 } else if (msg.method == 'redirectToSignin' &&
366 this.isParentMessage_(e)) {
367 $('gaia-frame').src = this.constructInitialFrameUrl_();
369 console.error('Authenticator.onMessage: unknown message + origin!?');
374 Authenticator.getInstance().initialize();