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 * Allowed origins of the hosting page.
40 * @type {Array<string>}
42 Authenticator
.ALLOWED_PARENT_ORIGINS
= [
44 'chrome://chrome-signin'
48 * Singleton getter of Authenticator.
49 * @return {Object} The singleton instance of Authenticator.
51 Authenticator
.getInstance = function() {
52 if (!Authenticator
.instance_
) {
53 Authenticator
.instance_
= new Authenticator();
55 return Authenticator
.instance_
;
58 Authenticator
.prototype = {
62 // Depending on the key type chosen, this will contain the plain text password
63 // or a credential derived from it along with the information required to
64 // repeat the derivation, such as a salt. The information will be encoded so
65 // that it contains printable ASCII characters only. The exact encoding is TBD
66 // when support for key types other than plain text password is added.
70 chooseWhatToSync_
: false,
75 // Input params from extension initialization URL.
76 inputLang_
: undefined,
77 intputEmail_
: undefined,
81 supportChannel_
: null,
86 GAIA_URL
: 'https://accounts.google.com/',
87 GAIA_PAGE_PATH
: 'ServiceLogin?skipvpage=true&sarp=1&rm=hide',
88 SERVICE_ID
: 'chromeoslogin',
89 CONTINUE_URL
: Authenticator
.THIS_EXTENSION_ORIGIN
+ '/success.html',
90 CONSTRAINED_FLOW_SOURCE
: 'chrome',
92 initialize: function() {
93 var handleInitializeMessage = function(e
) {
94 if (Authenticator
.ALLOWED_PARENT_ORIGINS
.indexOf(e
.origin
) == -1) {
95 console
.error('Unexpected parent message, origin=' + e
.origin
);
98 window
.removeEventListener('message', handleInitializeMessage
);
101 params
.parentPage
= e
.origin
;
102 this.initializeFromParent_(params
);
106 document
.addEventListener('DOMContentLoaded', function() {
107 window
.addEventListener('message', handleInitializeMessage
);
111 initializeFromParent_: function(params
) {
112 this.parentPage_
= params
.parentPage
;
113 this.gaiaUrl_
= params
.gaiaUrl
|| this.GAIA_URL
;
114 this.gaiaPath_
= params
.gaiaPath
|| this.GAIA_PAGE_PATH
;
115 this.inputLang_
= params
.hl
;
116 this.inputEmail_
= params
.email
;
117 this.service_
= params
.service
|| this.SERVICE_ID
;
118 this.continueUrl_
= params
.continueUrl
|| this.CONTINUE_URL
;
119 this.desktopMode_
= params
.desktopMode
== '1';
120 this.isConstrainedWindow_
= params
.constrained
== '1';
121 this.useEafe_
= params
.useEafe
|| false;
122 this.clientId_
= params
.clientId
|| '';
123 this.initialFrameUrl_
= params
.frameUrl
|| this.constructInitialFrameUrl_();
124 this.initialFrameUrlWithoutParams_
= stripParams(this.initialFrameUrl_
);
125 this.needPassword_
= params
.needPassword
== '1';
127 // For CrOS 'ServiceLogin' we assume that Gaia is loaded if we recieved
128 // 'clearOldAttempts' message. For other scenarios Gaia doesn't send this
129 // message so we have to rely on 'load' event.
130 // TODO(dzhioev): Do not rely on 'load' event after b/16313327 is fixed.
131 this.assumeLoadedOnLoadEvent_
=
132 this.gaiaPath_
.indexOf('ServiceLogin') !== 0 ||
133 this.service_
!== 'chromeoslogin' ||
137 isGaiaMessage_: function(msg
) {
138 // Not quite right, but good enough.
139 return this.gaiaUrl_
.indexOf(msg
.origin
) == 0 ||
140 this.GAIA_URL
.indexOf(msg
.origin
) == 0;
143 isParentMessage_: function(msg
) {
144 return msg
.origin
== this.parentPage_
;
147 constructInitialFrameUrl_: function() {
148 var url
= this.gaiaUrl_
+ this.gaiaPath_
;
150 url
= appendParam(url
, 'service', this.service_
);
151 // Easy bootstrap use auth_code message as success signal instead of
154 url
= appendParam(url
, 'continue', this.continueUrl_
);
156 url
= appendParam(url
, 'hl', this.inputLang_
);
157 if (this.inputEmail_
)
158 url
= appendParam(url
, 'Email', this.inputEmail_
);
159 if (this.isConstrainedWindow_
)
160 url
= appendParam(url
, 'source', this.CONSTRAINED_FLOW_SOURCE
);
164 onPageLoad_: function() {
165 window
.addEventListener('message', this.onMessage
.bind(this), false);
166 this.initSupportChannel_();
168 if (this.assumeLoadedOnLoadEvent_
) {
169 var gaiaFrame
= $('gaia-frame');
170 var handler = function() {
171 gaiaFrame
.removeEventListener('load', handler
);
172 if (!this.gaiaLoaded_
) {
173 this.gaiaLoaded_
= true;
174 this.maybeInitialized_();
176 if (this.useEafe_
&& this.clientId_
) {
177 // Sends initial handshake message to EAFE. Note this fails with
178 // SSO redirect because |gaiaFrame| sits on a different origin.
179 gaiaFrame
.contentWindow
.postMessage({
180 clientId
: this.clientId_
185 gaiaFrame
.addEventListener('load', handler
);
189 initSupportChannel_: function() {
190 var supportChannel
= new Channel();
191 supportChannel
.connect('authMain');
193 supportChannel
.registerMessage('channelConnected', function() {
194 // Load the gaia frame after the background page indicates that it is
195 // ready, so that the webRequest handlers are all setup first.
196 var gaiaFrame
= $('gaia-frame');
197 gaiaFrame
.src
= this.initialFrameUrl_
;
199 if (this.supportChannel_
) {
200 console
.error('Support channel is already initialized.');
203 this.supportChannel_
= supportChannel
;
205 if (this.desktopMode_
) {
206 this.supportChannel_
.send({
207 name
: 'initDesktopFlow',
208 gaiaUrl
: this.gaiaUrl_
,
209 continueUrl
: stripParams(this.continueUrl_
),
210 isConstrainedWindow
: this.isConstrainedWindow_
,
211 initialFrameUrlWithoutParams
: this.initialFrameUrlWithoutParams_
214 this.supportChannel_
.registerMessage(
215 'switchToFullTab', this.switchToFullTab_
.bind(this));
217 this.supportChannel_
.registerMessage(
218 'completeLogin', this.onCompleteLogin_
.bind(this));
220 this.supportChannel_
.send({name
: 'resetAuth'});
221 this.maybeInitialized_();
224 window
.setTimeout(function() {
225 if (!this.supportChannel_
) {
226 // Re-initialize the channel if it is not connected properly, e.g.
227 // connect may be called before background script started running.
228 this.initSupportChannel_();
234 * Called when one of the initialization stages has finished. If all the
235 * needed parts are initialized, notifies parent about successfull
238 maybeInitialized_: function() {
239 if (!this.gaiaLoaded_
|| !this.supportChannel_
)
242 'method': 'loginUILoaded'
244 window
.parent
.postMessage(msg
, this.parentPage_
);
248 * Invoked when the background script sends a message to indicate that the
249 * current content does not fit in a constrained window.
250 * @param {Object=} msg Extra info to send.
252 switchToFullTab_: function(msg
) {
254 'method': 'switchToFullTab',
257 window
.parent
.postMessage(parentMsg
, this.parentPage_
);
261 * Invoked when the signin flow is complete.
262 * @param {Object=} opt_extraMsg Optional extra info to send.
264 completeLogin_: function(opt_extraMsg
) {
266 'method': 'completeLogin',
267 'email': (opt_extraMsg
&& opt_extraMsg
.email
) || this.email_
,
268 'password': (opt_extraMsg
&& opt_extraMsg
.password
) ||
270 'usingSAML': this.isSAMLFlow_
,
271 'chooseWhatToSync': this.chooseWhatToSync_
|| false,
272 'skipForNow': (opt_extraMsg
&& opt_extraMsg
.skipForNow
) ||
274 'sessionIndex': (opt_extraMsg
&& opt_extraMsg
.sessionIndex
) ||
276 'gaiaId': (opt_extraMsg
&& opt_extraMsg
.gaiaId
) || this.gaiaId_
278 window
.parent
.postMessage(msg
, this.parentPage_
);
279 this.supportChannel_
.send({name
: 'resetAuth'});
283 * Invoked when support channel is connected.
285 initSAML_: function() {
286 this.isSAMLFlow_
= false;
288 this.supportChannel_
.registerMessage(
289 'onAuthPageLoaded', this.onAuthPageLoaded_
.bind(this));
290 this.supportChannel_
.registerMessage(
291 'onInsecureContentBlocked', this.onInsecureContentBlocked_
.bind(this));
292 this.supportChannel_
.registerMessage(
293 'apiCall', this.onAPICall_
.bind(this));
294 this.supportChannel_
.send({
296 gaiaUrl
: this.gaiaUrl_
298 if (!this.desktopMode_
&& this.gaiaUrl_
.indexOf('https://') == 0) {
299 // Abort the login flow when content served over an unencrypted connection
300 // is detected on Chrome OS. This does not apply to tests that explicitly
301 // set a non-https GAIA URL and want to perform all authentication over
303 this.supportChannel_
.send({
304 name
: 'setBlockInsecureContent',
305 blockInsecureContent
: true
311 * Invoked when the background page sends 'onHostedPageLoaded' message.
312 * @param {!Object} msg Details sent with the message.
314 onAuthPageLoaded_: function(msg
) {
315 if (msg
.isSAMLPage
&& !this.isSAMLFlow_
) {
316 // GAIA redirected to a SAML login page. The credentials provided to this
317 // page will determine what user gets logged in. The credentials obtained
318 // from the GAIA login form are no longer relevant and can be discarded.
319 this.isSAMLFlow_
= true;
322 this.passwordBytes_
= null;
325 window
.parent
.postMessage({
326 'method': 'authPageLoaded',
327 'isSAML': this.isSAMLFlow_
,
328 'domain': extractDomain(msg
.url
)
329 }, this.parentPage_
);
333 * Invoked when the background page sends an 'onInsecureContentBlocked'
335 * @param {!Object} msg Details sent with the message.
337 onInsecureContentBlocked_: function(msg
) {
338 window
.parent
.postMessage({
339 'method': 'insecureContentBlocked',
340 'url': stripParams(msg
.url
)
341 }, this.parentPage_
);
345 * Invoked when one of the credential passing API methods is called by a SAML
347 * @param {!Object} msg Details of the API call.
349 onAPICall_: function(msg
) {
351 if (call
.method
== 'initialize') {
352 if (!Number
.isInteger(call
.requestedVersion
) ||
353 call
.requestedVersion
< Authenticator
.MIN_API_VERSION_VERSION
) {
354 this.sendInitializationFailure_();
358 this.apiVersion_
= Math
.min(call
.requestedVersion
,
359 Authenticator
.MAX_API_VERSION_VERSION
);
360 this.initialized_
= true;
361 this.sendInitializationSuccess_();
365 if (call
.method
== 'add') {
366 if (Authenticator
.API_KEY_TYPES
.indexOf(call
.keyType
) == -1) {
367 console
.error('Authenticator.onAPICall_: unsupported key type');
370 // Not setting |email_| and |gaiaId_| because this API call will
371 // eventually be followed by onCompleteLogin_() which does set it.
372 this.apiToken_
= call
.token
;
373 this.passwordBytes_
= call
.passwordBytes
;
374 } else if (call
.method
== 'confirm') {
375 if (call
.token
!= this.apiToken_
)
376 console
.error('Authenticator.onAPICall_: token mismatch');
378 console
.error('Authenticator.onAPICall_: unknown message');
382 onGotAuthCode_: function(authCode
) {
383 window
.parent
.postMessage({
384 'method': 'completeAuthenticationAuthCodeOnly',
386 }, this.parentPage_
);
389 sendInitializationSuccess_: function() {
390 this.supportChannel_
.send({name
: 'apiResponse', response
: {
391 result
: 'initialized',
392 version
: this.apiVersion_
,
393 keyTypes
: Authenticator
.API_KEY_TYPES
397 sendInitializationFailure_: function() {
398 this.supportChannel_
.send({
400 response
: {result
: 'initialization_failed'}
405 * Callback invoked for 'completeLogin' message.
406 * @param {Object=} msg Message sent from background page.
408 onCompleteLogin_: function(msg
) {
409 if (!msg
.email
|| !msg
.gaiaId
|| !msg
.sessionIndex
) {
410 // On desktop, if the skipForNow message field is set, send it to handler.
411 // This does not require the email, gaiaid or session to be valid.
412 if (this.desktopMode_
&& msg
.skipForNow
) {
413 this.completeLogin_(msg
);
415 console
.error('Missing fields to complete login.');
416 window
.parent
.postMessage({method
: 'missingGaiaInfo'},
422 // Skip SAML extra steps for desktop flow and non-SAML flow.
423 if (!this.isSAMLFlow_
|| this.desktopMode_
) {
424 this.completeLogin_(msg
);
428 this.email_
= msg
.email
;
429 this.gaiaId_
= msg
.gaiaId
;
430 // Password from |msg| is not used because ChromeOS SAML flow
431 // gets password by asking user to confirm.
432 this.skipForNow_
= msg
.skipForNow
;
433 this.sessionIndex_
= msg
.sessionIndex
;
435 if (this.passwordBytes_
) {
436 // If the credentials passing API was used, login is complete.
437 window
.parent
.postMessage({method
: 'samlApiUsed'}, this.parentPage_
);
438 this.completeLogin_(msg
);
439 } else if (!this.needPassword_
) {
440 // If the credentials passing API was not used, the password was obtained
441 // by scraping. It must be verified before use. However, the host may not
442 // be interested in the password at all. In that case, verification is
443 // unnecessary and login is complete.
444 this.completeLogin_(msg
);
446 this.supportChannel_
.sendWithCallback(
447 {name
: 'getScrapedPasswords'},
448 function(passwords
) {
449 if (passwords
.length
== 0) {
450 window
.parent
.postMessage(
451 {method
: 'noPassword', email
: this.email_
},
454 window
.parent
.postMessage({method
: 'confirmPassword',
456 passwordCount
: passwords
.length
},
463 onVerifyConfirmedPassword_: function(password
) {
464 this.supportChannel_
.sendWithCallback(
465 {name
: 'getScrapedPasswords'},
466 function(passwords
) {
467 for (var i
= 0; i
< passwords
.length
; ++i
) {
468 if (passwords
[i
] == password
) {
469 this.passwordBytes_
= passwords
[i
];
470 // SAML login is complete when the user has successfully
471 // confirmed the password.
472 if (this.passwordBytes_
!== null)
473 this.completeLogin_();
477 window
.parent
.postMessage(
478 {method
: 'confirmPassword', email
: this.email_
},
483 onMessage: function(e
) {
487 if (msg
== '!_{h:\'gaia-frame\'}' && this.isGaiaMessage_(e
)) {
488 // Sends client ID again on the hello message to work around the SSO
490 // TODO(xiyuan): Revisit this when EAFE is integrated or for webview.
491 $('gaia-frame').contentWindow
.postMessage({
492 clientId
: this.clientId_
494 } else if (typeof msg
== 'object' &&
495 msg
.type
== 'authorizationCode' && this.isGaiaMessage_(e
)) {
496 this.onGotAuthCode_(msg
.authorizationCode
);
498 console
.error('Authenticator.onMessage: unknown message' +
499 ', msg=' + JSON
.stringify(msg
));
505 if (msg
.method
== 'attemptLogin' && this.isGaiaMessage_(e
)) {
506 // At this point GAIA does not yet know the gaiaId, so its not set here.
507 this.email_
= msg
.email
;
508 this.passwordBytes_
= msg
.password
;
509 this.attemptToken_
= msg
.attemptToken
;
510 this.chooseWhatToSync_
= msg
.chooseWhatToSync
;
511 this.isSAMLFlow_
= false;
512 if (this.supportChannel_
)
513 this.supportChannel_
.send({name
: 'startAuth'});
515 console
.error('Support channel is not initialized.');
516 } else if (msg
.method
== 'clearOldAttempts' && this.isGaiaMessage_(e
)) {
517 if (!this.gaiaLoaded_
) {
518 this.gaiaLoaded_
= true;
519 this.maybeInitialized_();
523 this.sessionIndex_
= false;
524 this.passwordBytes_
= null;
525 this.attemptToken_
= null;
526 this.isSAMLFlow_
= false;
527 this.skipForNow_
= false;
528 this.chooseWhatToSync_
= false;
529 if (this.supportChannel_
) {
530 this.supportChannel_
.send({name
: 'resetAuth'});
531 // This message is for clearing saml properties in gaia_auth_host and
532 // oobe_screen_oauth_enrollment.
533 window
.parent
.postMessage({
534 'method': 'resetAuthFlow',
535 }, this.parentPage_
);
537 } else if (msg
.method
== 'verifyConfirmedPassword' &&
538 this.isParentMessage_(e
)) {
539 this.onVerifyConfirmedPassword_(msg
.password
);
540 } else if (msg
.method
== 'redirectToSignin' &&
541 this.isParentMessage_(e
)) {
542 $('gaia-frame').src
= this.constructInitialFrameUrl_();
544 console
.error('Authenticator.onMessage: unknown message + origin!?');
549 Authenticator
.getInstance().initialize();