(function () { /* Imports */ var Meteor = Package.meteor.Meteor; var check = Package.check.check; var Match = Package.check.Match; var _ = Package.underscore._; var RoutePolicy = Package.routepolicy.RoutePolicy; var WebApp = Package.webapp.WebApp; var main = Package.webapp.main; var WebAppInternals = Package.webapp.WebAppInternals; var MongoInternals = Package.mongo.MongoInternals; var Mongo = Package.mongo.Mongo; var ServiceConfiguration = Package['service-configuration'].ServiceConfiguration; var Log = Package.logging.Log; var URL = Package.url.URL; /* Package-scope variables */ var OAuth, OAuthTest, Oauth; (function(){ //////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/oauth/oauth_server.js // // // //////////////////////////////////////////////////////////////////////////////////////////////////// // var Fiber = Npm.require('fibers'); // 1 var url = Npm.require('url'); // 2 // 3 OAuth = {}; // 4 OAuthTest = {}; // 5 // 6 RoutePolicy.declare('/_oauth/', 'network'); // 7 // 8 var registeredServices = {}; // 9 // 10 // Internal: Maps from service version to handler function. The // 11 // 'oauth1' and 'oauth2' packages manipulate this directly to register // 12 // for callbacks. // 13 OAuth._requestHandlers = {}; // 14 // 15 // 16 // Register a handler for an OAuth service. The handler will be called // 17 // when we get an incoming http request on /_oauth/{serviceName}. This // 18 // handler should use that information to fetch data about the user // 19 // logging in. // 20 // // 21 // @param name {String} e.g. "google", "facebook" // 22 // @param version {Number} OAuth version (1 or 2) // 23 // @param urls For OAuth1 only, specify the service's urls // 24 // @param handleOauthRequest {Function(oauthBinding|query)} // 25 // - (For OAuth1 only) oauthBinding {OAuth1Binding} bound to the appropriate provider // 26 // - (For OAuth2 only) query {Object} parameters passed in query string // 27 // - return value is: // 28 // - {serviceData:, (optional options:)} where serviceData should end // 29 // up in the user's services[name] field // 30 // - `null` if the user declined to give permissions // 31 // // 32 OAuth.registerService = function (name, version, urls, handleOauthRequest) { // 33 if (registeredServices[name]) // 34 throw new Error("Already registered the " + name + " OAuth service"); // 35 // 36 registeredServices[name] = { // 37 serviceName: name, // 38 version: version, // 39 urls: urls, // 40 handleOauthRequest: handleOauthRequest // 41 }; // 42 }; // 43 // 44 // For test cleanup. // 45 OAuthTest.unregisterService = function (name) { // 46 delete registeredServices[name]; // 47 }; // 48 // 49 // 50 OAuth.retrieveCredential = function(credentialToken, credentialSecret) { // 51 return OAuth._retrievePendingCredential(credentialToken, credentialSecret); // 52 }; // 53 // 54 // 55 // The state parameter is normally generated on the client using // 56 // `btoa`, but for tests we need a version that runs on the server. // 57 // // 58 OAuth._generateState = function (loginStyle, credentialToken, redirectUrl) { // 59 return new Buffer(JSON.stringify({ // 60 loginStyle: loginStyle, // 61 credentialToken: credentialToken, // 62 redirectUrl: redirectUrl})).toString('base64'); // 63 }; // 64 // 65 OAuth._stateFromQuery = function (query) { // 66 var string; // 67 try { // 68 string = new Buffer(query.state, 'base64').toString('binary'); // 69 } catch (e) { // 70 Log.warn('Unable to base64 decode state from OAuth query: ' + query.state); // 71 throw e; // 72 } // 73 // 74 try { // 75 return JSON.parse(string); // 76 } catch (e) { // 77 Log.warn('Unable to parse state from OAuth query: ' + string); // 78 throw e; // 79 } // 80 }; // 81 // 82 OAuth._loginStyleFromQuery = function (query) { // 83 var style; // 84 // For backwards-compatibility for older clients, catch any errors // 85 // that result from parsing the state parameter. If we can't parse it, // 86 // set login style to popup by default. // 87 try { // 88 style = OAuth._stateFromQuery(query).loginStyle; // 89 } catch (err) { // 90 style = "popup"; // 91 } // 92 if (style !== "popup" && style !== "redirect") { // 93 throw new Error("Unrecognized login style: " + style); // 94 } // 95 return style; // 96 }; // 97 // 98 OAuth._credentialTokenFromQuery = function (query) { // 99 var state; // 100 // For backwards-compatibility for older clients, catch any errors // 101 // that result from parsing the state parameter. If we can't parse it, // 102 // assume that the state parameter's value is the credential token, as // 103 // it used to be for older clients. // 104 try { // 105 state = OAuth._stateFromQuery(query); // 106 } catch (err) { // 107 return query.state; // 108 } // 109 return state.credentialToken; // 110 }; // 111 // 112 OAuth._isCordovaFromQuery = function (query) { // 113 try { // 114 return !! OAuth._stateFromQuery(query).isCordova; // 115 } catch (err) { // 116 // For backwards-compatibility for older clients, catch any errors // 117 // that result from parsing the state parameter. If we can't parse // 118 // it, assume that we are not on Cordova, since older Meteor didn't // 119 // do Cordova. // 120 return false; // 121 } // 122 }; // 123 // 124 // Checks if the `redirectUrl` matches the app host. // 125 // We export this function so that developers can override this // 126 // behavior to allow apps from external domains to login using the // 127 // redirect OAuth flow. // 128 OAuth._checkRedirectUrlOrigin = function (redirectUrl) { // 129 var appHost = Meteor.absoluteUrl(); // 130 var appHostReplacedLocalhost = Meteor.absoluteUrl(undefined, { // 131 replaceLocalhost: true // 132 }); // 133 return ( // 134 redirectUrl.substr(0, appHost.length) !== appHost && // 135 redirectUrl.substr(0, appHostReplacedLocalhost.length) !== appHostReplacedLocalhost // 136 ); // 137 }; // 138 // 139 // 140 // Listen to incoming OAuth http requests // 141 WebApp.connectHandlers.use(function(req, res, next) { // 142 // Need to create a Fiber since we're using synchronous http calls and nothing // 143 // else is wrapping this in a fiber automatically // 144 Fiber(function () { // 145 middleware(req, res, next); // 146 }).run(); // 147 }); // 148 // 149 var middleware = function (req, res, next) { // 150 // Make sure to catch any exceptions because otherwise we'd crash // 151 // the runner // 152 try { // 153 var serviceName = oauthServiceName(req); // 154 if (!serviceName) { // 155 // not an oauth request. pass to next middleware. // 156 next(); // 157 return; // 158 } // 159 // 160 var service = registeredServices[serviceName]; // 161 // 162 // Skip everything if there's no service set by the oauth middleware // 163 if (!service) // 164 throw new Error("Unexpected OAuth service " + serviceName); // 165 // 166 // Make sure we're configured // 167 ensureConfigured(serviceName); // 168 // 169 var handler = OAuth._requestHandlers[service.version]; // 170 if (!handler) // 171 throw new Error("Unexpected OAuth version " + service.version); // 172 handler(service, req.query, res); // 173 } catch (err) { // 174 // if we got thrown an error, save it off, it will get passed to // 175 // the appropriate login call (if any) and reported there. // 176 // // 177 // The other option would be to display it in the popup tab that // 178 // is still open at this point, ignoring the 'close' or 'redirect' // 179 // we were passed. But then the developer wouldn't be able to // 180 // style the error or react to it in any way. // 181 if (req.query.state && err instanceof Error) { // 182 try { // catch any exceptions to avoid crashing runner // 183 OAuth._storePendingCredential(OAuth._credentialTokenFromQuery(req.query), err); // 184 } catch (err) { // 185 // Ignore the error and just give up. If we failed to store the // 186 // error, then the login will just fail with a generic error. // 187 Log.warn("Error in OAuth Server while storing pending login result.\n" + // 188 err.stack || err.message); // 189 } // 190 } // 191 // 192 // close the popup. because nobody likes them just hanging // 193 // there. when someone sees this multiple times they might // 194 // think to check server logs (we hope?) // 195 // Catch errors because any exception here will crash the runner. // 196 try { // 197 OAuth._endOfLoginResponse(res, { // 198 query: req.query, // 199 loginStyle: OAuth._loginStyleFromQuery(req.query), // 200 error: err // 201 }); // 202 } catch (err) { // 203 Log.warn("Error generating end of login response\n" + // 204 (err && (err.stack || err.message))); // 205 } // 206 } // 207 }; // 208 // 209 OAuthTest.middleware = middleware; // 210 // 211 // Handle /_oauth/* paths and extract the service name. // 212 // // 213 // @returns {String|null} e.g. "facebook", or null if this isn't an // 214 // oauth request // 215 var oauthServiceName = function (req) { // 216 // req.url will be "/_oauth/" with an optional "?close". // 217 var i = req.url.indexOf('?'); // 218 var barePath; // 219 if (i === -1) // 220 barePath = req.url; // 221 else // 222 barePath = req.url.substring(0, i); // 223 var splitPath = barePath.split('/'); // 224 // 225 // Any non-oauth request will continue down the default // 226 // middlewares. // 227 if (splitPath[1] !== '_oauth') // 228 return null; // 229 // 230 // Find service based on url // 231 var serviceName = splitPath[2]; // 232 return serviceName; // 233 }; // 234 // 235 // Make sure we're configured // 236 var ensureConfigured = function(serviceName) { // 237 if (!ServiceConfiguration.configurations.findOne({service: serviceName})) { // 238 throw new ServiceConfiguration.ConfigError(); // 239 } // 240 }; // 241 // 242 var isSafe = function (value) { // 243 // This matches strings generated by `Random.secret` and // 244 // `Random.id`. // 245 return typeof value === "string" && // 246 /^[a-zA-Z0-9\-_]+$/.test(value); // 247 }; // 248 // 249 // Internal: used by the oauth1 and oauth2 packages // 250 OAuth._renderOauthResults = function(res, query, credentialSecret) { // 251 // For tests, we support the `only_credential_secret_for_test` // 252 // parameter, which just returns the credential secret without any // 253 // surrounding HTML. (The test needs to be able to easily grab the // 254 // secret and use it to log in.) // 255 // // 256 // XXX only_credential_secret_for_test could be useful for other // 257 // things beside tests, like command-line clients. We should give it a // 258 // real name and serve the credential secret in JSON. // 259 // 260 if (query.only_credential_secret_for_test) { // 261 res.writeHead(200, {'Content-Type': 'text/html'}); // 262 res.end(credentialSecret, 'utf-8'); // 263 } else { // 264 var details = { // 265 query: query, // 266 loginStyle: OAuth._loginStyleFromQuery(query) // 267 }; // 268 if (query.error) { // 269 details.error = query.error; // 270 } else { // 271 var token = OAuth._credentialTokenFromQuery(query); // 272 var secret = credentialSecret; // 273 if (token && secret && // 274 isSafe(token) && isSafe(secret)) { // 275 details.credentials = { token: token, secret: secret}; // 276 } else { // 277 details.error = "invalid_credential_token_or_secret"; // 278 } // 279 } // 280 // 281 OAuth._endOfLoginResponse(res, details); // 282 } // 283 }; // 284 // 285 // This "template" (not a real Spacebars template, just an HTML file // 286 // with some ##PLACEHOLDER##s) communicates the credential secret back // 287 // to the main window and then closes the popup. // 288 OAuth._endOfPopupResponseTemplate = Assets.getText( // 289 "end_of_popup_response.html"); // 290 // 291 OAuth._endOfRedirectResponseTemplate = Assets.getText( // 292 "end_of_redirect_response.html"); // 293 // 294 // Renders the end of login response template into some HTML and JavaScript // 295 // that closes the popup or redirects at the end of the OAuth flow. // 296 // // 297 // options are: // 298 // - loginStyle ("popup" or "redirect") // 299 // - setCredentialToken (boolean) // 300 // - credentialToken // 301 // - credentialSecret // 302 // - redirectUrl // 303 // - isCordova (boolean) // 304 // // 305 var renderEndOfLoginResponse = function (options) { // 306 // It would be nice to use Blaze here, but it's a little tricky // 307 // because our mustaches would be inside a