mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Merge branch 'auth-twitter' into auth
This commit is contained in:
@@ -12,3 +12,4 @@ accounts-weibo
|
||||
accounts-google
|
||||
accounts-facebook
|
||||
accounts-passwords
|
||||
accounts-twitter
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Meteor.accounts.config({
|
||||
requireEmail: true,
|
||||
requireEmail: false,
|
||||
requireUsername: false,
|
||||
validateEmails: true
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
'&redirect_uri=' + Meteor.accounts.facebook._appUrl + '/_oauth/facebook?close' +
|
||||
'&display=' + display + '&scope=' + scope + '&state=' + state;
|
||||
|
||||
Meteor.accounts.oauth2.initiateLogin(state, loginUrl);
|
||||
Meteor.accounts.oauth.initiateLogin(state, loginUrl);
|
||||
};
|
||||
|
||||
})();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
if (!Meteor.accounts.facebook) {
|
||||
Meteor.accounts.facebook = {};
|
||||
Meteor.accounts.facebook._requireConfigs = ['_appId', '_appUrl'];
|
||||
}
|
||||
|
||||
Meteor.accounts.facebook.config = function(appId, appUrl, options) {
|
||||
|
||||
@@ -4,16 +4,7 @@
|
||||
Meteor.accounts.facebook._secret = secret;
|
||||
};
|
||||
|
||||
Meteor.accounts.oauth2.registerService('facebook', function(query) {
|
||||
if (query.error) {
|
||||
// The user didn't authorize access
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!Meteor.accounts.facebook._appId || !Meteor.accounts.facebook._appUrl)
|
||||
throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.facebook.config first");
|
||||
if (!Meteor.accounts.facebook._secret)
|
||||
throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.facebook.setSecret first");
|
||||
Meteor.accounts.oauth.registerService('facebook', 2, function(query) {
|
||||
|
||||
var accessToken = getAccessToken(query);
|
||||
var identity = getIdentity(accessToken);
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
'&redirect_uri=' + Meteor.accounts.google._appUrl + '/_oauth/google?close' +
|
||||
'&state=' + state;
|
||||
|
||||
Meteor.accounts.oauth2.initiateLogin(state, loginUrl);
|
||||
Meteor.accounts.oauth.initiateLogin(state, loginUrl);
|
||||
};
|
||||
|
||||
}) ();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
if (!Meteor.accounts.google) {
|
||||
Meteor.accounts.google = {};
|
||||
Meteor.accounts.google._requireConfigs = ['_clientId', '_appUrl'];
|
||||
}
|
||||
|
||||
Meteor.accounts.google.config = function(clientId, appUrl, options) {
|
||||
|
||||
@@ -4,16 +4,7 @@
|
||||
Meteor.accounts.google._secret = secret;
|
||||
};
|
||||
|
||||
Meteor.accounts.oauth2.registerService('google', function(query) {
|
||||
if (query.error) {
|
||||
// The user didn't authorize access
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!Meteor.accounts.google._clientId || !Meteor.accounts.google._appUrl)
|
||||
throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.google.config first");
|
||||
if (!Meteor.accounts.google._secret)
|
||||
throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.google.setSecret first");
|
||||
Meteor.accounts.oauth.registerService('google', 2, function(query) {
|
||||
|
||||
var accessToken = getAccessToken(query);
|
||||
var identity = getIdentity(accessToken);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//
|
||||
// @param state {String} The OAuth state generated by the client
|
||||
// @param url {String} url to page
|
||||
Meteor.accounts.oauth2.initiateLogin = function(state, url) {
|
||||
Meteor.accounts.oauth.initiateLogin = function(state, url) {
|
||||
// XXX these dimensions worked well for facebook and google, but
|
||||
// it's sort of weird to have these here. Maybe an optional
|
||||
// argument instead?
|
||||
@@ -22,10 +22,8 @@
|
||||
// nothing should happen.
|
||||
var tryLoginAfterPopupClosed = function(state) {
|
||||
Meteor.apply('login', [
|
||||
{oauth: {version: 2, state: state}}
|
||||
{oauth: {state: state}}
|
||||
], {wait: true}, function(error, result) {
|
||||
// XXX this is the wrong thing to do with the error! It should be
|
||||
// delivered to the user via a callback.
|
||||
if (error)
|
||||
throw error;
|
||||
|
||||
1
packages/accounts-oauth-helper/oauth_common.js
Normal file
1
packages/accounts-oauth-helper/oauth_common.js
Normal file
@@ -0,0 +1 @@
|
||||
Meteor.accounts.oauth = {};
|
||||
181
packages/accounts-oauth-helper/oauth_server.js
Normal file
181
packages/accounts-oauth-helper/oauth_server.js
Normal file
@@ -0,0 +1,181 @@
|
||||
(function () {
|
||||
var connect = __meteor_bootstrap__.require("connect");
|
||||
|
||||
Meteor.accounts.oauth._services = {};
|
||||
|
||||
// Register a handler for an OAuth service. The handler will be called
|
||||
// when we get an incoming http request on /_oauth/{serviceName}. This
|
||||
// handler should use that information to fetch data about the user
|
||||
// logging in.
|
||||
//
|
||||
// @param name {String} e.g. "google", "facebook"
|
||||
// @param version {Number} OAuth version (1 or 2)
|
||||
// @param handleOauthRequest {Function(oauthBinding|query)}
|
||||
// - (For OAuth1 only) oauthBinding {OAuth1Binding} bound to the appropriate provider
|
||||
// - (For OAuth2 only) query {Object} parameters passed in query string
|
||||
// - return value is:
|
||||
// - {options: (options), extra: (optional extra)} (same as the
|
||||
// arguments to Meteor.accounts.updateOrCreateUser)
|
||||
// - `null` if the user declined to give permissions
|
||||
Meteor.accounts.oauth.registerService = function (name, version, handleOauthRequest) {
|
||||
if (Meteor.accounts.oauth._services[name])
|
||||
throw new Error("Already registered the " + name + " OAuth service");
|
||||
|
||||
Meteor.accounts.oauth._services[name] = {
|
||||
serviceName: name,
|
||||
version: version,
|
||||
handleOauthRequest: handleOauthRequest
|
||||
};
|
||||
};
|
||||
|
||||
// When we get an incoming OAuth http request we complete the oauth
|
||||
// handshake, account and token setup before responding. The
|
||||
// results are stored in this map which is then read when the login
|
||||
// method is called. Maps state --> return value of `login`
|
||||
//
|
||||
// XXX we should periodically clear old entries
|
||||
Meteor.accounts.oauth._loginResultForState = {};
|
||||
|
||||
// Listen to calls to `login` with an oauth option set
|
||||
Meteor.accounts.registerLoginHandler(function (options) {
|
||||
if (!options.oauth)
|
||||
return undefined; // don't handle
|
||||
|
||||
var result = Meteor.accounts.oauth._loginResultForState[options.oauth.state];
|
||||
if (result === undefined) // not using `!result` since can be null
|
||||
// We weren't notified of the user authorizing the login.
|
||||
return null;
|
||||
else if (result instanceof Error)
|
||||
// We tried to login, but there was a fatal error. Report it back
|
||||
// to the user.
|
||||
throw result;
|
||||
else
|
||||
return result;
|
||||
});
|
||||
|
||||
// Listen to incoming OAuth http requests
|
||||
__meteor_bootstrap__.app
|
||||
.use(connect.query())
|
||||
.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
|
||||
Fiber(function () {
|
||||
Meteor.accounts.oauth._middleware(req, res, next);
|
||||
}).run();
|
||||
});
|
||||
|
||||
Meteor.accounts.oauth._middleware = function (req, res, next) {
|
||||
// Make sure to catch any exceptions because otherwise we'd crash
|
||||
// the runner
|
||||
try {
|
||||
var serviceName = oauthServiceName(req);
|
||||
if (!serviceName) {
|
||||
// not an oauth request. pass to next middleware.
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
var service = Meteor.accounts.oauth._services[serviceName];
|
||||
|
||||
// Skip everything if there's no service set by the oauth middleware
|
||||
if (!service)
|
||||
throw new Error("Unexpected OAuth service " + serviceName);
|
||||
|
||||
// Make sure we're configured
|
||||
ensureConfigured(serviceName);
|
||||
|
||||
if (service.version === 1)
|
||||
Meteor.accounts.oauth1._handleRequest(service, req.query, res);
|
||||
else if (service.version === 2)
|
||||
Meteor.accounts.oauth2._handleRequest(service, req.query, res);
|
||||
else
|
||||
throw new Error("Unexpected OAuth version " + service.version);
|
||||
} catch (err) {
|
||||
// if we got thrown an error, save it off, it will get passed to
|
||||
// the approporiate login call (if any) and reported there.
|
||||
//
|
||||
// The other option would be to display it in the popup tab that
|
||||
// is still open at this point, ignoring the 'close' or 'redirect'
|
||||
// we were passed. But then the developer wouldn't be able to
|
||||
// style the error or react to it in any way.
|
||||
if (req.query.state && err instanceof Error)
|
||||
Meteor.accounts.oauth._loginResultForState[req.query.state] = err;
|
||||
|
||||
// also log to the server console, so the developer sees it.
|
||||
Meteor._debug("Exception in oauth request handler", err);
|
||||
|
||||
// XXX the following is actually wrong. if someone wants to
|
||||
// redirect rather than close once we are done with the OAuth
|
||||
// flow, as supported by
|
||||
// Meteor.accounts.oauth_renderOauthResults, this will still
|
||||
// close the popup instead. Once we fully support the redirect
|
||||
// flow (by supporting that in places such as
|
||||
// packages/facebook/facebook_client.js) we should revisit this.
|
||||
//
|
||||
// close the popup. because nobody likes them just hanging
|
||||
// there. when someone sees this multiple times they might
|
||||
// think to check server logs (we hope?)
|
||||
closePopup(res);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle /_oauth/* paths and extract the service name
|
||||
//
|
||||
// @returns {String|null} e.g. "facebook", or null if this isn't an
|
||||
// oauth request
|
||||
var oauthServiceName = function (req) {
|
||||
|
||||
// req.url will be "/_oauth/<service name>?<action>"
|
||||
var barePath = req.url.substring(0, req.url.indexOf('?'));
|
||||
var splitPath = barePath.split('/');
|
||||
|
||||
// Any non-oauth request will continue down the default
|
||||
// middlewares.
|
||||
if (splitPath[1] !== '_oauth')
|
||||
return null;
|
||||
|
||||
// Find service based on url
|
||||
var serviceName = splitPath[2];
|
||||
return serviceName;
|
||||
};
|
||||
|
||||
// Make sure we're configured
|
||||
var ensureConfigured = function(serviceName) {
|
||||
var service = Meteor.accounts[serviceName];
|
||||
|
||||
_.each(service._requireConfigs, function(key) {
|
||||
if (!service[key])
|
||||
throw new Meteor.accounts.ConfigError(
|
||||
"Need to call Meteor.accounts." + serviceName + ".config first");
|
||||
});
|
||||
|
||||
if (Meteor.is_server && !service._secret)
|
||||
throw new Meteor.accounts.ConfigError(
|
||||
"Need to call Meteor.accounts." + serviceName + ".setSecret first");
|
||||
};
|
||||
|
||||
Meteor.accounts.oauth._renderOauthResults = function(res, query) {
|
||||
// We support ?close and ?redirect=URL. Any other query should
|
||||
// just serve a blank page
|
||||
if ('close' in query) { // check with 'in' because we don't set a value
|
||||
closePopup(res);
|
||||
} else if (query.redirect) {
|
||||
res.writeHead(302, {'Location': query.redirect});
|
||||
res.end();
|
||||
} else {
|
||||
res.writeHead(200, {'Content-Type': 'text/html'});
|
||||
res.end('', 'utf-8');
|
||||
}
|
||||
};
|
||||
|
||||
var closePopup = function(res) {
|
||||
res.writeHead(200, {'Content-Type': 'text/html'});
|
||||
var content =
|
||||
'<html><head><script>window.close()</script></head></html>';
|
||||
res.end(content, 'utf-8');
|
||||
};
|
||||
|
||||
})();
|
||||
|
||||
|
||||
12
packages/accounts-oauth-helper/package.js
Normal file
12
packages/accounts-oauth-helper/package.js
Normal file
@@ -0,0 +1,12 @@
|
||||
Package.describe({
|
||||
summary: "Common code for OAuth-based login services",
|
||||
internal: true
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.use('accounts', ['client', 'server']);
|
||||
|
||||
api.add_files('oauth_common.js', ['client', 'server']);
|
||||
api.add_files('oauth_client.js', 'client');
|
||||
api.add_files('oauth_server.js', 'server');
|
||||
});
|
||||
137
packages/accounts-oauth1-helper/oauth1_binding.js
Normal file
137
packages/accounts-oauth1-helper/oauth1_binding.js
Normal file
@@ -0,0 +1,137 @@
|
||||
var crypto = __meteor_bootstrap__.require("crypto");
|
||||
var querystring = __meteor_bootstrap__.require("querystring");
|
||||
|
||||
// An OAuth1 wrapper around http calls which helps get tokens and
|
||||
// takes care of HTTP headers
|
||||
//
|
||||
// @param consumerKey {String} As supplied by the OAuth1 provider
|
||||
// @param consumerSecret {String} As supplied by the OAuth1 provider
|
||||
// @param urls {Object}
|
||||
// - requestToken (String): url
|
||||
// - authorize (String): url
|
||||
// - accessToken (String): url
|
||||
// - authenticate (String): url
|
||||
OAuth1Binding = function(consumerKey, consumerSecret, urls) {
|
||||
this._consumerKey = consumerKey;
|
||||
this._secret = consumerSecret;
|
||||
this._urls = urls;
|
||||
};
|
||||
|
||||
OAuth1Binding.prototype.prepareRequestToken = function(callbackUrl) {
|
||||
var self = this;
|
||||
|
||||
var headers = self._buildHeader({
|
||||
oauth_callback: callbackUrl
|
||||
});
|
||||
|
||||
var response = self._call('POST', self._urls.requestToken, headers);
|
||||
var tokens = querystring.parse(response.content);
|
||||
|
||||
// XXX should we also store oauth_token_secret here?
|
||||
if (!tokens.oauth_callback_confirmed)
|
||||
throw new Error("oauth_callback_confirmed false when requesting oauth1 token", tokens);
|
||||
self.requestToken = tokens.oauth_token;
|
||||
};
|
||||
|
||||
OAuth1Binding.prototype.prepareAccessToken = function(query) {
|
||||
var self = this;
|
||||
|
||||
var headers = self._buildHeader({
|
||||
oauth_token: query.oauth_token
|
||||
});
|
||||
|
||||
var params = {
|
||||
oauth_verifier: query.oauth_verifier
|
||||
};
|
||||
|
||||
var response = self._call('POST', self._urls.accessToken, headers, params);
|
||||
var tokens = querystring.parse(response.content);
|
||||
|
||||
self.accessToken = tokens.oauth_token;
|
||||
self.accessTokenSecret = tokens.oauth_token_secret;
|
||||
};
|
||||
|
||||
OAuth1Binding.prototype.call = function(method, url) {
|
||||
var self = this;
|
||||
|
||||
var headers = self._buildHeader({
|
||||
oauth_token: self.accessToken
|
||||
});
|
||||
|
||||
var response = self._call(method, url, headers);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
OAuth1Binding.prototype.get = function(url) {
|
||||
return this.call('GET', url);
|
||||
};
|
||||
|
||||
OAuth1Binding.prototype._buildHeader = function(headers) {
|
||||
var self = this;
|
||||
return _.extend({
|
||||
oauth_consumer_key: self._consumerKey,
|
||||
oauth_nonce: Meteor.uuid().replace(/\W/g, ''),
|
||||
oauth_signature_method: 'HMAC-SHA1',
|
||||
oauth_timestamp: (new Date().valueOf()/1000).toFixed().toString(),
|
||||
oauth_version: '1.0'
|
||||
}, headers);
|
||||
};
|
||||
|
||||
OAuth1Binding.prototype._getSignature = function(method, url, rawHeaders, accessTokenSecret) {
|
||||
var self = this;
|
||||
var headers = self._encodeHeader(rawHeaders);
|
||||
|
||||
var parameters = _.map(headers, function(val, key) {
|
||||
return key + '=' + val;
|
||||
}).sort().join('&');
|
||||
|
||||
var signatureBase = [
|
||||
method,
|
||||
encodeURIComponent(url),
|
||||
encodeURIComponent(parameters)
|
||||
].join('&');
|
||||
|
||||
var signingKey = encodeURIComponent(self._secret) + '&';
|
||||
if (accessTokenSecret)
|
||||
signingKey += encodeURIComponent(accessTokenSecret);
|
||||
|
||||
return crypto.createHmac('SHA1', signingKey).update(signatureBase).digest('base64');
|
||||
};
|
||||
|
||||
OAuth1Binding.prototype._call = function(method, url, headers, params) {
|
||||
var self = this;
|
||||
|
||||
// Get the signature
|
||||
headers.oauth_signature = self._getSignature(method, url, headers, self.accessTokenSecret);
|
||||
|
||||
// Make a authorization string according to oauth1 spec
|
||||
var authString = self._getAuthHeaderString(headers);
|
||||
|
||||
// Make signed request
|
||||
var response = Meteor.http.call(method, url, {
|
||||
params: params,
|
||||
headers: {
|
||||
Authorization: authString
|
||||
}
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
Meteor._debug('Error sending OAuth1 HTTP call', method, url, params, authString);
|
||||
throw response.error;
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
OAuth1Binding.prototype._encodeHeader = function(header) {
|
||||
return _.reduce(header, function(memo, val, key) {
|
||||
memo[encodeURIComponent(key)] = encodeURIComponent(val);
|
||||
return memo;
|
||||
}, {});
|
||||
};
|
||||
|
||||
OAuth1Binding.prototype._getAuthHeaderString = function(headers) {
|
||||
return 'OAuth ' + _.map(headers, function(val, key) {
|
||||
return encodeURIComponent(key) + '="' + encodeURIComponent(val) + '"';
|
||||
}).sort().join(', ');
|
||||
};
|
||||
1
packages/accounts-oauth1-helper/oauth1_common.js
Normal file
1
packages/accounts-oauth1-helper/oauth1_common.js
Normal file
@@ -0,0 +1 @@
|
||||
Meteor.accounts.oauth1 = {};
|
||||
66
packages/accounts-oauth1-helper/oauth1_server.js
Normal file
66
packages/accounts-oauth1-helper/oauth1_server.js
Normal file
@@ -0,0 +1,66 @@
|
||||
(function () {
|
||||
var connect = __meteor_bootstrap__.require("connect");
|
||||
|
||||
// A place to store request tokens pending verification
|
||||
Meteor.accounts.oauth1._requestTokens = {};
|
||||
|
||||
// connect middleware
|
||||
Meteor.accounts.oauth1._handleRequest = function (service, query, res) {
|
||||
|
||||
var config = Meteor.accounts[service.serviceName];
|
||||
var oauthBinding = new OAuth1Binding(config._consumerKey, config._secret, config._urls);
|
||||
|
||||
if (query.requestTokenAndRedirect) {
|
||||
// step 1 - get and store a request token
|
||||
|
||||
// Get a request token to start auth process
|
||||
oauthBinding.prepareRequestToken(query.requestTokenAndRedirect);
|
||||
|
||||
// Keep track of request token so we can verify it on the next step
|
||||
Meteor.accounts.oauth1._requestTokens[query.state] = oauthBinding.requestToken;
|
||||
|
||||
// redirect to provider login, which will redirect back to "step 2" below
|
||||
var redirectUrl = config._urls.authenticate + '?oauth_token=' + oauthBinding.requestToken;
|
||||
res.writeHead(302, {'Location': redirectUrl});
|
||||
res.end();
|
||||
|
||||
} else {
|
||||
// step 2, redirected from provider login - complete the login
|
||||
// process: if the user authorized permissions, get an access
|
||||
// token and access token secret and log in as user
|
||||
|
||||
// Get the user's request token so we can verify it and clear it
|
||||
var requestToken = Meteor.accounts.oauth1._requestTokens[query.state];
|
||||
delete Meteor.accounts.oauth1._requestTokens[query.state];
|
||||
|
||||
// Verify user authorized access and the oauth_token matches
|
||||
// the requestToken from previous step
|
||||
if (query.oauth_token && query.oauth_token === requestToken) {
|
||||
|
||||
// Prepare the login results before returning. This way the
|
||||
// subsequent call to the `login` method will be immediate.
|
||||
|
||||
// Get the access token for signing requests
|
||||
oauthBinding.prepareAccessToken(query);
|
||||
|
||||
// Get or create user id
|
||||
var oauthResult = service.handleOauthRequest(oauthBinding);
|
||||
var userId = Meteor.accounts.updateOrCreateUser(
|
||||
oauthResult.options, oauthResult.extra);
|
||||
|
||||
// Generate and store a login token for reconnect
|
||||
// XXX this could go in accounts_server.js instead
|
||||
var loginToken = Meteor.accounts._loginTokens.insert({userId: userId});
|
||||
|
||||
// Store results to subsequent call to `login`
|
||||
Meteor.accounts.oauth._loginResultForState[query.state] =
|
||||
{token: loginToken, id: userId};
|
||||
}
|
||||
}
|
||||
|
||||
// Either close the window, redirect, or render nothing
|
||||
// if all else fails
|
||||
Meteor.accounts.oauth._renderOauthResults(res, query);
|
||||
};
|
||||
|
||||
})();
|
||||
140
packages/accounts-oauth1-helper/oauth1_tests.js
Normal file
140
packages/accounts-oauth1-helper/oauth1_tests.js
Normal file
@@ -0,0 +1,140 @@
|
||||
|
||||
Tinytest.add("oauth1 - loginResultForState is stored", function (test) {
|
||||
var http = __meteor_bootstrap__.require('http');
|
||||
var twitterfooId = Meteor.uuid();
|
||||
var twitterfooName = 'nickname' + Meteor.uuid();
|
||||
var twitterfooAccessToken = Meteor.uuid();
|
||||
var twitterfooAccessTokenSecret = Meteor.uuid();
|
||||
|
||||
OAuth1Binding.prototype.prepareRequestToken = function() {};
|
||||
OAuth1Binding.prototype.prepareAccessToken = function() {
|
||||
this.accessToken = twitterfooAccessToken;
|
||||
this.accessTokenSecret = twitterfooAccessTokenSecret;
|
||||
};
|
||||
|
||||
// XXX XXX test isolation fail! Avital: but actually -- why would
|
||||
// we run server tests more than once? or even more so in parallel?
|
||||
Meteor.accounts._loginTokens.remove({});
|
||||
Meteor.accounts.oauth._loginResultForState = {};
|
||||
Meteor.accounts.oauth._services = {};
|
||||
|
||||
Meteor.accounts.twitterfoo = {};
|
||||
Meteor.accounts.twitterfoo._requireConfigs = [];
|
||||
Meteor.accounts.twitterfoo._secret = 'XXX';
|
||||
|
||||
// register a fake login service - twitterfoo
|
||||
Meteor.accounts.oauth.registerService("twitterfoo", 1, function (query) {
|
||||
return {
|
||||
options: {
|
||||
services: {
|
||||
twitter: {
|
||||
id: twitterfooId,
|
||||
screenName: twitterfooName,
|
||||
accessToken: twitterfooAccessToken,
|
||||
accessTokenSecret: twitterfooAccessTokenSecret
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// simulate logging in using twitterfoo
|
||||
Meteor.accounts.oauth1._requestTokens['STATE'] = twitterfooAccessToken;
|
||||
|
||||
var req = {
|
||||
method: "POST",
|
||||
url: "/_oauth/twitterfoo?close",
|
||||
query: {
|
||||
state: "STATE",
|
||||
oauth_token: twitterfooAccessToken
|
||||
}
|
||||
};
|
||||
|
||||
Meteor.accounts.oauth._middleware(req, new http.ServerResponse(req));
|
||||
|
||||
// verify that a user is created
|
||||
var user = Meteor.users.findOne({"services.twitter.screenName": twitterfooName});
|
||||
test.notEqual(user, undefined);
|
||||
test.equal(user.services.twitter.accessToken, twitterfooAccessToken);
|
||||
test.equal(user.services.twitter.accessTokenSecret, twitterfooAccessTokenSecret);
|
||||
|
||||
// and that that user has a login token
|
||||
var token = Meteor.accounts._loginTokens.findOne({userId: user._id});
|
||||
test.notEqual(token, undefined);
|
||||
|
||||
// and that the login result for that user is prepared
|
||||
test.equal(
|
||||
Meteor.accounts.oauth._loginResultForState['STATE'].id, user._id);
|
||||
test.equal(
|
||||
Meteor.accounts.oauth._loginResultForState['STATE'].token, token._id);
|
||||
});
|
||||
|
||||
|
||||
Tinytest.add("oauth1 - error in user creation", function (test) {
|
||||
var http = __meteor_bootstrap__.require('http');
|
||||
var state = Meteor.uuid();
|
||||
var twitterfailId = Meteor.uuid();
|
||||
var twitterfailName = 'nickname' + Meteor.uuid();
|
||||
var twitterfailAccessToken = Meteor.uuid();
|
||||
var twitterfailAccessTokenSecret = Meteor.uuid();
|
||||
|
||||
Meteor.accounts.twitterfail = {};
|
||||
Meteor.accounts.twitterfail._requireConfigs = [];
|
||||
Meteor.accounts.twitterfail._secret = 'XXX';
|
||||
|
||||
// Wire up access token so that verification passes
|
||||
Meteor.accounts.oauth1._requestTokens[state] = twitterfailAccessToken;
|
||||
|
||||
// register a failing login service
|
||||
Meteor.accounts.oauth.registerService("twitterfail", 1, function (query) {
|
||||
return {
|
||||
options: {
|
||||
services: {
|
||||
twitter: {
|
||||
id: twitterfailId,
|
||||
screenName: twitterfailName,
|
||||
accessToken: twitterfailAccessToken,
|
||||
accessTokenSecret: twitterfailAccessTokenSecret
|
||||
}
|
||||
}
|
||||
},
|
||||
extra: {
|
||||
invalid: true
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// a way to fail new users. duplicated from passwords_tests, but
|
||||
// shouldn't hurt.
|
||||
Meteor.accounts.validateNewUser(function (user) {
|
||||
return !user.invalid;
|
||||
});
|
||||
|
||||
// simulate logging in with failure
|
||||
Meteor._suppress_log(1);
|
||||
var req = {
|
||||
method: "POST",
|
||||
url: "/_oauth/twitterfail?close",
|
||||
query: {
|
||||
state: state,
|
||||
oauth_token: twitterfailAccessToken
|
||||
}
|
||||
};
|
||||
|
||||
Meteor.accounts.oauth._middleware(req, new http.ServerResponse(req));
|
||||
|
||||
// verify that a user is not created
|
||||
var user = Meteor.users.findOne({"services.twitter.screenName": twitterfailName});
|
||||
test.equal(user, undefined);
|
||||
|
||||
// verify an error is stored in login state
|
||||
test.equal(Meteor.accounts.oauth._loginResultForState[state].error, 403);
|
||||
|
||||
// verify error is handed back to login method.
|
||||
test.throws(function () {
|
||||
Meteor.apply('login', [{oauth: {version: 1, state: state}}]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
17
packages/accounts-oauth1-helper/package.js
Normal file
17
packages/accounts-oauth1-helper/package.js
Normal file
@@ -0,0 +1,17 @@
|
||||
Package.describe({
|
||||
summary: "Common code for OAuth1-based login services",
|
||||
internal: true
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.use('accounts-oauth-helper', 'client');
|
||||
|
||||
api.add_files('oauth1_binding.js', 'server');
|
||||
api.add_files('oauth1_common.js', ['client', 'server']);
|
||||
api.add_files('oauth1_server.js', 'server');
|
||||
});
|
||||
|
||||
Package.on_test(function (api) {
|
||||
api.use('accounts-oauth1-helper', 'server');
|
||||
api.add_files("oauth1_tests.js", 'server');
|
||||
});
|
||||
@@ -1,134 +1,31 @@
|
||||
(function () {
|
||||
var connect = __meteor_bootstrap__.require("connect");
|
||||
|
||||
Meteor.accounts.oauth2._services = {};
|
||||
|
||||
// Register a handler for an OAuth2 service. The handler will be called
|
||||
// when we get an incoming http request on /_oauth/{serviceName}. This
|
||||
// handler should use that information to fetch data about the user
|
||||
// logging in.
|
||||
//
|
||||
// @param name {String} e.g. "google", "facebook"
|
||||
// @param handleOauthRequest {Function(query)}
|
||||
// - query is an object with the parameters passed in the query string
|
||||
// - return value is:
|
||||
// - {options: (options), extra: (optional extra)} (same as the
|
||||
// arguments to Meteor.accounts.updateOrCreateUser)
|
||||
// - `null` if the user declined to give permissions
|
||||
Meteor.accounts.oauth2.registerService = function (name, handleOauthRequest) {
|
||||
if (Meteor.accounts.oauth2._services[name])
|
||||
throw new Error("Already registered the " + name + " OAuth2 service");
|
||||
|
||||
Meteor.accounts.oauth2._services[name] = {
|
||||
handleOauthRequest: handleOauthRequest
|
||||
};
|
||||
};
|
||||
|
||||
// Listen to calls to `login` with an oauth option set
|
||||
Meteor.accounts.registerLoginHandler(function (options) {
|
||||
if (!options.oauth)
|
||||
return undefined; // don't handle
|
||||
|
||||
var result = Meteor.accounts.oauth2._loginResultForState[options.oauth.state];
|
||||
if (result === undefined) // not using `!result` since can be null
|
||||
// We weren't notified of the user authorizing the login.
|
||||
return null;
|
||||
else if (result instanceof Error)
|
||||
// We tried to login, but there was a fatal error. Report it back
|
||||
// to the user.
|
||||
throw result;
|
||||
else
|
||||
return result;
|
||||
});
|
||||
|
||||
// When we get an incoming OAuth http request we complete the oauth
|
||||
// handshake, account and token setup before responding. The
|
||||
// results are stored in this map which is then read when the login
|
||||
// method is called. Maps state --> return value of `login`
|
||||
//
|
||||
// XXX we should periodically clear old entries
|
||||
Meteor.accounts.oauth2._loginResultForState = {};
|
||||
|
||||
// connect middleware
|
||||
Meteor.accounts.oauth2._handleRequest = function (req, res, next) {
|
||||
// req.url will be "/_oauth/<service name>?<action>"
|
||||
// NOTE: query param is mandatory.
|
||||
var barePath = req.url.substring(0, req.url.indexOf('?'));
|
||||
var splitPath = barePath.split('/');
|
||||
Meteor.accounts.oauth2._handleRequest = function (service, query, res) {
|
||||
// check if user authorized access
|
||||
if (!query.error) {
|
||||
// Prepare the login results before returning. This way the
|
||||
// subsequent call to the `login` method will be immediate.
|
||||
|
||||
// Any non-oauth request will continue down the default middlewares
|
||||
if (splitPath[1] !== '_oauth') {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure we prepare the login results before returning.
|
||||
// This way the subsequent call to the `login` method will be
|
||||
// immediate.
|
||||
|
||||
var serviceName = splitPath[2];
|
||||
var service = Meteor.accounts.oauth2._services[serviceName];
|
||||
|
||||
try {
|
||||
// Get or create user id
|
||||
var oauthResult = service && service.handleOauthRequest(req.query);
|
||||
var oauthResult = service.handleOauthRequest(query);
|
||||
|
||||
// could be null if user declined permissions, or if there was an
|
||||
// error of some sort.
|
||||
if (oauthResult && req.query.state) {
|
||||
var userId = Meteor.accounts.updateOrCreateUser(
|
||||
oauthResult.options, oauthResult.extra);
|
||||
var userId = Meteor.accounts.updateOrCreateUser(
|
||||
oauthResult.options, oauthResult.extra);
|
||||
|
||||
// Generate and store a login token for reconnect
|
||||
// XXX this could go in accounts_server.js instead
|
||||
var loginToken = Meteor.accounts._loginTokens.insert({userId: userId});
|
||||
// Generate and store a login token for reconnect
|
||||
// XXX this could go in accounts_server.js instead
|
||||
var loginToken = Meteor.accounts._loginTokens.insert({userId: userId});
|
||||
|
||||
// Store results to subsequent call to `login`
|
||||
Meteor.accounts.oauth2._loginResultForState[req.query.state] =
|
||||
{token: loginToken, id: userId};
|
||||
}
|
||||
} catch (err) {
|
||||
// if we got thrown an error, save it off, it will get passed to
|
||||
// the approporiate login call (if any) and reported there.
|
||||
//
|
||||
// The other option would be to display it in the popup tab that
|
||||
// is still open at this point, ignoring the 'close' or 'redirect'
|
||||
// we were passed. But then the developer wouldn't be able to
|
||||
// style the error or react to it in any way.
|
||||
if (req.query.state && err instanceof Error)
|
||||
Meteor.accounts.oauth2._loginResultForState[req.query.state] = err;
|
||||
|
||||
// also log to the server console, so the developer sees it.
|
||||
Meteor._debug("Exception in oauth2 handler", err);
|
||||
// Store results to subsequent call to `login`
|
||||
Meteor.accounts.oauth._loginResultForState[query.state] =
|
||||
{token: loginToken, id: userId};
|
||||
}
|
||||
|
||||
// We support ?close and ?redirect=URL. Any other query should
|
||||
// just serve a blank page
|
||||
if ('close' in req.query) { // check with 'in' because we don't set a value
|
||||
// Close the popup window
|
||||
res.writeHead(200, {'Content-Type': 'text/html'});
|
||||
var content =
|
||||
'<html><head><script>window.close()</script></head></html>';
|
||||
res.end(content, 'utf-8');
|
||||
} else if (req.query.redirect) {
|
||||
res.writeHead(302, {'Location': req.query.redirect});
|
||||
res.end();
|
||||
} else {
|
||||
res.writeHead(200, {'Content-Type': 'text/html'});
|
||||
res.end('', 'utf-8');
|
||||
}
|
||||
// Either close the window, redirect, or render nothing
|
||||
// if all else fails
|
||||
Meteor.accounts.oauth._renderOauthResults(res, query);
|
||||
};
|
||||
|
||||
// Listen on /_oauth/*
|
||||
__meteor_bootstrap__.app
|
||||
.use(connect.query())
|
||||
.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
|
||||
Fiber(function () {
|
||||
Meteor.accounts.oauth2._handleRequest(req, res, next);
|
||||
}).run();
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
@@ -6,11 +6,15 @@ Tinytest.add("oauth2 - loginResultForState is stored", function (test) {
|
||||
// XXX XXX test isolation fail! Avital: but actually -- why would
|
||||
// we run server tests more than once? or even more so in parallel?
|
||||
Meteor.accounts._loginTokens.remove({});
|
||||
Meteor.accounts.oauth2._loginResultForState = {};
|
||||
Meteor.accounts.oauth2._services = {};
|
||||
Meteor.accounts.oauth._loginResultForState = {};
|
||||
Meteor.accounts.oauth._services = {};
|
||||
|
||||
Meteor.accounts.foobook = {};
|
||||
Meteor.accounts.foobook._requireConfigs = [];
|
||||
Meteor.accounts.foobook._secret = 'XXX';
|
||||
|
||||
// register a fake login service - foobook
|
||||
Meteor.accounts.oauth2.registerService("foobook", function (query) {
|
||||
Meteor.accounts.oauth.registerService("foobook", 2, function (query) {
|
||||
return {
|
||||
options: {
|
||||
email: email,
|
||||
@@ -23,7 +27,7 @@ Tinytest.add("oauth2 - loginResultForState is stored", function (test) {
|
||||
var req = {method: "POST",
|
||||
url: "/_oauth/foobook?close",
|
||||
query: {state: "STATE"}};
|
||||
Meteor.accounts.oauth2._handleRequest(req, new http.ServerResponse(req));
|
||||
Meteor.accounts.oauth._middleware(req, new http.ServerResponse(req));
|
||||
|
||||
// verify that a user is created
|
||||
var user = Meteor.users.findOne({"emails.email": email});
|
||||
@@ -37,9 +41,9 @@ Tinytest.add("oauth2 - loginResultForState is stored", function (test) {
|
||||
|
||||
// and that the login result for that user is prepared
|
||||
test.equal(
|
||||
Meteor.accounts.oauth2._loginResultForState['STATE'].id, user._id);
|
||||
Meteor.accounts.oauth._loginResultForState['STATE'].id, user._id);
|
||||
test.equal(
|
||||
Meteor.accounts.oauth2._loginResultForState['STATE'].token, token._id);
|
||||
Meteor.accounts.oauth._loginResultForState['STATE'].token, token._id);
|
||||
});
|
||||
|
||||
|
||||
@@ -48,8 +52,12 @@ Tinytest.add("oauth2 - error in user creation", function (test) {
|
||||
var email = Meteor.uuid() + "@example.com";
|
||||
var state = Meteor.uuid();
|
||||
|
||||
Meteor.accounts.failbook = {};
|
||||
Meteor.accounts.failbook._requireConfigs = [];
|
||||
Meteor.accounts.failbook._secret = 'XXX';
|
||||
|
||||
// register a failing login service
|
||||
Meteor.accounts.oauth2.registerService("failbook", function (query) {
|
||||
Meteor.accounts.oauth.registerService("failbook", 2, function (query) {
|
||||
return {
|
||||
options: {
|
||||
email: email,
|
||||
@@ -72,14 +80,14 @@ Tinytest.add("oauth2 - error in user creation", function (test) {
|
||||
var req = {method: "POST",
|
||||
url: "/_oauth/failbook?close",
|
||||
query: {state: state}};
|
||||
Meteor.accounts.oauth2._handleRequest(req, new http.ServerResponse(req));
|
||||
Meteor.accounts.oauth._middleware(req, new http.ServerResponse(req));
|
||||
|
||||
// verify that a user is not created
|
||||
var user = Meteor.users.findOne({"emails.email": email});
|
||||
test.equal(user, undefined);
|
||||
|
||||
// verify an error is stored in login state
|
||||
test.equal(Meteor.accounts.oauth2._loginResultForState[state].error, 403);
|
||||
test.equal(Meteor.accounts.oauth._loginResultForState[state].error, 403);
|
||||
|
||||
// verify error is handed back to login method.
|
||||
test.throws(function () {
|
||||
@@ -87,3 +95,5 @@ Tinytest.add("oauth2 - error in user creation", function (test) {
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@ Package.describe({
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.use('accounts-oauth-helper', 'client');
|
||||
api.use('accounts', ['client', 'server']);
|
||||
|
||||
api.add_files('oauth2_common.js', ['client', 'server']);
|
||||
api.add_files('oauth2_server.js', 'server');
|
||||
api.add_files('oauth2_client.js', 'client');
|
||||
});
|
||||
|
||||
Package.on_test(function (api) {
|
||||
|
||||
13
packages/accounts-twitter/package.js
Normal file
13
packages/accounts-twitter/package.js
Normal file
@@ -0,0 +1,13 @@
|
||||
Package.describe({
|
||||
summary: "Login service for Twitter accounts"
|
||||
});
|
||||
|
||||
Package.on_use(function(api) {
|
||||
api.use('accounts', ['client', 'server']);
|
||||
api.use('accounts-oauth1-helper', ['client', 'server']);
|
||||
api.use('http', ['client', 'server']);
|
||||
|
||||
api.add_files('twitter_common.js', ['client', 'server']);
|
||||
api.add_files('twitter_server.js', 'server');
|
||||
api.add_files('twitter_client.js', 'client');
|
||||
});
|
||||
24
packages/accounts-twitter/twitter_client.js
Normal file
24
packages/accounts-twitter/twitter_client.js
Normal file
@@ -0,0 +1,24 @@
|
||||
(function () {
|
||||
Meteor.loginWithTwitter = function () {
|
||||
if (!Meteor.accounts.twitter._appUrl)
|
||||
throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.twitter.config first");
|
||||
|
||||
var state = Meteor.uuid();
|
||||
// We need to keep state across the next two 'steps' so we're adding
|
||||
// a state parameter to the url and the callback url that we'll be returned
|
||||
// to by oauth provider
|
||||
|
||||
// url back to app, enters "step 2" as described in
|
||||
// packages/accounts-oauth1-helper/oauth1_server.js
|
||||
var callbackUrl = Meteor.accounts.twitter._appUrl + '/_oauth/twitter?close&state=' + state;
|
||||
|
||||
// url to app, enters "step 1" as described in
|
||||
// packages/accounts-oauth1-helper/oauth1_server.js
|
||||
var url = '/_oauth/twitter/?requestTokenAndRedirect='
|
||||
+ encodeURIComponent(callbackUrl)
|
||||
+ '&state=' + state;
|
||||
|
||||
Meteor.accounts.oauth.initiateLogin(state, url);
|
||||
};
|
||||
|
||||
})();
|
||||
16
packages/accounts-twitter/twitter_common.js
Normal file
16
packages/accounts-twitter/twitter_common.js
Normal file
@@ -0,0 +1,16 @@
|
||||
if (!Meteor.accounts.twitter) {
|
||||
Meteor.accounts.twitter = {};
|
||||
Meteor.accounts.twitter._requireConfigs = ['_consumerKey', '_appUrl'];
|
||||
}
|
||||
|
||||
Meteor.accounts.twitter.config = function(consumerKey, appUrl) {
|
||||
Meteor.accounts.twitter._consumerKey = consumerKey;
|
||||
Meteor.accounts.twitter._appUrl = appUrl;
|
||||
};
|
||||
|
||||
Meteor.accounts.twitter._urls = {
|
||||
requestToken: "https://api.twitter.com/oauth/request_token",
|
||||
authorize: "https://api.twitter.com/oauth/authorize",
|
||||
accessToken: "https://api.twitter.com/oauth/access_token",
|
||||
authenticate: "https://api.twitter.com/oauth/authenticate"
|
||||
};
|
||||
26
packages/accounts-twitter/twitter_server.js
Normal file
26
packages/accounts-twitter/twitter_server.js
Normal file
@@ -0,0 +1,26 @@
|
||||
(function () {
|
||||
|
||||
Meteor.accounts.twitter.setSecret = function (consumerSecret) {
|
||||
Meteor.accounts.twitter._secret = consumerSecret;
|
||||
};
|
||||
|
||||
Meteor.accounts.oauth.registerService('twitter', 1, function(oauthBinding) {
|
||||
var identity = oauthBinding.get('https://api.twitter.com/1/account/verify_credentials.json');
|
||||
|
||||
return {
|
||||
options: {
|
||||
services: {
|
||||
twitter: {
|
||||
id: identity.id,
|
||||
screenName: identity.screen_name,
|
||||
accessToken: oauthBinding.accessToken,
|
||||
accessTokenSecret: oauthBinding.accessTokenSecret
|
||||
}
|
||||
}
|
||||
},
|
||||
extra: {
|
||||
name: identity.name
|
||||
}
|
||||
};
|
||||
});
|
||||
}) ();
|
||||
@@ -69,6 +69,19 @@
|
||||
};
|
||||
},
|
||||
|
||||
'click #login-buttons-Twitter': function () {
|
||||
try {
|
||||
Meteor.loginWithTwitter();
|
||||
} catch (e) {
|
||||
if (e instanceof Meteor.accounts.ConfigError)
|
||||
alert("Twitter API key not set. Configure app details with "
|
||||
+ "Meteor.accounts.twitter.config() and "
|
||||
+ "Meteor.accounts.twitter.setSecret()");
|
||||
else
|
||||
throw e;
|
||||
};
|
||||
},
|
||||
|
||||
'click #login-buttons-logout': function() {
|
||||
Meteor.logout();
|
||||
resetSession();
|
||||
@@ -540,6 +553,8 @@
|
||||
ret.push({name: 'Google'});
|
||||
if (Meteor.accounts.weibo)
|
||||
ret.push({name: 'Weibo'});
|
||||
if (Meteor.accounts.twitter)
|
||||
ret.push({name: 'Twitter'});
|
||||
|
||||
// make sure to put accounts last, since this is the order in the
|
||||
// ui as well
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* These should be in their respective packages */
|
||||
|
||||
#login-buttons-image-Google {
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAADCklEQVQ4jSXSy2ucVRjA4d97zvdNJpPJbTJJE9rYaCINShZtRCFIA1bbLryBUlyoLQjqVl12W7UbN4qb1gtuYhFRRBCDBITaesFbbI3RFBLSptEY05l0ZjLfnMvrov/Bs3gAcF71x6VVHTk+o8nDH+hrH89rUK9Z9Yaen57S3wVtGaMBNGC0IegWKIDxTtVaOHVugZVmH3HX3Zz+4l+W1xvkOjuZfPsspY4CNkZELEgEIJKwYlBjEwjec/mfCMVuorVs76R8+P0KYMmP30U2dT8eIZqAR2ipRcWjEYxGSCRhV08e04oYMoxYLi97EI9YCJ0FHBYbIVGDlUBLwRlLIuYW6chEmQt/rJO09RJjhjEJEYvJYGNhkbUhw43OXtIWDFRq9G87nAaSK6sVRm8r8fzRMWbOX2Xx7ypd7ZET03sQhDOz73DqSJOrd+7HSo4QIu0Nx/4rOzx+cRXZ9+z7+uqJ+3hiepxK3fHZT2tMjXYzOtzL6dmznPzhLexgN0QlxAAYxAlqUqRmkf5j59RlNQ6MFHhgcpCTTx8EUb5e+plD7x4jjg1ANCAgrRQAdR7xKXjBlGyLYi7PxaUmb8z8xcpGHVXLHaXdjI0egKyJiQYTEhSPREVIEUBNC+Mqm+xpz3j0njLPHB2nsh1QgeG+IS48dYbD5YNoo0ZUAbVEuTUoKuBSZOarX/WhyQn6eg2+usDWf0s0tq8zNPYk+WI/Lnge++hlvlyfQ3NdECzGRWKwEEA0qNY251n69kV6+Y0kbaCZoebG2X3oU7pKoyxuXOPe945zs9DCeosGIXoBDyaLdf6ce4Hbk+/Y299ksKtAuaeNsiyw8c1LKIZ95b0MdgxA5giixACpTxEPSau6QdFfI5/2cLPmEW+JAQrtJUJzDXF1dkwHzVodJMX4HFEcQQMaFdPeM0Jb/4PUtzzaLKAhRyJFwo6lbegRNFfk819muV5dR4JBQoQdQ2xFiDmSNDHiaptamR9Gq5cQ18AledrGDpOfeI5Lq8u88smbhMRisoSAgAYghdfn5H/JkHuRZ1owLAAAAABJRU5ErkJggg==);
|
||||
}
|
||||
@@ -9,3 +11,7 @@
|
||||
#login-buttons-image-Weibo {
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAKySURBVDhPY2AgEpR5sjf/nS/6//UkoX+XJltuCvVxkcOp9cyZM1w/r13TuXvmDD9MkYIwg7qrNrubnzFb6J5intPHqrnvCnIwyKIYsmrVKuaFYWEFW2Sk79zX0f6/REHhKFABC0zRsky+rXMSeZdKCTLIHqgUvLAknW8L3IAQDw/RFlbWnQ801P+DNN8D4n0qyk94GRiEjTg5Lbz4+YOCdbhjVmTxbZwex7PUW58t8O1Ukf9gA2IDAoRPWFudfayt9f+mpsb/6yrK/28qKf4/ISf7YZu83K07QMNe6On9nyWusMtVm813azH/UWctZo/vc8TABjB3CApufAzSqKjw/7apyf+nMdH/XxUX/X+RnfX/qY/3/5tqqv/vq6v936KsfB2onltaiEHGx5AteFep4EmGUEHB1Adamv9v6er8fztp0v//79////nr1/+3X778B4N///5/O3jw/0N39//nlBQ/louLd4MMAWImcPhsU1G6DfLvt717wepnz537X0FB4T8fL+//AH///2/evgWL/7l///9dE+P/b4AWTZSWXg/UzAj2/w2gs59mZYEV7d+//z8rE9N/JUXF/w62tiD//a+urIS4BAgeA712Cxg2F40M36alpXGBDTgmI/3hdUU5WEFjff3/wvx8MNvcxARsQE1VFUQ30Et37Oz+P1RV+b/J0nIjUATigmgBvtzH5mb//9++/f/mkyf/A4KC/nv7+oI1W1hb/3/1+fP//9+//39ekP//CVDzTlnZxxtnz1ZBSUDeDAyZh7W13nybOeP/7W1b/09rbf2/FhgWHy9c+P912bL/D11d/l+WEP8/SUR4Ox8DA6pmmEkpHh4ya0JCim4lJGx7kZp8821CwrN7Hh4Pr7m6nDoSET61PjDQichsA3T7//+s/16/5gXSkIAa1AAAh8dhOVd5xHAAAAAASUVORK5CYII=);
|
||||
}
|
||||
|
||||
#login-buttons-image-Twitter {
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAByklEQVQ4jaVTz0sbQRh92V10l006GaKJCtEtmqMYU0Qpwqb4B6zgXdT0WEr7B0ih4MGLP05CUWMvHkQwglhvGhsvKmJOBhTUQjWU2slilKarrAfdZROTQ8m7fPMx33szb75vXKZpohpwVbEBCNaCMUYopXppAWOMxDNsOPf3H1WIeDoSURYYYwQAKKW6y7KgLe2vam11KyMRZcEpEP6SOkwbUgc4ATAKUF8YW2fXhZejvaHPsc7gvH2DnCfQGEtdxrd/5NRJteUDpVTf+5kLp2WlA6JsCyZv9ChplPKdTfJZkYWhEF3bvnV3fb36NZSY3dP6Q/5V4hFvIAaKPckE8W5pLBIQdwHAthBdPtpJuhpeAwDu74DrP4/R1/Ts4cwBWg/gN+DowoSqTBPezAMAeAHw+suSw4Q7schFApF6af19a+2yLVIB7xR+0Zk75yCveu82FMnMViKHCXcSa3PPVBJAX5BszL2SP2kNwvdy5M1e+S2AogME4HFYPibPpxKZC03nRAp/M+Dx2UWDzTXfpttrx72ikCoVtrrAAwgdXBk9iazxxtpskfhs1O86aHXXpAEcA7ivJGDBDcDnyAsA2FMsi1KB/0bVv/EBBBSY9mZ7PAsAAAAASUVORK5CYII=);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
'&redirect_uri=' + Meteor.accounts.weibo._appUrl + '/_oauth/weibo?close' +
|
||||
'&state=' + state;
|
||||
|
||||
Meteor.accounts.oauth2.initiateLogin(state, loginUrl);
|
||||
Meteor.accounts.oauth.initiateLogin(state, loginUrl);
|
||||
};
|
||||
|
||||
}) ();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
if (!Meteor.accounts.weibo) {
|
||||
Meteor.accounts.weibo = {};
|
||||
Meteor.accounts.weibo._requireConfigs = ['_clientId', '_appUrl'];
|
||||
}
|
||||
|
||||
Meteor.accounts.weibo.config = function(clientId, appUrl) {
|
||||
|
||||
@@ -4,26 +4,18 @@
|
||||
Meteor.accounts.weibo._secret = secret;
|
||||
};
|
||||
|
||||
Meteor.accounts.oauth2.registerService('weibo', function(query) {
|
||||
if (query.error) {
|
||||
// The user didn't authorize access
|
||||
return null;
|
||||
}
|
||||
Meteor.accounts.oauth.registerService('weibo', 2, function(query) {
|
||||
|
||||
if (!Meteor.accounts.weibo._clientId || !Meteor.accounts.weibo._appUrl)
|
||||
throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.weibo.config first");
|
||||
if (!Meteor.accounts.weibo._secret)
|
||||
throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.weibo.setSecret first");
|
||||
|
||||
var result = getAccessToken(query);
|
||||
var identity = getIdentity(result.access_token, parseInt(result.uid, 10));
|
||||
var accessToken = getAccessToken(query);
|
||||
var identity = getIdentity(accessToken.access_token, parseInt(accessToken.uid, 10));
|
||||
|
||||
return {
|
||||
options: {
|
||||
services: {
|
||||
weibo: {
|
||||
id: result.uid,
|
||||
accessToken: result.accessToken
|
||||
id: accessToken.uid,
|
||||
accessToken: accessToken.accessToken,
|
||||
screenName: identity.screen_name
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user