blob: 1c61a2448150e9d110e5aa9ae4682331e695b599 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* Authenticator class wraps the communications between Gaia and its host.
*/
function Authenticator() {
}
/**
* Gaia auth extension url origin.
* @type {string}
*/
Authenticator.THIS_EXTENSION_ORIGIN =
'chrome-extension://mfffpogegjflfpflabcdkioaeobkgjik';
/**
* The lowest version of the credentials passing API supported.
* @type {number}
*/
Authenticator.MIN_API_VERSION_VERSION = 1;
/**
* The highest version of the credentials passing API supported.
* @type {number}
*/
Authenticator.MAX_API_VERSION_VERSION = 1;
/**
* The key types supported by the credentials passing API.
* @type {Array} Array of strings.
*/
Authenticator.API_KEY_TYPES = [
'KEY_TYPE_PASSWORD_PLAIN',
];
/**
* Singleton getter of Authenticator.
* @return {Object} The singleton instance of Authenticator.
*/
Authenticator.getInstance = function() {
if (!Authenticator.instance_) {
Authenticator.instance_ = new Authenticator();
}
return Authenticator.instance_;
};
Authenticator.prototype = {
email_: null,
// Depending on the key type chosen, this will contain the plain text password
// or a credential derived from it along with the information required to
// repeat the derivation, such as a salt. The information will be encoded so
// that it contains printable ASCII characters only. The exact encoding is TBD
// when support for key types other than plain text password is added.
passwordBytes_: null,
attemptToken_: null,
// Input params from extension initialization URL.
inputLang_: undefined,
intputEmail_: undefined,
isSAMLFlow_: false,
gaiaLoaded_: false,
supportChannel_: null,
GAIA_URL: 'https://accounts.google.com/',
GAIA_PAGE_PATH: 'ServiceLogin?skipvpage=true&sarp=1&rm=hide',
PARENT_PAGE: 'chrome://oobe/',
SERVICE_ID: 'chromeoslogin',
CONTINUE_URL: Authenticator.THIS_EXTENSION_ORIGIN + '/success.html',
CONSTRAINED_FLOW_SOURCE: 'chrome',
initialize: function() {
var params = getUrlSearchParams(location.search);
this.parentPage_ = params.parentPage || this.PARENT_PAGE;
this.gaiaUrl_ = params.gaiaUrl || this.GAIA_URL;
this.gaiaPath_ = params.gaiaPath || this.GAIA_PAGE_PATH;
this.inputLang_ = params.hl;
this.inputEmail_ = params.email;
this.service_ = params.service || this.SERVICE_ID;
this.continueUrl_ = params.continueUrl || this.CONTINUE_URL;
this.desktopMode_ = params.desktopMode == '1';
this.isConstrainedWindow_ = params.constrained == '1';
this.initialFrameUrl_ = params.frameUrl || this.constructInitialFrameUrl_();
this.initialFrameUrlWithoutParams_ = stripParams(this.initialFrameUrl_);
// For CrOS 'ServiceLogin' we assume that Gaia is loaded if we recieved
// 'clearOldAttempts' message. For other scenarios Gaia doesn't send this
// message so we have to rely on 'load' event.
// TODO(dzhioev): Do not rely on 'load' event after b/16313327 is fixed.
this.assumeLoadedOnLoadEvent_ =
this.gaiaPath_.indexOf('ServiceLogin') !== 0 ||
this.service_ !== 'chromeoslogin';
document.addEventListener('DOMContentLoaded', this.onPageLoad_.bind(this));
},
isGaiaMessage_: function(msg) {
// Not quite right, but good enough.
return this.gaiaUrl_.indexOf(msg.origin) == 0 ||
this.GAIA_URL.indexOf(msg.origin) == 0;
},
isInternalMessage_: function(msg) {
return msg.origin == Authenticator.THIS_EXTENSION_ORIGIN;
},
isParentMessage_: function(msg) {
return msg.origin == this.parentPage_;
},
constructInitialFrameUrl_: function() {
var url = this.gaiaUrl_ + this.gaiaPath_;
url = appendParam(url, 'service', this.service_);
url = appendParam(url, 'continue', this.continueUrl_);
if (this.inputLang_)
url = appendParam(url, 'hl', this.inputLang_);
if (this.inputEmail_)
url = appendParam(url, 'Email', this.inputEmail_);
if (this.isConstrainedWindow_)
url = appendParam(url, 'source', this.CONSTRAINED_FLOW_SOURCE);
return url;
},
onPageLoad_: function() {
window.addEventListener('message', this.onMessage.bind(this), false);
this.initSupportChannel_();
var gaiaFrame = $('gaia-frame');
gaiaFrame.src = this.initialFrameUrl_;
if (this.assumeLoadedOnLoadEvent_) {
var handler = function() {
gaiaFrame.removeEventListener('load', handler);
if (!this.gaiaLoaded_) {
this.gaiaLoaded_ = true;
this.maybeInitialized_();
}
}.bind(this);
gaiaFrame.addEventListener('load', handler);
}
},
initSupportChannel_: function() {
var supportChannel = new Channel();
supportChannel.connect('authMain');
supportChannel.registerMessage('channelConnected', function() {
if (this.supportChannel_) {
console.error('Support channel is already initialized.');
return;
}
this.supportChannel_ = supportChannel;
if (this.desktopMode_) {
this.supportChannel_.send({
name: 'initDesktopFlow',
gaiaUrl: this.gaiaUrl_,
continueUrl: stripParams(this.continueUrl_),
isConstrainedWindow: this.isConstrainedWindow_
});
this.supportChannel_.registerMessage(
'switchToFullTab', this.switchToFullTab_.bind(this));
this.supportChannel_.registerMessage(
'completeLogin', this.completeLogin_.bind(this));
}
this.initSAML_();
this.maybeInitialized_();
}.bind(this));
window.setTimeout(function() {
if (!this.supportChannel_) {
// Re-initialize the channel if it is not connected properly, e.g.
// connect may be called before background script started running.
this.initSupportChannel_();
}
}.bind(this), 200);
},
/**
* Called when one of the initialization stages has finished. If all the
* needed parts are initialized, notifies parent about successfull
* initialization.
*/
maybeInitialized_: function() {
if (!this.gaiaLoaded_ || !this.supportChannel_)
return;
var msg = {
'method': 'loginUILoaded'
};
window.parent.postMessage(msg, this.parentPage_);
},
/**
* Invoked when the background script sends a message to indicate that the
* current content does not fit in a constrained window.
* @param {Object=} opt_extraMsg Optional extra info to send.
*/
switchToFullTab_: function(msg) {
var parentMsg = {
'method': 'switchToFullTab',
'url': msg.url
};
window.parent.postMessage(parentMsg, this.parentPage_);
},
/**
* Invoked when the signin flow is complete.
* @param {Object=} opt_extraMsg Optional extra info to send.
*/
completeLogin_: function(opt_extraMsg) {
var msg = {
'method': 'completeLogin',
'email': (opt_extraMsg && opt_extraMsg.email) || this.email_,
'password': (opt_extraMsg && opt_extraMsg.password) ||
this.passwordBytes_,
'usingSAML': this.isSAMLFlow_,
'chooseWhatToSync': this.chooseWhatToSync_ || false,
'skipForNow': opt_extraMsg && opt_extraMsg.skipForNow,
'sessionIndex': opt_extraMsg && opt_extraMsg.sessionIndex
};
window.parent.postMessage(msg, this.parentPage_);
this.supportChannel_.send({name: 'resetAuth'});
},
/**
* Invoked when support channel is connected.
*/
initSAML_: function() {
this.isSAMLFlow_ = false;
this.supportChannel_.registerMessage(
'onAuthPageLoaded', this.onAuthPageLoaded_.bind(this));
this.supportChannel_.registerMessage(
'onInsecureContentBlocked', this.onInsecureContentBlocked_.bind(this));
this.supportChannel_.registerMessage(
'apiCall', this.onAPICall_.bind(this));
this.supportChannel_.send({
name: 'setGaiaUrl',
gaiaUrl: this.gaiaUrl_
});
if (!this.desktopMode_ && this.gaiaUrl_.indexOf('https://') == 0) {
// Abort the login flow when content served over an unencrypted connection
// is detected on Chrome OS. This does not apply to tests that explicitly
// set a non-https GAIA URL and want to perform all authentication over
// http.
this.supportChannel_.send({
name: 'setBlockInsecureContent',
blockInsecureContent: true
});
}
},
/**
* Invoked when the background page sends 'onHostedPageLoaded' message.
* @param {!Object} msg Details sent with the message.
*/
onAuthPageLoaded_: function(msg) {
var isSAMLPage = msg.url.indexOf(this.gaiaUrl_) != 0;
if (isSAMLPage && !this.isSAMLFlow_) {
// GAIA redirected to a SAML login page. The credentials provided to this
// page will determine what user gets logged in. The credentials obtained
// from the GAIA login form are no longer relevant and can be discarded.
this.isSAMLFlow_ = true;
this.email_ = null;
this.passwordBytes_ = null;
}
window.parent.postMessage({
'method': 'authPageLoaded',
'isSAML': this.isSAMLFlow_,
'domain': extractDomain(msg.url)
}, this.parentPage_);
},
/**
* Invoked when the background page sends an 'onInsecureContentBlocked'
* message.
* @param {!Object} msg Details sent with the message.
*/
onInsecureContentBlocked_: function(msg) {
window.parent.postMessage({
'method': 'insecureContentBlocked',
'url': stripParams(msg.url)
}, this.parentPage_);
},
/**
* Invoked when one of the credential passing API methods is called by a SAML
* provider.
* @param {!Object} msg Details of the API call.
*/
onAPICall_: function(msg) {
var call = msg.call;
if (call.method == 'initialize') {
if (!Number.isInteger(call.requestedVersion) ||
call.requestedVersion < Authenticator.MIN_API_VERSION_VERSION) {
this.sendInitializationFailure_();
return;
}
this.apiVersion_ = Math.min(call.requestedVersion,
Authenticator.MAX_API_VERSION_VERSION);
this.initialized_ = true;
this.sendInitializationSuccess_();
return;
}
if (call.method == 'add') {
if (Authenticator.API_KEY_TYPES.indexOf(call.keyType) == -1) {
console.error('Authenticator.onAPICall_: unsupported key type');
return;
}
this.apiToken_ = call.token;
this.email_ = call.user;
this.passwordBytes_ = call.passwordBytes;
} else if (call.method == 'confirm') {
if (call.token != this.apiToken_)
console.error('Authenticator.onAPICall_: token mismatch');
} else {
console.error('Authenticator.onAPICall_: unknown message');
}
},
sendInitializationSuccess_: function() {
this.supportChannel_.send({name: 'apiResponse', response: {
result: 'initialized',
version: this.apiVersion_,
keyTypes: Authenticator.API_KEY_TYPES
}});
},
sendInitializationFailure_: function() {
this.supportChannel_.send({
name: 'apiResponse',
response: {result: 'initialization_failed'}
});
},
onConfirmLogin_: function() {
if (!this.isSAMLFlow_) {
this.completeLogin_();
return;
}
var apiUsed = !!this.passwordBytes_;
// Retrieve the e-mail address of the user who just authenticated from GAIA.
window.parent.postMessage({method: 'retrieveAuthenticatedUserEmail',
attemptToken: this.attemptToken_,
apiUsed: apiUsed},
this.parentPage_);
if (!apiUsed) {
this.supportChannel_.sendWithCallback(
{name: 'getScrapedPasswords'},
function(passwords) {
if (passwords.length == 0) {
window.parent.postMessage(
{method: 'noPassword', email: this.email_},
this.parentPage_);
} else {
window.parent.postMessage({method: 'confirmPassword',
email: this.email_,
passwordCount: passwords.length},
this.parentPage_);
}
}.bind(this));
}
},
maybeCompleteSAMLLogin_: function() {
// SAML login is complete when the user's e-mail address has been retrieved
// from GAIA and the user has successfully confirmed the password.
if (this.email_ !== null && this.passwordBytes_ !== null)
this.completeLogin_();
},
onVerifyConfirmedPassword_: function(password) {
this.supportChannel_.sendWithCallback(
{name: 'getScrapedPasswords'},
function(passwords) {
for (var i = 0; i < passwords.length; ++i) {
if (passwords[i] == password) {
this.passwordBytes_ = passwords[i];
this.maybeCompleteSAMLLogin_();
return;
}
}
window.parent.postMessage(
{method: 'confirmPassword', email: this.email_},
this.parentPage_);
}.bind(this));
},
onMessage: function(e) {
var msg = e.data;
if (msg.method == 'attemptLogin' && this.isGaiaMessage_(e)) {
this.email_ = msg.email;
this.passwordBytes_ = msg.password;
this.attemptToken_ = msg.attemptToken;
this.chooseWhatToSync_ = msg.chooseWhatToSync;
this.isSAMLFlow_ = false;
if (this.supportChannel_)
this.supportChannel_.send({name: 'startAuth'});
else
console.error('Support channel is not initialized.');
} else if (msg.method == 'clearOldAttempts' && this.isGaiaMessage_(e)) {
if (!this.gaiaLoaded_) {
this.gaiaLoaded_ = true;
this.maybeInitialized_();
}
this.email_ = null;
this.passwordBytes_ = null;
this.attemptToken_ = null;
this.isSAMLFlow_ = false;
if (this.supportChannel_)
this.supportChannel_.send({name: 'resetAuth'});
} else if (msg.method == 'setAuthenticatedUserEmail' &&
this.isParentMessage_(e)) {
if (this.attemptToken_ == msg.attemptToken) {
this.email_ = msg.email;
this.maybeCompleteSAMLLogin_();
}
} else if (msg.method == 'confirmLogin' && this.isInternalMessage_(e)) {
if (this.attemptToken_ == msg.attemptToken)
this.onConfirmLogin_();
else
console.error('Authenticator.onMessage: unexpected attemptToken!?');
} else if (msg.method == 'verifyConfirmedPassword' &&
this.isParentMessage_(e)) {
this.onVerifyConfirmedPassword_(msg.password);
} else if (msg.method == 'redirectToSignin' &&
this.isParentMessage_(e)) {
$('gaia-frame').src = this.constructInitialFrameUrl_();
} else {
console.error('Authenticator.onMessage: unknown message + origin!?');
}
}
};
Authenticator.getInstance().initialize();