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 = {
53 // Depending on the key type chosen, this will contain the plain text password
54 // or a credential derived from it along with the information required to
55 // repeat the derivation, such as a salt. The information will be encoded so
56 // that it contains printable ASCII characters only. The exact encoding is TBD
57 // when support for key types other than plain text password is added.
60 chooseWhatToSync_
: false,
65 // Input params from extension initialization URL.
66 inputLang_
: undefined,
67 intputEmail_
: undefined,
71 supportChannel_
: null,
73 GAIA_URL
: 'https://accounts.google.com/',
74 GAIA_PAGE_PATH
: 'ServiceLogin?skipvpage=true&sarp=1&rm=hide',
75 PARENT_PAGE
: 'chrome://oobe/',
76 SERVICE_ID
: 'chromeoslogin',
77 CONTINUE_URL
: Authenticator
.THIS_EXTENSION_ORIGIN
+ '/success.html',
78 CONSTRAINED_FLOW_SOURCE
: 'chrome',
80 initialize: function() {
81 var params
= getUrlSearchParams(location
.search
);
82 this.parentPage_
= params
.parentPage
|| this.PARENT_PAGE
;
83 this.gaiaUrl_
= params
.gaiaUrl
|| this.GAIA_URL
;
84 this.gaiaPath_
= params
.gaiaPath
|| this.GAIA_PAGE_PATH
;
85 this.inputLang_
= params
.hl
;
86 this.inputEmail_
= params
.email
;
87 this.service_
= params
.service
|| this.SERVICE_ID
;
88 this.continueUrl_
= params
.continueUrl
|| this.CONTINUE_URL
;
89 this.desktopMode_
= params
.desktopMode
== '1';
90 this.isConstrainedWindow_
= params
.constrained
== '1';
91 this.initialFrameUrl_
= params
.frameUrl
|| this.constructInitialFrameUrl_();
92 this.initialFrameUrlWithoutParams_
= stripParams(this.initialFrameUrl_
);
94 // For CrOS 'ServiceLogin' we assume that Gaia is loaded if we recieved
95 // 'clearOldAttempts' message. For other scenarios Gaia doesn't send this
96 // message so we have to rely on 'load' event.
97 // TODO(dzhioev): Do not rely on 'load' event after b/16313327 is fixed.
98 this.assumeLoadedOnLoadEvent_
=
99 this.gaiaPath_
.indexOf('ServiceLogin') !== 0 ||
100 this.service_
!== 'chromeoslogin';
102 document
.addEventListener('DOMContentLoaded', this.onPageLoad_
.bind(this));
105 isGaiaMessage_: function(msg
) {
106 // Not quite right, but good enough.
107 return this.gaiaUrl_
.indexOf(msg
.origin
) == 0 ||
108 this.GAIA_URL
.indexOf(msg
.origin
) == 0;
111 isParentMessage_: function(msg
) {
112 return msg
.origin
== this.parentPage_
;
115 constructInitialFrameUrl_: function() {
116 var url
= this.gaiaUrl_
+ this.gaiaPath_
;
118 url
= appendParam(url
, 'service', this.service_
);
119 url
= appendParam(url
, 'continue', this.continueUrl_
);
121 url
= appendParam(url
, 'hl', this.inputLang_
);
122 if (this.inputEmail_
)
123 url
= appendParam(url
, 'Email', this.inputEmail_
);
124 if (this.isConstrainedWindow_
)
125 url
= appendParam(url
, 'source', this.CONSTRAINED_FLOW_SOURCE
);
129 onPageLoad_: function() {
130 window
.addEventListener('message', this.onMessage
.bind(this), false);
131 this.initSupportChannel_();
133 var gaiaFrame
= $('gaia-frame');
134 gaiaFrame
.src
= this.initialFrameUrl_
;
136 if (this.assumeLoadedOnLoadEvent_
) {
137 var handler = function() {
138 gaiaFrame
.removeEventListener('load', handler
);
139 if (!this.gaiaLoaded_
) {
140 this.gaiaLoaded_
= true;
141 this.maybeInitialized_();
144 gaiaFrame
.addEventListener('load', handler
);
148 initSupportChannel_: function() {
149 var supportChannel
= new Channel();
150 supportChannel
.connect('authMain');
152 supportChannel
.registerMessage('channelConnected', function() {
153 if (this.supportChannel_
) {
154 console
.error('Support channel is already initialized.');
157 this.supportChannel_
= supportChannel
;
159 if (this.desktopMode_
) {
160 this.supportChannel_
.send({
161 name
: 'initDesktopFlow',
162 gaiaUrl
: this.gaiaUrl_
,
163 continueUrl
: stripParams(this.continueUrl_
),
164 isConstrainedWindow
: this.isConstrainedWindow_
166 this.supportChannel_
.registerMessage(
167 'switchToFullTab', this.switchToFullTab_
.bind(this));
169 this.supportChannel_
.registerMessage(
170 'completeLogin', this.onCompleteLogin_
.bind(this));
172 this.maybeInitialized_();
175 window
.setTimeout(function() {
176 if (!this.supportChannel_
) {
177 // Re-initialize the channel if it is not connected properly, e.g.
178 // connect may be called before background script started running.
179 this.initSupportChannel_();
185 * Called when one of the initialization stages has finished. If all the
186 * needed parts are initialized, notifies parent about successfull
189 maybeInitialized_: function() {
190 if (!this.gaiaLoaded_
|| !this.supportChannel_
)
193 'method': 'loginUILoaded'
195 window
.parent
.postMessage(msg
, this.parentPage_
);
199 * Invoked when the background script sends a message to indicate that the
200 * current content does not fit in a constrained window.
201 * @param {Object=} msg Extra info to send.
203 switchToFullTab_: function(msg
) {
205 'method': 'switchToFullTab',
208 window
.parent
.postMessage(parentMsg
, this.parentPage_
);
212 * Invoked when the signin flow is complete.
213 * @param {Object=} opt_extraMsg Optional extra info to send.
215 completeLogin_: function(opt_extraMsg
) {
217 'method': 'completeLogin',
218 'email': (opt_extraMsg
&& opt_extraMsg
.email
) || this.email_
,
219 'password': (opt_extraMsg
&& opt_extraMsg
.password
) ||
221 'usingSAML': this.isSAMLFlow_
,
222 'chooseWhatToSync': this.chooseWhatToSync_
|| false,
223 'skipForNow': (opt_extraMsg
&& opt_extraMsg
.skipForNow
) ||
225 'sessionIndex': (opt_extraMsg
&& opt_extraMsg
.sessionIndex
) ||
227 'gaiaId': (opt_extraMsg
&& opt_extraMsg
.gaiaId
) || this.gaiaId_
229 window
.parent
.postMessage(msg
, this.parentPage_
);
230 this.supportChannel_
.send({name
: 'resetAuth'});
234 * Invoked when support channel is connected.
236 initSAML_: function() {
237 this.isSAMLFlow_
= false;
239 this.supportChannel_
.registerMessage(
240 'onAuthPageLoaded', this.onAuthPageLoaded_
.bind(this));
241 this.supportChannel_
.registerMessage(
242 'onInsecureContentBlocked', this.onInsecureContentBlocked_
.bind(this));
243 this.supportChannel_
.registerMessage(
244 'apiCall', this.onAPICall_
.bind(this));
245 this.supportChannel_
.send({
247 gaiaUrl
: this.gaiaUrl_
249 if (!this.desktopMode_
&& this.gaiaUrl_
.indexOf('https://') == 0) {
250 // Abort the login flow when content served over an unencrypted connection
251 // is detected on Chrome OS. This does not apply to tests that explicitly
252 // set a non-https GAIA URL and want to perform all authentication over
254 this.supportChannel_
.send({
255 name
: 'setBlockInsecureContent',
256 blockInsecureContent
: true
262 * Invoked when the background page sends 'onHostedPageLoaded' message.
263 * @param {!Object} msg Details sent with the message.
265 onAuthPageLoaded_: function(msg
) {
266 if (msg
.isSAMLPage
&& !this.isSAMLFlow_
) {
267 // GAIA redirected to a SAML login page. The credentials provided to this
268 // page will determine what user gets logged in. The credentials obtained
269 // from the GAIA login form are no longer relevant and can be discarded.
270 this.isSAMLFlow_
= true;
273 this.passwordBytes_
= null;
276 window
.parent
.postMessage({
277 'method': 'authPageLoaded',
278 'isSAML': this.isSAMLFlow_
,
279 'domain': extractDomain(msg
.url
)
280 }, this.parentPage_
);
284 * Invoked when the background page sends an 'onInsecureContentBlocked'
286 * @param {!Object} msg Details sent with the message.
288 onInsecureContentBlocked_: function(msg
) {
289 window
.parent
.postMessage({
290 'method': 'insecureContentBlocked',
291 'url': stripParams(msg
.url
)
292 }, this.parentPage_
);
296 * Invoked when one of the credential passing API methods is called by a SAML
298 * @param {!Object} msg Details of the API call.
300 onAPICall_: function(msg
) {
302 if (call
.method
== 'initialize') {
303 if (!Number
.isInteger(call
.requestedVersion
) ||
304 call
.requestedVersion
< Authenticator
.MIN_API_VERSION_VERSION
) {
305 this.sendInitializationFailure_();
309 this.apiVersion_
= Math
.min(call
.requestedVersion
,
310 Authenticator
.MAX_API_VERSION_VERSION
);
311 this.initialized_
= true;
312 this.sendInitializationSuccess_();
316 if (call
.method
== 'add') {
317 if (Authenticator
.API_KEY_TYPES
.indexOf(call
.keyType
) == -1) {
318 console
.error('Authenticator.onAPICall_: unsupported key type');
321 // Not setting |email_| and |gaiaId_| because this API call will
322 // eventually be followed by onCompleteLogin_() which does set it.
323 this.apiToken_
= call
.token
;
324 this.passwordBytes_
= call
.passwordBytes
;
325 } else if (call
.method
== 'confirm') {
326 if (call
.token
!= this.apiToken_
)
327 console
.error('Authenticator.onAPICall_: token mismatch');
329 console
.error('Authenticator.onAPICall_: unknown message');
333 sendInitializationSuccess_: function() {
334 this.supportChannel_
.send({name
: 'apiResponse', response
: {
335 result
: 'initialized',
336 version
: this.apiVersion_
,
337 keyTypes
: Authenticator
.API_KEY_TYPES
341 sendInitializationFailure_: function() {
342 this.supportChannel_
.send({
344 response
: {result
: 'initialization_failed'}
349 * Callback invoked for 'completeLogin' message.
350 * @param {Object=} msg Message sent from background page.
352 onCompleteLogin_: function(msg
) {
353 if (!msg
.email
|| !msg
.gaiaId
|| !msg
.sessionIndex
) {
354 console
.error('Missing fields to complete login.');
355 window
.parent
.postMessage({method
: 'missingGaiaInfo'}, this.parentPage_
);
359 // Skip SAML extra steps for desktop flow and non-SAML flow.
360 if (!this.isSAMLFlow_
|| this.desktopMode_
) {
361 this.completeLogin_(msg
);
365 this.email_
= msg
.email
;
366 this.gaiaId_
= msg
.gaiaId
;
367 // Password from |msg| is not used because ChromeOS SAML flow
368 // gets password by asking user to confirm.
369 this.skipForNow_
= msg
.skipForNow
;
370 this.sessionIndex_
= msg
.sessionIndex
;
372 if (this.passwordBytes_
) {
373 window
.parent
.postMessage({method
: 'samlApiUsed'}, this.parentPage_
);
374 this.completeLogin_(msg
);
376 this.supportChannel_
.sendWithCallback(
377 {name
: 'getScrapedPasswords'},
378 function(passwords
) {
379 if (passwords
.length
== 0) {
380 window
.parent
.postMessage(
381 {method
: 'noPassword', email
: this.email_
},
384 window
.parent
.postMessage({method
: 'confirmPassword',
386 passwordCount
: passwords
.length
},
393 onVerifyConfirmedPassword_: function(password
) {
394 this.supportChannel_
.sendWithCallback(
395 {name
: 'getScrapedPasswords'},
396 function(passwords
) {
397 for (var i
= 0; i
< passwords
.length
; ++i
) {
398 if (passwords
[i
] == password
) {
399 this.passwordBytes_
= passwords
[i
];
400 // SAML login is complete when the user has successfully
401 // confirmed the password.
402 if (this.passwordBytes_
!== null)
403 this.completeLogin_();
407 window
.parent
.postMessage(
408 {method
: 'confirmPassword', email
: this.email_
},
413 onMessage: function(e
) {
415 if (msg
.method
== 'attemptLogin' && this.isGaiaMessage_(e
)) {
416 // At this point GAIA does not yet know the gaiaId, so its not set here.
417 this.email_
= msg
.email
;
418 this.passwordBytes_
= msg
.password
;
419 this.attemptToken_
= msg
.attemptToken
;
420 this.chooseWhatToSync_
= msg
.chooseWhatToSync
;
421 this.isSAMLFlow_
= false;
422 if (this.supportChannel_
)
423 this.supportChannel_
.send({name
: 'startAuth'});
425 console
.error('Support channel is not initialized.');
426 } else if (msg
.method
== 'clearOldAttempts' && this.isGaiaMessage_(e
)) {
427 if (!this.gaiaLoaded_
) {
428 this.gaiaLoaded_
= true;
429 this.maybeInitialized_();
433 this.sessionIndex_
= false;
434 this.passwordBytes_
= null;
435 this.attemptToken_
= null;
436 this.isSAMLFlow_
= false;
437 this.skipForNow_
= false;
438 this.chooseWhatToSync_
= false;
439 if (this.supportChannel_
)
440 this.supportChannel_
.send({name
: 'resetAuth'});
441 } else if (msg
.method
== 'verifyConfirmedPassword' &&
442 this.isParentMessage_(e
)) {
443 this.onVerifyConfirmedPassword_(msg
.password
);
444 } else if (msg
.method
== 'redirectToSignin' &&
445 this.isParentMessage_(e
)) {
446 $('gaia-frame').src
= this.constructInitialFrameUrl_();
448 console
.error('Authenticator.onMessage: unknown message + origin!?');
453 Authenticator
.getInstance().initialize();