mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
};
|
||||
|
||||
@@ -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
|
||||
// '&'). 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, "&").
|
||||
replace(/</g, "<").
|
||||
replace(/>/g, ">").
|
||||
replace(/\"/g, """).
|
||||
replace(/\'/g, "'").
|
||||
replace(/\//g, "/");
|
||||
} 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");
|
||||
};
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user