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 isSAMLEnabled_: false,
40 supportChannel_: null,
42 GAIA_URL: 'https://accounts.google.com/',
43 GAIA_PAGE_PATH: 'ServiceLogin?skipvpage=true&sarp=1&rm=hide',
44 PARENT_PAGE: 'chrome://oobe/',
45 SERVICE_ID: 'chromeoslogin',
46 CONTINUE_URL: Authenticator.THIS_EXTENSION_ORIGIN + '/success.html',
47 CONSTRAINED_FLOW_SOURCE: 'chrome',
49 initialize: function() {
50 var params = getUrlSearchParams(location.search);
51 this.parentPage_ = params.parentPage || this.PARENT_PAGE;
52 this.gaiaUrl_ = params.gaiaUrl || this.GAIA_URL;
53 this.gaiaPath_ = params.gaiaPath || this.GAIA_PAGE_PATH;
54 this.inputLang_ = params.hl;
55 this.inputEmail_ = params.email;
56 this.service_ = params.service || this.SERVICE_ID;
57 this.continueUrl_ = params.continueUrl || this.CONTINUE_URL;
58 this.desktopMode_ = params.desktopMode == '1';
59 this.isConstrainedWindow_ = params.constrained == '1';
60 this.initialFrameUrl_ = params.frameUrl || this.constructInitialFrameUrl_();
61 this.initialFrameUrlWithoutParams_ = stripParams(this.initialFrameUrl_);
63 document.addEventListener('DOMContentLoaded', this.onPageLoad_.bind(this));
64 document.addEventListener('enableSAML', this.onEnableSAML_.bind(this));
67 isGaiaMessage_: function(msg) {
68 // Not quite right, but good enough.
69 return this.gaiaUrl_.indexOf(msg.origin) == 0 ||
70 this.GAIA_URL.indexOf(msg.origin) == 0;
73 isInternalMessage_: function(msg) {
74 return msg.origin == Authenticator.THIS_EXTENSION_ORIGIN;
77 isParentMessage_: function(msg) {
78 return msg.origin == this.parentPage_;
81 constructInitialFrameUrl_: function() {
82 var url = this.gaiaUrl_ + this.gaiaPath_;
84 url = appendParam(url, 'service', this.service_);
85 url = appendParam(url, 'continue', this.continueUrl_);
87 url = appendParam(url, 'hl', this.inputLang_);
89 url = appendParam(url, 'Email', this.inputEmail_);
90 if (this.isConstrainedWindow_)
91 url = appendParam(url, 'source', this.CONSTRAINED_FLOW_SOURCE);
95 onPageLoad_: function() {
96 window.addEventListener('message', this.onMessage.bind(this), false);
98 var gaiaFrame = $('gaia-frame');
99 gaiaFrame.src = this.initialFrameUrl_;
101 if (this.desktopMode_) {
102 var handler = function() {
103 this.onLoginUILoaded_();
104 gaiaFrame.removeEventListener('load', handler);
106 this.initDesktopChannel_();
108 gaiaFrame.addEventListener('load', handler);
112 initDesktopChannel_: function() {
113 this.supportChannel_ = new Channel();
114 this.supportChannel_.connect('authMain');
116 var channelConnected = false;
117 this.supportChannel_.registerMessage('channelConnected', function() {
118 channelConnected = true;
120 this.supportChannel_.send({
121 name: 'initDesktopFlow',
122 gaiaUrl: this.gaiaUrl_,
123 continueUrl: stripParams(this.continueUrl_),
124 isConstrainedWindow: this.isConstrainedWindow_
126 this.supportChannel_.registerMessage(
127 'switchToFullTab', this.switchToFullTab_.bind(this));
128 this.supportChannel_.registerMessage(
129 'completeLogin', this.completeLogin_.bind(this));
132 window.setTimeout(function() {
133 if (!channelConnected) {
134 // Re-initialize the channel if it is not connected properly, e.g.
135 // connect may be called before background script started running.
136 this.initDesktopChannel_();
142 * Invoked when the login UI is initialized or reset.
144 onLoginUILoaded_: function() {
146 'method': 'loginUILoaded'
148 window.parent.postMessage(msg, this.parentPage_);
152 * Invoked when the background script sends a message to indicate that the
153 * current content does not fit in a constrained window.
154 * @param {Object=} opt_extraMsg Optional extra info to send.
156 switchToFullTab_: function(msg) {
158 'method': 'switchToFullTab',
161 window.parent.postMessage(parentMsg, this.parentPage_);
165 * Invoked when the signin flow is complete.
166 * @param {Object=} opt_extraMsg Optional extra info to send.
168 completeLogin_: function(opt_extraMsg) {
170 'method': 'completeLogin',
171 'email': (opt_extraMsg && opt_extraMsg.email) || this.email_,
172 'password': this.password_,
173 'usingSAML': this.isSAMLFlow_,
174 'chooseWhatToSync': this.chooseWhatToSync_ || false,
175 'skipForNow': opt_extraMsg && opt_extraMsg.skipForNow,
176 'sessionIndex': opt_extraMsg && opt_extraMsg.sessionIndex
178 window.parent.postMessage(msg, this.parentPage_);
179 if (this.isSAMLEnabled_)
180 this.supportChannel_.send({name: 'resetAuth'});
184 * Invoked when 'enableSAML' event is received to initialize SAML support.
186 onEnableSAML_: function() {
187 this.isSAMLEnabled_ = true;
188 this.isSAMLFlow_ = false;
190 if (!this.supportChannel_) {
191 this.supportChannel_ = new Channel();
192 this.supportChannel_.connect('authMain');
195 this.supportChannel_.registerMessage(
196 'onAuthPageLoaded', this.onAuthPageLoaded_.bind(this));
197 this.supportChannel_.registerMessage(
198 'apiCall', this.onAPICall_.bind(this));
199 this.supportChannel_.send({
201 gaiaUrl: this.gaiaUrl_
206 * Invoked when the background page sends 'onHostedPageLoaded' message.
207 * @param {!Object} msg Details sent with the message.
209 onAuthPageLoaded_: function(msg) {
210 var isSAMLPage = msg.url.indexOf(this.gaiaUrl_) != 0;
212 if (isSAMLPage && !this.isSAMLFlow_) {
213 // GAIA redirected to a SAML login page. The credentials provided to this
214 // page will determine what user gets logged in. The credentials obtained
215 // from the GAIA login from are no longer relevant and can be discarded.
216 this.isSAMLFlow_ = true;
218 this.password_ = null;
221 window.parent.postMessage({
222 'method': 'authPageLoaded',
223 'isSAML': this.isSAMLFlow_,
224 'domain': extractDomain(msg.url)
225 }, this.parentPage_);
229 * Invoked when one of the credential passing API methods is called by a SAML
231 * @param {!Object} msg Details of the API call.
233 onAPICall_: function(msg) {
235 if (call.method == 'add') {
236 this.apiToken_ = call.token;
237 this.email_ = call.user;
238 this.password_ = call.password;
239 } else if (call.method == 'confirm') {
240 if (call.token != this.apiToken_)
241 console.error('Authenticator.onAPICall_: token mismatch');
243 console.error('Authenticator.onAPICall_: unknown message');
247 onConfirmLogin_: function() {
248 if (!this.isSAMLFlow_) {
249 this.completeLogin_();
253 var apiUsed = !!this.password_;
255 // Retrieve the e-mail address of the user who just authenticated from GAIA.
256 window.parent.postMessage({method: 'retrieveAuthenticatedUserEmail',
257 attemptToken: this.attemptToken_,
262 this.supportChannel_.sendWithCallback(
263 {name: 'getScrapedPasswords'},
264 function(passwords) {
265 if (passwords.length == 0) {
266 window.parent.postMessage(
267 {method: 'noPassword', email: this.email_},
270 window.parent.postMessage({method: 'confirmPassword',
272 passwordCount: passwords.length},
279 maybeCompleteSAMLLogin_: function() {
280 // SAML login is complete when the user's e-mail address has been retrieved
281 // from GAIA and the user has successfully confirmed the password.
282 if (this.email_ !== null && this.password_ !== null)
283 this.completeLogin_();
286 onVerifyConfirmedPassword_: function(password) {
287 this.supportChannel_.sendWithCallback(
288 {name: 'getScrapedPasswords'},
289 function(passwords) {
290 for (var i = 0; i < passwords.length; ++i) {
291 if (passwords[i] == password) {
292 this.password_ = passwords[i];
293 this.maybeCompleteSAMLLogin_();
297 window.parent.postMessage(
298 {method: 'confirmPassword', email: this.email_},
303 onMessage: function(e) {
305 if (msg.method == 'attemptLogin' && this.isGaiaMessage_(e)) {
306 this.email_ = msg.email;
307 this.password_ = msg.password;
308 this.attemptToken_ = msg.attemptToken;
309 this.chooseWhatToSync_ = msg.chooseWhatToSync;
310 this.isSAMLFlow_ = false;
311 if (this.isSAMLEnabled_)
312 this.supportChannel_.send({name: 'startAuth'});
313 } else if (msg.method == 'clearOldAttempts' && this.isGaiaMessage_(e)) {
315 this.password_ = null;
316 this.attemptToken_ = null;
317 this.isSAMLFlow_ = false;
318 this.onLoginUILoaded_();
319 if (this.isSAMLEnabled_)
320 this.supportChannel_.send({name: 'resetAuth'});
321 } else if (msg.method == 'setAuthenticatedUserEmail' &&
322 this.isParentMessage_(e)) {
323 if (this.attemptToken_ == msg.attemptToken) {
324 this.email_ = msg.email;
325 this.maybeCompleteSAMLLogin_();
327 } else if (msg.method == 'confirmLogin' && this.isInternalMessage_(e)) {
328 if (this.attemptToken_ == msg.attemptToken)
329 this.onConfirmLogin_();
331 console.error('Authenticator.onMessage: unexpected attemptToken!?');
332 } else if (msg.method == 'verifyConfirmedPassword' &&
333 this.isParentMessage_(e)) {
334 this.onVerifyConfirmedPassword_(msg.password);
335 } else if (msg.method == 'navigate' &&
336 this.isParentMessage_(e)) {
337 $('gaia-frame').src = msg.src;
338 } else if (msg.method == 'redirectToSignin' &&
339 this.isParentMessage_(e)) {
340 $('gaia-frame').src = this.constructInitialFrameUrl_();
342 console.error('Authenticator.onMessage: unknown message + origin!?');
347 Authenticator.getInstance().initialize();