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 * The lowest version of the credentials passing API supported.
22 Authenticator
.MIN_API_VERSION_VERSION
= 1;
25 * The highest version of the credentials passing API supported.
28 Authenticator
.MAX_API_VERSION_VERSION
= 1;
31 * The key types supported by the credentials passing API.
32 * @type {Array} Array of strings.
34 Authenticator
.API_KEY_TYPES
= [
35 'KEY_TYPE_PASSWORD_PLAIN',
39 * Singleton getter of Authenticator.
40 * @return {Object} The singleton instance of Authenticator.
42 Authenticator
.getInstance = function() {
43 if (!Authenticator
.instance_
) {
44 Authenticator
.instance_
= new Authenticator();
46 return Authenticator
.instance_
;
49 Authenticator
.prototype = {
52 // Depending on the key type chosen, this will contain the plain text password
53 // or a credential derived from it along with the information required to
54 // repeat the derivation, such as a salt. The information will be encoded so
55 // that it contains printable ASCII characters only. The exact encoding is TBD
56 // when support for key types other than plain text password is added.
61 // Input params from extension initialization URL.
62 inputLang_
: undefined,
63 intputEmail_
: undefined,
66 isSAMLEnabled_
: false,
67 supportChannel_
: null,
69 GAIA_URL
: 'https://accounts.google.com/',
70 GAIA_PAGE_PATH
: 'ServiceLogin?skipvpage=true&sarp=1&rm=hide',
71 PARENT_PAGE
: 'chrome://oobe/',
72 SERVICE_ID
: 'chromeoslogin',
73 CONTINUE_URL
: Authenticator
.THIS_EXTENSION_ORIGIN
+ '/success.html',
74 CONSTRAINED_FLOW_SOURCE
: 'chrome',
76 initialize: function() {
77 var params
= getUrlSearchParams(location
.search
);
78 this.parentPage_
= params
.parentPage
|| this.PARENT_PAGE
;
79 this.gaiaUrl_
= params
.gaiaUrl
|| this.GAIA_URL
;
80 this.gaiaPath_
= params
.gaiaPath
|| this.GAIA_PAGE_PATH
;
81 this.inputLang_
= params
.hl
;
82 this.inputEmail_
= params
.email
;
83 this.service_
= params
.service
|| this.SERVICE_ID
;
84 this.continueUrl_
= params
.continueUrl
|| this.CONTINUE_URL
;
85 this.desktopMode_
= params
.desktopMode
== '1';
86 this.isConstrainedWindow_
= params
.constrained
== '1';
87 this.initialFrameUrl_
= params
.frameUrl
|| this.constructInitialFrameUrl_();
88 this.initialFrameUrlWithoutParams_
= stripParams(this.initialFrameUrl_
);
90 document
.addEventListener('DOMContentLoaded', this.onPageLoad_
.bind(this));
91 if (!this.desktopMode_
) {
92 // SAML is always enabled in desktop mode, thus no need to listen for
94 document
.addEventListener('enableSAML', this.onEnableSAML_
.bind(this));
98 isGaiaMessage_: function(msg
) {
99 // Not quite right, but good enough.
100 return this.gaiaUrl_
.indexOf(msg
.origin
) == 0 ||
101 this.GAIA_URL
.indexOf(msg
.origin
) == 0;
104 isInternalMessage_: function(msg
) {
105 return msg
.origin
== Authenticator
.THIS_EXTENSION_ORIGIN
;
108 isParentMessage_: function(msg
) {
109 return msg
.origin
== this.parentPage_
;
112 constructInitialFrameUrl_: function() {
113 var url
= this.gaiaUrl_
+ this.gaiaPath_
;
115 url
= appendParam(url
, 'service', this.service_
);
116 url
= appendParam(url
, 'continue', this.continueUrl_
);
118 url
= appendParam(url
, 'hl', this.inputLang_
);
119 if (this.inputEmail_
)
120 url
= appendParam(url
, 'Email', this.inputEmail_
);
121 if (this.isConstrainedWindow_
)
122 url
= appendParam(url
, 'source', this.CONSTRAINED_FLOW_SOURCE
);
126 onPageLoad_: function() {
127 window
.addEventListener('message', this.onMessage
.bind(this), false);
129 var gaiaFrame
= $('gaia-frame');
130 gaiaFrame
.src
= this.initialFrameUrl_
;
132 if (this.desktopMode_
) {
133 var handler = function() {
134 this.onLoginUILoaded_();
135 gaiaFrame
.removeEventListener('load', handler
);
137 this.initDesktopChannel_();
139 gaiaFrame
.addEventListener('load', handler
);
143 initDesktopChannel_: function() {
144 this.supportChannel_
= new Channel();
145 this.supportChannel_
.connect('authMain');
147 var channelConnected
= false;
148 this.supportChannel_
.registerMessage('channelConnected', function() {
149 channelConnected
= true;
151 this.supportChannel_
.send({
152 name
: 'initDesktopFlow',
153 gaiaUrl
: this.gaiaUrl_
,
154 continueUrl
: stripParams(this.continueUrl_
),
155 isConstrainedWindow
: this.isConstrainedWindow_
157 this.supportChannel_
.registerMessage(
158 'switchToFullTab', this.switchToFullTab_
.bind(this));
159 this.supportChannel_
.registerMessage(
160 'completeLogin', this.completeLogin_
.bind(this));
162 this.onEnableSAML_();
165 window
.setTimeout(function() {
166 if (!channelConnected
) {
167 // Re-initialize the channel if it is not connected properly, e.g.
168 // connect may be called before background script started running.
169 this.initDesktopChannel_();
175 * Invoked when the login UI is initialized or reset.
177 onLoginUILoaded_: function() {
179 'method': 'loginUILoaded'
181 window
.parent
.postMessage(msg
, this.parentPage_
);
185 * Invoked when the background script sends a message to indicate that the
186 * current content does not fit in a constrained window.
187 * @param {Object=} opt_extraMsg Optional extra info to send.
189 switchToFullTab_: function(msg
) {
191 'method': 'switchToFullTab',
194 window
.parent
.postMessage(parentMsg
, this.parentPage_
);
198 * Invoked when the signin flow is complete.
199 * @param {Object=} opt_extraMsg Optional extra info to send.
201 completeLogin_: function(opt_extraMsg
) {
203 'method': 'completeLogin',
204 'email': (opt_extraMsg
&& opt_extraMsg
.email
) || this.email_
,
205 'password': (opt_extraMsg
&& opt_extraMsg
.password
) ||
207 'usingSAML': this.isSAMLFlow_
,
208 'chooseWhatToSync': this.chooseWhatToSync_
|| false,
209 'skipForNow': opt_extraMsg
&& opt_extraMsg
.skipForNow
,
210 'sessionIndex': opt_extraMsg
&& opt_extraMsg
.sessionIndex
212 window
.parent
.postMessage(msg
, this.parentPage_
);
213 if (this.isSAMLEnabled_
)
214 this.supportChannel_
.send({name
: 'resetAuth'});
218 * Invoked when 'enableSAML' event is received to initialize SAML support on
219 * Chrome OS, or when initDesktopChannel_ is called on desktop.
221 onEnableSAML_: function() {
222 this.isSAMLEnabled_
= true;
223 this.isSAMLFlow_
= false;
225 if (!this.supportChannel_
) {
226 this.supportChannel_
= new Channel();
227 this.supportChannel_
.connect('authMain');
230 this.supportChannel_
.registerMessage(
231 'onAuthPageLoaded', this.onAuthPageLoaded_
.bind(this));
232 this.supportChannel_
.registerMessage(
233 'onInsecureContentBlocked', this.onInsecureContentBlocked_
.bind(this));
234 this.supportChannel_
.registerMessage(
235 'apiCall', this.onAPICall_
.bind(this));
236 this.supportChannel_
.send({
238 gaiaUrl
: this.gaiaUrl_
240 if (!this.desktopMode_
&& this.gaiaUrl_
.indexOf('https://') == 0) {
241 // Abort the login flow when content served over an unencrypted connection
242 // is detected on Chrome OS. This does not apply to tests that explicitly
243 // set a non-https GAIA URL and want to perform all authentication over
245 this.supportChannel_
.send({
246 name
: 'setBlockInsecureContent',
247 blockInsecureContent
: true
253 * Invoked when the background page sends 'onHostedPageLoaded' message.
254 * @param {!Object} msg Details sent with the message.
256 onAuthPageLoaded_: function(msg
) {
257 var isSAMLPage
= msg
.url
.indexOf(this.gaiaUrl_
) != 0;
259 if (isSAMLPage
&& !this.isSAMLFlow_
) {
260 // GAIA redirected to a SAML login page. The credentials provided to this
261 // page will determine what user gets logged in. The credentials obtained
262 // from the GAIA login form are no longer relevant and can be discarded.
263 this.isSAMLFlow_
= true;
265 this.passwordBytes_
= null;
268 window
.parent
.postMessage({
269 'method': 'authPageLoaded',
270 'isSAML': this.isSAMLFlow_
,
271 'domain': extractDomain(msg
.url
)
272 }, this.parentPage_
);
276 * Invoked when the background page sends an 'onInsecureContentBlocked'
278 * @param {!Object} msg Details sent with the message.
280 onInsecureContentBlocked_: function(msg
) {
281 window
.parent
.postMessage({
282 'method': 'insecureContentBlocked',
283 'url': stripParams(msg
.url
)
284 }, this.parentPage_
);
288 * Invoked when one of the credential passing API methods is called by a SAML
290 * @param {!Object} msg Details of the API call.
292 onAPICall_: function(msg
) {
294 if (call
.method
== 'initialize') {
295 if (!Number
.isInteger(call
.requestedVersion
) ||
296 call
.requestedVersion
< Authenticator
.MIN_API_VERSION_VERSION
) {
297 this.sendInitializationFailure_();
301 this.apiVersion_
= Math
.min(call
.requestedVersion
,
302 Authenticator
.MAX_API_VERSION_VERSION
);
303 this.initialized_
= true;
304 this.sendInitializationSuccess_();
308 if (call
.method
== 'add') {
309 if (Authenticator
.API_KEY_TYPES
.indexOf(call
.keyType
) == -1) {
310 console
.error('Authenticator.onAPICall_: unsupported key type');
313 this.apiToken_
= call
.token
;
314 this.email_
= call
.user
;
315 this.passwordBytes_
= call
.passwordBytes
;
316 } else if (call
.method
== 'confirm') {
317 if (call
.token
!= this.apiToken_
)
318 console
.error('Authenticator.onAPICall_: token mismatch');
320 console
.error('Authenticator.onAPICall_: unknown message');
324 sendInitializationSuccess_: function() {
325 this.supportChannel_
.send({name
: 'apiResponse', response
: {
326 result
: 'initialized',
327 version
: this.apiVersion_
,
328 keyTypes
: Authenticator
.API_KEY_TYPES
332 sendInitializationFailure_: function() {
333 this.supportChannel_
.send({
335 response
: {result
: 'initialization_failed'}
339 onConfirmLogin_: function() {
340 if (!this.isSAMLFlow_
) {
341 this.completeLogin_();
345 var apiUsed
= !!this.passwordBytes_
;
347 // Retrieve the e-mail address of the user who just authenticated from GAIA.
348 window
.parent
.postMessage({method
: 'retrieveAuthenticatedUserEmail',
349 attemptToken
: this.attemptToken_
,
354 this.supportChannel_
.sendWithCallback(
355 {name
: 'getScrapedPasswords'},
356 function(passwords
) {
357 if (passwords
.length
== 0) {
358 window
.parent
.postMessage(
359 {method
: 'noPassword', email
: this.email_
},
362 window
.parent
.postMessage({method
: 'confirmPassword',
364 passwordCount
: passwords
.length
},
371 maybeCompleteSAMLLogin_: function() {
372 // SAML login is complete when the user's e-mail address has been retrieved
373 // from GAIA and the user has successfully confirmed the password.
374 if (this.email_
!== null && this.passwordBytes_
!== null)
375 this.completeLogin_();
378 onVerifyConfirmedPassword_: function(password
) {
379 this.supportChannel_
.sendWithCallback(
380 {name
: 'getScrapedPasswords'},
381 function(passwords
) {
382 for (var i
= 0; i
< passwords
.length
; ++i
) {
383 if (passwords
[i
] == password
) {
384 this.passwordBytes_
= passwords
[i
];
385 this.maybeCompleteSAMLLogin_();
389 window
.parent
.postMessage(
390 {method
: 'confirmPassword', email
: this.email_
},
395 onMessage: function(e
) {
397 if (msg
.method
== 'attemptLogin' && this.isGaiaMessage_(e
)) {
398 this.email_
= msg
.email
;
399 this.passwordBytes_
= msg
.password
;
400 this.attemptToken_
= msg
.attemptToken
;
401 this.chooseWhatToSync_
= msg
.chooseWhatToSync
;
402 this.isSAMLFlow_
= false;
403 if (this.isSAMLEnabled_
)
404 this.supportChannel_
.send({name
: 'startAuth'});
405 } else if (msg
.method
== 'clearOldAttempts' && this.isGaiaMessage_(e
)) {
407 this.passwordBytes_
= null;
408 this.attemptToken_
= null;
409 this.isSAMLFlow_
= false;
410 this.onLoginUILoaded_();
411 if (this.isSAMLEnabled_
)
412 this.supportChannel_
.send({name
: 'resetAuth'});
413 } else if (msg
.method
== 'setAuthenticatedUserEmail' &&
414 this.isParentMessage_(e
)) {
415 if (this.attemptToken_
== msg
.attemptToken
) {
416 this.email_
= msg
.email
;
417 this.maybeCompleteSAMLLogin_();
419 } else if (msg
.method
== 'confirmLogin' && this.isInternalMessage_(e
)) {
420 if (this.attemptToken_
== msg
.attemptToken
)
421 this.onConfirmLogin_();
423 console
.error('Authenticator.onMessage: unexpected attemptToken!?');
424 } else if (msg
.method
== 'verifyConfirmedPassword' &&
425 this.isParentMessage_(e
)) {
426 this.onVerifyConfirmedPassword_(msg
.password
);
427 } else if (msg
.method
== 'redirectToSignin' &&
428 this.isParentMessage_(e
)) {
429 $('gaia-frame').src
= this.constructInitialFrameUrl_();
431 console
.error('Authenticator.onMessage: unknown message + origin!?');
436 Authenticator
.getInstance().initialize();