Clean up OAuth redirect and Cordova flows.

* Be less XSS-able by HTML-encoding 'config' in the end-of-login
  responses and reading it out of the DOM.
* Thread 'isCordova' through to end-of-login responses. Trying to do a
  'window.close' in Cordova, even in a try/catch, breaks the in-app
  browser.
* Remove some stray 'console.log's.
This commit is contained in:
Emily Stark
2014-08-28 21:37:52 -07:00
parent ef25e736d5
commit 9ba7a6f117
5 changed files with 132 additions and 82 deletions

View File

@@ -1,9 +1,14 @@
<html>
<head>
<script type="text/javascript">
if (##SET_CREDENTIAL_TOKEN##) {
var credentialToken = ##TOKEN##;
var credentialSecret = ##SECRET##;
function storeAndClose() {
var config = JSON.parse(document.getElementById("config").innerHTML);
if (config.setCredentialToken) {
var credentialToken = config.credentialToken;
var credentialSecret = config.credentialSecret;
var credentialString = JSON.stringify({
credentialToken: credentialToken,
@@ -18,20 +23,26 @@
credentialToken, credentialSecret);
} else {
try {
localStorage[##LOCAL_STORAGE_PREFIX## + credentialToken] = credentialSecret;
localStorage[config.storagePrefix + credentialToken] = credentialSecret;
} catch (err) {
// We can't do much else, but at least close the popup instead
// of having it hang around on a blank page.
}
}
}
//window.close();
if (! config.isCordova) {
window.close();
}
};
</script>
</head>
<body>
<body onload="storeAndClose()">
<p>
Login completed. <a href="#" onclick="window.close()">
Click here</a> to close this window.
</p>
<div id="config" style="display:none;">##CONFIG##</div>
</body>
</html>

View File

@@ -1,17 +1,24 @@
<html>
<head>
<script type="text/javascript">
var config = ##CONFIG##;
if (config.setCredentialToken) {
sessionStorage[config.sessionStoragePrefix + config.credentialToken] =
function storeAndRedirect () {
var config = JSON.parse(document.getElementById("config").innerHTML);
if (config.setCredentialToken) {
sessionStorage[config.storagePrefix + config.credentialToken] =
config.credentialSecret;
}
}
window.location = config.redirectUrl;
};
window.location = config.redirectUrl;
</script>
</head>
<body>
<body onload="storeAndRedirect()">
<h1>Logging In</h1>
<div id="config" style="display:none;">##CONFIG##</div>
</body>
</html>

View File

@@ -13,6 +13,11 @@ OAuth.showPopup = function (url, callback, dimensions) {
//
//
OAuth._loginStyle = function (service, config, options) {
if (Meteor.isCordova) {
return "popup";
}
var loginStyle = (options && options.loginStyle) || config.loginStyle || 'popup';
if (! _.contains(["popup", "redirect"], loginStyle))
@@ -34,9 +39,10 @@ OAuth._loginStyle = function (service, config, options) {
};
OAuth._stateParam = function (loginStyle, credentialToken) {
state = {
var state = {
loginStyle: loginStyle,
credentialToken: credentialToken
credentialToken: credentialToken,
isCordova: Meteor.isCordova
};
if (loginStyle === 'redirect')
@@ -88,7 +94,7 @@ OAuth.getDataAfterRedirect = function () {
credentialToken: credentialToken,
credentialSecret: credentialSecret
};
}
};
// Launch an OAuth login flow. For the popup login style, show the
// popup. For the redirect login style, save the credential token for
@@ -155,6 +161,5 @@ OAuth._retrieveCredentialSecret = function (credentialToken) {
} else {
delete credentialSecrets[credentialToken];
}
console.log("new secret: ", secret);
return secret;
};

View File

@@ -9,36 +9,31 @@
// @param dimensions {optional Object(width, height)} The dimensions of
// the popup. If not passed defaults to something sane.
OAuth.showPopup = function (url, callback, dimensions) {
console.log("showing url", url);
var popup = window.open(url, '_blank', 'location=yes,hidden=yes');
popup.addEventListener('loadstart', pageStartLoad);
popup.addEventListener('loadstop', pageLoaded);
popup.addEventListener('loaderror', fail);
popup.addEventListener('exit', close);
popup.show();
var fail = function (err) {
Meteor._debug("Error from OAuth popup:", err);
};
function pageStartLoad (event) {
console.log("page start load", JSON.stringify(event));
}
function fail (err) {
Meteor._debug(err);
}
var pageLoaded = function (event) {
if (event.url.indexOf(Meteor.absoluteUrl('_oauth')) === 0) {
var splitUrl = event.url.split("#");
var hashFragment = splitUrl[1];
function close () {
console.log("close");
}
function pageLoaded (event) {
console.log("loaded", event.url);
console.log("comparing to", Meteor.absoluteUrl('_oauth'));
var url = decodeURI(event.url);
console.log("decoded", url);
if (url.indexOf(Meteor.absoluteUrl('_oauth')) === 0) {
var credentials = JSON.parse(url.split('#')[1]);
if (! hashFragment) {
throw new Error("No hash fragment in OAuth popup?");
}
var credentials = JSON.parse(decodeURIComponent(hashFragment));
OAuth._handleCredentialSecret(credentials.credentialToken,
credentials.credentialSecret);
credentials.credentialSecret);
popup.close();
callback();
}
}
};
};
var popup = window.open(url, '_blank', 'location=yes,hidden=yes');
popup.addEventListener('loadstop', pageLoaded);
popup.addEventListener('loaderror', fail);
popup.show();
};

View File

@@ -81,19 +81,26 @@ OAuth._stateFromQuery = function (query) {
}
OAuth._loginStyleFromQuery = function (query) {
return OAuth._stateFromQuery(query).loginStyle;
var style = OAuth._stateFromQuery(query).loginStyle;
if (style !== "popup" && style !== "redirect") {
throw new Error("Unrecognized login style: " + style);
}
return style;
};
OAuth._credentialTokenFromQuery = function (query) {
return OAuth._stateFromQuery(query).credentialToken;
};
OAuth._isCordovaFromQuery = function (query) {
return !! OAuth._stateFromQuery(query).isCordova;
};
// Listen to incoming OAuth http requests
WebApp.connectHandlers.use(function(req, res, next) {
// Need to create a Fiber since we're using synchronous http calls and nothing
// else is wrapping this in a fiber automatically
console.log(req.url);
Fiber(function () {
middleware(req, res, next);
}).run();
@@ -124,7 +131,6 @@ var middleware = function (req, res, next) {
throw new Error("Unexpected OAuth version " + service.version);
handler(service, req.query, res);
} catch (err) {
console.log("error in middlware:", err.stack, req.url);
// if we got thrown an error, save it off, it will get passed to
// the appropriate login call (if any) and reported there.
//
@@ -231,7 +237,7 @@ OAuth._renderOauthResults = function(res, query, credentialSecret) {
details.error = "invalid_credential_token_or_secret";
}
}
console.log("writing response to client");
OAuth._endOfLoginResponse(res, details);
}
};
@@ -247,40 +253,56 @@ var endOfRedirectResponseTemplate = Assets.getText(
// Renders the end of login response template into some HTML and JavaScript
// that closes the popup or redirects at the end of the OAuth flow.
var renderEndOfLoginResponse = function (loginStyle, setCredentialToken, token, secret, redirectUrl) {
//
// options are:
// - loginStyle ("popup" or "redirect")
// - setCredentialToken (boolean)
// - credentialToken
// - credentialSecret
// - redirectUrl
// - isCordova (boolean)
//
var renderEndOfLoginResponse = function (options) {
// It would be nice to use Blaze here, but it's a little tricky
// because our mustaches would be inside a <script> tag, and Blaze
// would treat the <script> tag contents as text (e.g. encode '&' as
// '&amp;'). So we just do a simple replace.
var result;
if (loginStyle === 'popup') {
result = OAuth._endOfPopupResponseTemplate.replace(
/##SET_CREDENTIAL_TOKEN##/,
JSON.stringify(setCredentialToken));
result = result.replace(
/##TOKEN##/, JSON.stringify(token));
result = result.replace(
/##SECRET##/, JSON.stringify(secret));
result = result.replace(
/##LOCAL_STORAGE_PREFIX##/,
JSON.stringify(OAuth._storageTokenPrefix));
} else if (loginStyle === 'redirect') {
var config = {
sessionStoragePrefix: OAuth._storageTokenPrefix,
setCredentialToken: setCredentialToken,
credentialToken: token,
credentialSecret: secret,
redirectUrl: redirectUrl
};
result = endOfRedirectResponseTemplate.replace(
/##CONFIG##/,
JSON.stringify(config)
);
var escape = function (s) {
if (s) {
return s.replace(/&/g, "&amp;").
replace(/</g, "&lt;").
replace(/>/g, "&gt;").
replace(/\"/g, "&quot;").
replace(/\'/g, "&#x27;").
replace(/\//g, "&#x2F;");
} else {
return s;
}
};
// Escape everything just to be safe (we've already checked that some
// of this data -- the token and secret -- are safe).
var config = {
setCredentialToken: !! options.setCredentialToken,
credentialToken: escape(options.credentialToken),
credentialSecret: escape(options.credentialSecret),
storagePrefix: escape(OAuth._storageTokenPrefix),
redirectUrl: escape(options.redirectUrl),
isCordova: !! options.isCordova
};
var template;
if (options.loginStyle === 'popup') {
template = OAuth._endOfPopupResponseTemplate;
} else if (options.loginStyle === 'redirect') {
template = endOfRedirectResponseTemplate;
} else {
throw new Error('invalid loginStyle: ' + options.loginStyle);
}
else
throw new Error('invalid loginStyle');
console.log("results of login response", result);
var result = template.replace(/##CONFIG##/, JSON.stringify(config));
return "<!DOCTYPE html>\n" + result;
};
@@ -323,28 +345,38 @@ OAuth._endOfLoginResponse = function (res, details) {
redirectUrl = OAuth._stateFromQuery(details.query).redirectUrl;
var appHost = Meteor.absoluteUrl();
if (redirectUrl.substr(0, appHost.length) !== appHost) {
details.error = "redirectUrl (" + redirectUrl + ") is not on the same host as the app (" + appHost + ")";
details.error = "redirectUrl (" + redirectUrl +
") is not on the same host as the app (" + appHost + ")";
redirectUrl = appHost;
}
}
var isCordova = OAuth._isCordovaFromQuery(details.query);
if (details.error) {
Log.warn("Error in OAuth Server: " +
(details.error instanceof Error ?
details.error.message : details.error));
res.end(renderEndOfLoginResponse(details.loginStyle, false, null, null, redirectUrl), "utf-8");
res.end(renderEndOfLoginResponse({
loginStyle: details.loginStyle,
setCredentialToken: false,
redirectUrl: redirectUrl,
isCordova: isCordova
}), "utf-8");
return;
}
// If we have a credentialSecret, report it back to the parent
// window, with the corresponding credentialToken. The parent window
// uses the credentialToken and credentialSecret to log in over DDP.
res.end(renderEndOfLoginResponse(details.loginStyle,
true,
details.credentials.token,
details.credentials.secret,
redirectUrl),
"utf-8");
res.end(renderEndOfLoginResponse({
loginStyle: details.loginStyle,
setCredentialToken: true,
credentialToken: details.credentials.token,
credentialSecret: details.credentials.secret,
redirectUrl: redirectUrl,
isCordova: isCordova
}), "utf-8");
};