Commit d14eb558 authored by bartfab@chromium.org's avatar bartfab@chromium.org

Add credential passing API for Chrome OS SAML login

This CL adds an API that SAML IdPs can use to pass credentials to
Chrome OS during login instead of relying on password scraping.

BUG=127095
TEST=New browser test + manual

Review URL: https://codereview.chromium.org/138133006

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@245480 0039d316-1c4b-4281-b951-d872f2087c98
parent ef444425
...@@ -4,19 +4,29 @@ ...@@ -4,19 +4,29 @@
/** /**
* @fileoverview * @fileoverview
* The background script of auth extension that bridges the communications * A background script of the auth extension that bridges the communication
* between main and injected script. * between the main and injected scripts.
* Here are the communications along a SAML sign-in flow: *
* 1. Main script sends an 'onAuthStarted' signal to indicate the authentication * Here is an overview of the communication flow when SAML is being used:
* flow is started and SAML pages might be loaded from now on; * 1. The main script sends the |startAuth| signal to this background script,
* 2. After the 'onAuthTstarted' signal, injected script starts to scraping * indicating that the authentication flow has started and SAML pages may be
* all password fields on normal page (i.e. http or https) and sends page * loaded from now on.
* load signal as well as the passwords to the background script here; * 2. A script is injected into each SAML page. The injected script sends three
* main types of messages to this background script:
* a) A |pageLoaded| message is sent when the page has been loaded. This is
* forwarded to the main script as |onAuthPageLoaded|.
* b) If the SAML provider supports the credential passing API, the API calls
* are sent to this backgroudn script as |apiCall| messages. These
* messages are forwarded unmodified to the main script.
* c) The injected script scrapes passwords. They are sent to this background
* script in |updatePassword| messages. The main script can request a list
* of the scraped passwords by sending the |getScrapedPasswords| message.
*/ */
/** /**
* BackgroundBridge holds the main script's state and the scraped passwords * BackgroundBridge allows the main script and the injected script to
* from the injected script to help the two collaborate. * collaborate. It forwards credentials API calls to the main script and
* maintains a list of scraped passwords.
*/ */
function BackgroundBridge() { function BackgroundBridge() {
} }
...@@ -88,6 +98,8 @@ BackgroundBridge.prototype = { ...@@ -88,6 +98,8 @@ BackgroundBridge.prototype = {
setupForInjected_: function(port) { setupForInjected_: function(port) {
this.channelInjected_ = new Channel(); this.channelInjected_ = new Channel();
this.channelInjected_.init(port); this.channelInjected_.init(port);
this.channelInjected_.registerMessage(
'apiCall', this.onAPICall_.bind(this));
this.channelInjected_.registerMessage( this.channelInjected_.registerMessage(
'updatePassword', this.onUpdatePassword_.bind(this)); 'updatePassword', this.onUpdatePassword_.bind(this));
this.channelInjected_.registerMessage( this.channelInjected_.registerMessage(
...@@ -141,6 +153,10 @@ BackgroundBridge.prototype = { ...@@ -141,6 +153,10 @@ BackgroundBridge.prototype = {
return Object.keys(passwords); return Object.keys(passwords);
}, },
onAPICall_: function(msg) {
this.channelMain_.send(msg);
},
onUpdatePassword_: function(msg) { onUpdatePassword_: function(msg) {
if (!this.authStarted_) if (!this.authStarted_)
return; return;
......
...@@ -204,6 +204,8 @@ Authenticator.prototype = { ...@@ -204,6 +204,8 @@ Authenticator.prototype = {
this.samlSupportChannel_.connect('authMain'); this.samlSupportChannel_.connect('authMain');
this.samlSupportChannel_.registerMessage( this.samlSupportChannel_.registerMessage(
'onAuthPageLoaded', this.onAuthPageLoaded_.bind(this)); 'onAuthPageLoaded', this.onAuthPageLoaded_.bind(this));
this.samlSupportChannel_.registerMessage(
'apiCall', this.onAPICall_.bind(this));
this.samlSupportChannel_.send({ this.samlSupportChannel_.send({
name: 'setGaiaUrl', name: 'setGaiaUrl',
gaiaUrl: this.gaiaUrl_ gaiaUrl: this.gaiaUrl_
...@@ -233,6 +235,25 @@ Authenticator.prototype = { ...@@ -233,6 +235,25 @@ Authenticator.prototype = {
}, this.parentPage_); }, 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 == 'add') {
this.apiToken_ = call.token;
this.email_ = call.user;
this.password_ = call.password;
} else if (call.method == 'confirm') {
if (call.token != this.apiToken_)
console.error('Authenticator.onAPICall_: token mismatch');
} else {
console.error('Authenticator.onAPICall_: unknown message');
}
},
onLoginUILoaded: function() { onLoginUILoaded: function() {
var msg = { var msg = {
'method': 'loginUILoaded' 'method': 'loginUILoaded'
...@@ -261,19 +282,21 @@ Authenticator.prototype = { ...@@ -261,19 +282,21 @@ Authenticator.prototype = {
attemptToken: this.attemptToken_}, attemptToken: this.attemptToken_},
this.parentPage_); this.parentPage_);
this.samlSupportChannel_.sendWithCallback( if (!this.password_) {
{name: 'getScrapedPasswords'}, this.samlSupportChannel_.sendWithCallback(
function(passwords) { {name: 'getScrapedPasswords'},
if (passwords.length == 0) { function(passwords) {
window.parent.postMessage( if (passwords.length == 0) {
{method: 'noPassword', email: this.email_}, window.parent.postMessage(
this.parentPage_); {method: 'noPassword', email: this.email_},
} else { this.parentPage_);
window.parent.postMessage( } else {
{method: 'confirmPassword', email: this.email_}, window.parent.postMessage(
this.parentPage_); {method: 'confirmPassword', email: this.email_},
} this.parentPage_);
}.bind(this)); }
}.bind(this));
}
}, },
maybeCompleteSAMLLogin_: function() { maybeCompleteSAMLLogin_: function() {
......
...@@ -4,15 +4,57 @@ ...@@ -4,15 +4,57 @@
/** /**
* @fileoverview * @fileoverview
* Script to be injected into SAML provider pages that do not support the * Script to be injected into SAML provider pages, serving three main purposes:
* auth service provider postMessage API. It serves two main purposes:
* 1. Signal hosting extension that an external page is loaded so that the * 1. Signal hosting extension that an external page is loaded so that the
* UI around it could be changed accordingly; * UI around it should be changed accordingly;
* 2. Scrape password and send it back to be used for encrypt user data and * 2. Provide an API via which the SAML provider can pass user credentials to
* use for offline login; * Chrome OS, allowing the password to be used for encrypting user data and
* offline login.
* 3. Scrape password fields, making the password available to Chrome OS even if
* the SAML provider does not support the credential passing API.
*/ */
(function() { (function() {
function APICallForwarder() {
}
/**
* The credential passing API is used by sending messages to the SAML page's
* |window| object. This class forwards the calls to a background script via a
* |Channel|.
*/
APICallForwarder.prototype = {
// Channel to which API calls are forwarded.
channel_: null,
/**
* Initialize the API call forwarder.
* @param {!Object} channel Channel to which API calls should be forwarded.
*/
init: function(channel) {
this.channel_ = channel;
window.addEventListener('message', this.onMessage_.bind(this));
},
onMessage_: function(event) {
if (event.source != window ||
typeof event.data != 'object' ||
!event.data.hasOwnProperty('type') ||
event.data.type != 'gaia_saml_api') {
return;
}
if (event.data.call.method == 'initialize') {
// Respond to the |initialize| call directly.
event.source.postMessage({
type: 'gaia_saml_api_reply',
response: {result: 'initialized', version: 1}}, '/');
} else {
// Forward all other calls.
this.channel_.send({name: 'apiCall', call: event.data.call});
}
}
};
/** /**
* A class to scrape password from type=password input elements under a given * A class to scrape password from type=password input elements under a given
* docRoot and send them back via a Channel. * docRoot and send them back via a Channel.
...@@ -125,6 +167,9 @@ ...@@ -125,6 +167,9 @@
channel.connect('injected'); channel.connect('injected');
channel.send({name: 'pageLoaded', url: pageURL}); channel.send({name: 'pageLoaded', url: pageURL});
apiCallForwarder = new APICallForwarder();
apiCallForwarder.init(channel);
passwordScraper = new PasswordInputScraper(); passwordScraper = new PasswordInputScraper();
passwordScraper.init(channel, pageURL, document.documentElement); passwordScraper.init(channel, pageURL, document.documentElement);
} }
......
<html>
<head>
<script type="text/javascript">
function initialize() {
window.setTimeout(function() {
window.postMessage({
type: 'gaia_saml_api',
call: {method: 'initialize'}}, '/');
}, 0);
}
function send_and_submit() {
var form = document.forms[0];
var token = form.elements['RelayState'].value;
var user = form.elements['Email'].value;
var password = form.elements['Password'].value;
window.setTimeout(function() {
window.postMessage({
type: 'gaia_saml_api',
call: {method: 'add',
token: token,
user: user,
password: password}}, '/');
form.submit();
}, 0);
}
</script>
</head>
<body onload="initialize();">
<form method=post action="$Post">
<input type=hidden name=RelayState value="$RelayState">
User: <input type=text id=Email name=Email>
Password: <input type=password id=Password name=Password>
<input id=Submit type=button value="Login" onclick="send_and_submit();"/>
</form>
</body>
</html>
<html>
<head>
<script type="text/javascript">
function send_and_submit() {
var form = document.forms[0];
var token = form.elements['RelayState'].value;
window.setTimeout(function() {
window.postMessage({
type: 'gaia_saml_api',
call: {method: 'confirm', token: token}}, '/');
form.submit();
}, 0);
}
</script>
</head>
<body onload="send_and_submit();">
<form method=post action="$Post">
<input type=hidden name=SAMLResponse value="fake_response"/>
<input type=hidden name=RelayState value="$RelayState">
</form>
</body>
</html>
<html>
<head></head>
<body>
<form method=post action="$Post">
<input type=hidden name=RelayState value="$RelayState">
User: <input type=text id=Email name=Email>
Password: <input type=password id=Password name=Password>
<input id=Submit type=submit>
</form>
</body>
</html>
<html>
<head></head>
<body>
<form method=post action="$Post">
<input type=hidden name=RelayState value="$RelayState">
User: <input type=text id=Email name=Email>
<input id=Submit type=submit>
</form>
</body>
</html>
<html>
<head></head>
<body>
<form method=post action="$Post">
<input type=hidden name=RelayState value="$RelayState">
User: <input type=text id=Email name=Email>
Password: <input type=password id=Password name=Password>
Password: <input type=password id=Password1 name=Password1>
<input id=Submit type=submit>
</form>
</body>
</html>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment